c83ddf62a0
A member's tmux server keeps the version it started with until their last pane closes, but the dashboard asked it with TMUX_BIN, the newest tmux: a 3.8 client on a 3.6 server spun it at 100% CPU (three members needed a manual kill on 2026-10-15). - /opt/agenthub/libexec/tmux-member (install-system, in sudoers): runs a member's tmux call with the binary of THEIR running server (/proc/<pid>/exe, found from the socket in /proc/net/unix), else TMUX_BIN. It runs as the member and execs only a process they own. sudoArgv uses it once installed (linux-accounts readConf), TMUX_BIN until then. - Users → Members' tmux (GET /api/users/tmux): each member server's version beside the installed one, sessions, agents working. An admin restarts one (POST /api/users/:id/tmux-restart) or every idle outdated one (POST /api/users/tmux-restart-idle), with their password again, audited per member; a server that will not stop answers 409 ETMUXSTUCK naming its pid for root. - server-s3-system.test.js: `delete` is a provision action since Delete user, so the "unknown action" case now uses `purge`. Operators: run `sudo agenthub server install-system` once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
817 lines
27 KiB
JavaScript
817 lines
27 KiB
JavaScript
/**
|
|
* @file OpenAPI 3.0 fragment for server mode's login routes (server/routes/auth.js):
|
|
* - GET /api/auth/me (Auth)
|
|
* - POST /api/auth/login (Auth)
|
|
* - POST /api/auth/logout (Auth)
|
|
* - GET /api/accounts (Auth) — the Claude and Codex account pools, no keys or tokens
|
|
* - POST /api/accounts, DELETE /api/accounts/{name} (Auth) — an admin adds/removes one
|
|
* - GET /api/users (Auth) — every account's usage (admin/analyst)
|
|
* - GET /api/users/audit (Auth) — staff reads of developers' sessions
|
|
* - GET/PUT /api/me/prefs, POST /api/me/onboarded (Auth) — the account's own UI preferences
|
|
* - GET/PUT /api/me/instructions (Auth) — the account's own agent instructions
|
|
* - GET/PATCH /api/me/account, POST /api/me/password, POST /api/me/sign-out-others
|
|
* (Auth) — Settings → Account: the account's own profile, password and logins
|
|
* - GET/PUT/DELETE /api/me/avatar (Auth) — the account's own picture
|
|
* - GET/PATCH /api/users/{id} (Auth) — the Users page's Edit user form
|
|
* - GET /api/users/{id}/avatar (Auth) — one account's picture (admin/analyst)
|
|
* - GET /api/users/tmux, POST /api/users/{id}/tmux-restart, POST /api/users/tmux-restart-idle
|
|
* (Auth) — each member's own tmux server and an admin's restart of it (S3)
|
|
* Exports `{ tags, schemas, paths }`, combined into the spec by
|
|
* server/openapi-extra.js. See docs/SERVER-MODE.md.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const tags = [
|
|
{
|
|
name: "Auth",
|
|
description:
|
|
'Server mode\'s login (AGENTHUB_MODE=server). In local mode — the default — only GET /api/auth/me answers, with {mode: "local"}.',
|
|
},
|
|
];
|
|
|
|
const schemas = {
|
|
UserPrefs: {
|
|
type: "object",
|
|
required: ["prefs", "onboardedAt"],
|
|
properties: {
|
|
prefs: { type: "object", additionalProperties: { type: "string" } },
|
|
onboardedAt: { type: "string", nullable: true },
|
|
},
|
|
},
|
|
AuthUser: {
|
|
type: "object",
|
|
required: ["id", "username", "role"],
|
|
properties: {
|
|
id: { type: "integer" },
|
|
username: { type: "string" },
|
|
role: { type: "string", enum: ["admin", "analyst", "developer"] },
|
|
disabled: { type: "boolean" },
|
|
displayName: { type: "string", nullable: true },
|
|
email: { type: "string", nullable: true },
|
|
avatarAt: {
|
|
type: "string",
|
|
nullable: true,
|
|
description: "When the avatar was last set (null = none)",
|
|
},
|
|
createdAt: { type: "string", nullable: true },
|
|
},
|
|
},
|
|
MyAccount: {
|
|
type: "object",
|
|
required: ["mode"],
|
|
properties: {
|
|
mode: { type: "string", enum: ["local", "server"] },
|
|
user: { $ref: "#/components/schemas/AuthUser" },
|
|
workspaces: { type: "integer" },
|
|
sessions: { type: "integer" },
|
|
logins: { type: "integer", description: "Unexpired logins of the account" },
|
|
lastLoginAt: { type: "string", nullable: true },
|
|
},
|
|
},
|
|
AuthMe: {
|
|
type: "object",
|
|
required: ["mode"],
|
|
properties: {
|
|
mode: { type: "string", enum: ["local", "server"] },
|
|
user: {
|
|
description: "Server mode only: the logged-in user, or null.",
|
|
nullable: true,
|
|
allOf: [{ $ref: "#/components/schemas/AuthUser" }],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const err = (description) => ({
|
|
description,
|
|
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } },
|
|
});
|
|
|
|
// The Users page's account changes: every body carries the admin's own
|
|
// password again (`currentPassword`).
|
|
const userBody = {
|
|
"application/json": {
|
|
schema: { type: "object", properties: { user: { $ref: "#/components/schemas/AuthUser" } } },
|
|
},
|
|
};
|
|
const idParam = { name: "id", in: "path", required: true, schema: { type: "integer" } };
|
|
const changeBody = (properties, required = []) => ({
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: [...required, "currentPassword"],
|
|
properties: { ...properties, currentPassword: { type: "string" } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const changeResponses = {
|
|
200: { description: "Changed", content: userBody },
|
|
400: err("EBADPASSWORD / EBADROLE / EBADFIELD"),
|
|
403: err("EREAUTH (own password wrong) / EADMIN / ELOCALMODE"),
|
|
404: err("ENOUSER / ENOLANE"),
|
|
409: err("ESELF — your own role or disable"),
|
|
429: err("ETOOMANY — 10 wrong passwords in 15 minutes"),
|
|
};
|
|
|
|
const accountBody = {
|
|
"application/json": { schema: { $ref: "#/components/schemas/MyAccount" } },
|
|
};
|
|
const prefsBody = {
|
|
"application/json": { schema: { $ref: "#/components/schemas/UserPrefs" } },
|
|
};
|
|
const instrBody = {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["instructions", "max", "tools"],
|
|
properties: {
|
|
instructions: { type: "string" },
|
|
max: { type: "integer" },
|
|
tools: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "AI tool ids whose new panes receive the instructions (claude, codex)",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const paths = {
|
|
"/api/me/prefs": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
summary: "The signed-in account's own UI preferences",
|
|
description:
|
|
"Local mode is the one account `local`; server mode one row per account. Keys are the browser's localStorage names (docs/API.md).",
|
|
responses: { 200: { description: "Preferences", content: prefsBody }, 401: err("EAUTH") },
|
|
},
|
|
put: {
|
|
tags: ["Auth"],
|
|
summary: "Merge the account's preferences (null deletes a key)",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["prefs"],
|
|
properties: {
|
|
prefs: {
|
|
type: "object",
|
|
additionalProperties: { type: "string", nullable: true, maxLength: 8192 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: { description: "Saved", content: prefsBody },
|
|
400: err("EBADPREFS — an unknown key or a value that is not a short string"),
|
|
401: err("EAUTH"),
|
|
},
|
|
},
|
|
},
|
|
"/api/me/instructions": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
summary: "The account's own agent instructions",
|
|
description:
|
|
"Put before the workspace's own instructions in every new Claude/Codex pane of a workspace the account owns.",
|
|
responses: { 200: { description: "Instructions", content: instrBody }, 401: err("EAUTH") },
|
|
},
|
|
put: {
|
|
tags: ["Auth"],
|
|
summary: "Replace the account's agent instructions (blank or null clears)",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: { instructions: { type: "string", nullable: true, maxLength: 16000 } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: { description: "Saved", content: instrBody },
|
|
400: err("EBADFIELD — not a string"),
|
|
413: err("ETOOBIG — longer than 16 000 characters"),
|
|
401: err("EAUTH"),
|
|
},
|
|
},
|
|
},
|
|
"/api/me/onboarded": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
summary: "The account went through (or skipped) the first-run setup",
|
|
responses: { 200: { description: "Marked", content: prefsBody }, 401: err("EAUTH") },
|
|
},
|
|
},
|
|
"/api/me/account": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "getMyAccount",
|
|
summary: 'Settings → Account: the signed-in account (local mode: {mode: "local"})',
|
|
responses: {
|
|
200: { description: "Account", content: accountBody },
|
|
401: err("EAUTH"),
|
|
},
|
|
},
|
|
patch: {
|
|
tags: ["Auth"],
|
|
operationId: "updateMyAccount",
|
|
summary:
|
|
'The account\'s own display name and email (""/null clears). Login cookie only, audited',
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
displayName: { type: "string", nullable: true, maxLength: 100 },
|
|
email: { type: "string", nullable: true, maxLength: 254 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: { description: "Updated", content: accountBody },
|
|
400: err("EBADFIELD / EBADEMAIL"),
|
|
401: err("EAUTH — no login cookie (a pane token is refused)"),
|
|
403: err("ELOCALMODE"),
|
|
},
|
|
},
|
|
},
|
|
"/api/me/password": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "changeMyPassword",
|
|
summary:
|
|
"Change the account's own password: the current one required (throttled); this login stays, every other ends",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["currentPassword", "newPassword"],
|
|
properties: {
|
|
currentPassword: { type: "string" },
|
|
newPassword: { type: "string", minLength: 1 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: { description: "Changed ({ok: true})" },
|
|
400: err("EBADPASSWORD"),
|
|
401: err("EAUTH — no login cookie (a pane token is refused)"),
|
|
403: err("EREAUTH (current password wrong) / ELOCALMODE"),
|
|
429: err("ETOOMANY — 10 wrong passwords in 15 minutes"),
|
|
},
|
|
},
|
|
},
|
|
"/api/me/sign-out-others": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "signOutMyOtherLogins",
|
|
summary: "End every login of the account but this one; answers {ended}",
|
|
responses: {
|
|
200: { description: "Ended" },
|
|
401: err("EAUTH — no login cookie (a pane token is refused)"),
|
|
403: err("ELOCALMODE"),
|
|
},
|
|
},
|
|
},
|
|
"/api/me/avatar": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "getMyAvatar",
|
|
summary: "The account's own picture (nosniff, sandbox CSP)",
|
|
responses: {
|
|
200: { description: "The image (PNG, JPEG, GIF or WebP)" },
|
|
401: err("EAUTH"),
|
|
404: err("ENOAVATAR"),
|
|
},
|
|
},
|
|
put: {
|
|
tags: ["Auth"],
|
|
operationId: "setMyAvatar",
|
|
summary:
|
|
"Set the account's picture from the raw image bytes (PNG/JPEG/GIF/WebP, at most 1 MB). Login cookie only, audited",
|
|
requestBody: {
|
|
required: true,
|
|
content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } },
|
|
},
|
|
responses: {
|
|
200: { description: "Set", content: accountBody },
|
|
400: err("EBADIMAGE"),
|
|
401: err("EAUTH — no login cookie (a pane token is refused)"),
|
|
403: err("ELOCALMODE"),
|
|
413: err("ETOOBIG"),
|
|
},
|
|
},
|
|
delete: {
|
|
tags: ["Auth"],
|
|
operationId: "removeMyAvatar",
|
|
summary: "Remove the account's picture",
|
|
responses: {
|
|
200: { description: "Removed", content: accountBody },
|
|
401: err("EAUTH — no login cookie (a pane token is refused)"),
|
|
403: err("ELOCALMODE"),
|
|
},
|
|
},
|
|
},
|
|
"/api/users": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "listServerUsers",
|
|
summary:
|
|
"Every account with its workspaces, sessions and 7/30-day usage (admin/analyst; 403 EADMIN for a developer)",
|
|
responses: {
|
|
200: {
|
|
description: "Accounts",
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
users: {
|
|
type: "array",
|
|
items: {
|
|
allOf: [
|
|
{ $ref: "#/components/schemas/AuthUser" },
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
workspaces: { type: "integer" },
|
|
sessions: { type: "integer" },
|
|
last7d: {
|
|
type: "object",
|
|
properties: { cost: { type: "number" }, tokens: { type: "integer" } },
|
|
},
|
|
last30d: {
|
|
type: "object",
|
|
properties: { cost: { type: "number" }, tokens: { type: "integer" } },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
403: err("EADMIN — a developer"),
|
|
},
|
|
},
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "createServerUser",
|
|
summary:
|
|
"Admin: add an account. Needs the admin's own password again (currentPassword); audited",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["username", "password", "currentPassword"],
|
|
properties: {
|
|
username: { type: "string" },
|
|
password: { type: "string", minLength: 1 },
|
|
role: {
|
|
type: "string",
|
|
enum: ["admin", "analyst", "developer"],
|
|
default: "developer",
|
|
},
|
|
currentPassword: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
201: { description: "Created", content: userBody },
|
|
400: err("EBADUSER / EBADPASSWORD / EBADROLE"),
|
|
403: err("EREAUTH (own password wrong) / EADMIN / ELOCALMODE"),
|
|
409: err("EEXISTS"),
|
|
429: err("ETOOMANY — 10 wrong passwords in 15 minutes"),
|
|
},
|
|
},
|
|
},
|
|
"/api/users/tmux": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "listMemberTmux",
|
|
summary:
|
|
"Each member's own tmux server (one Linux user per account): its version beside the installed one, sessions, agents working (admin/analyst)",
|
|
responses: {
|
|
200: {
|
|
description:
|
|
"{available, installed, matched, users: [{id, username, running, stuck?, version, pid, startedAt, sessions, busy, outdated}]} — available false without S3",
|
|
},
|
|
403: err("EADMIN — a developer"),
|
|
},
|
|
},
|
|
},
|
|
"/api/users/{id}/tmux-restart": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "restartMemberTmux",
|
|
summary:
|
|
"End one member's tmux server — every pane of theirs closes; the next start runs the installed tmux (admin, own password again, audited)",
|
|
parameters: [idParam],
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["currentPassword"],
|
|
properties: { currentPassword: { type: "string" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: { description: "{user}" },
|
|
403: err("EREAUTH (own password wrong) / EADMIN / ELOCALMODE"),
|
|
404: err("ENOUSER"),
|
|
409: err(
|
|
"ETMUXSTUCK — the server did not stop (the message names the pid for root) / ENOS3"
|
|
),
|
|
},
|
|
},
|
|
},
|
|
"/api/users/tmux-restart-idle": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "restartIdleMemberTmux",
|
|
summary:
|
|
"Restart every member server older than the installed tmux while no agent of theirs works (admin, own password again, audited per member)",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["currentPassword"],
|
|
properties: { currentPassword: { type: "string" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description:
|
|
"{restarted: [username], skipped: [username — an agent working], failed: [{username, message}]}",
|
|
},
|
|
403: err("EREAUTH / EADMIN / ELOCALMODE"),
|
|
},
|
|
},
|
|
},
|
|
"/api/users/{id}/avatar": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "getUserAvatar",
|
|
summary: "One account's picture, for the sidebar's owner groups (admin/analyst)",
|
|
parameters: [{ name: "id", in: "path", required: true, schema: { type: "integer" } }],
|
|
responses: {
|
|
200: { description: "The image (PNG, JPEG, GIF or WebP)" },
|
|
403: err("EADMIN — a developer"),
|
|
404: err("ENOAVATAR"),
|
|
},
|
|
},
|
|
},
|
|
"/api/users/{id}": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "getServerUser",
|
|
summary: "One account with its logins and the workspaces it owns (admin/analyst)",
|
|
parameters: [idParam],
|
|
responses: {
|
|
200: { description: "Account" },
|
|
403: err("EADMIN — a developer"),
|
|
404: err("ENOUSER"),
|
|
},
|
|
},
|
|
patch: {
|
|
tags: ["Auth"],
|
|
operationId: "updateServerUser",
|
|
summary:
|
|
"Admin (Edit user): display name, email, role, disabled, a new password, workspaces given or taken back, sign out everywhere — every field validated before any is written. Never your own role or disable (ESELF)",
|
|
parameters: [idParam],
|
|
requestBody: changeBody({
|
|
displayName: { type: "string", nullable: true, maxLength: 100 },
|
|
email: { type: "string", nullable: true, maxLength: 254 },
|
|
role: { type: "string", enum: ["admin", "analyst", "developer"] },
|
|
disabled: { type: "boolean" },
|
|
password: { type: "string", minLength: 1 },
|
|
addWorkspaces: { type: "array", items: { type: "integer" } },
|
|
removeWorkspaces: { type: "array", items: { type: "integer" } },
|
|
signOut: { type: "boolean", enum: [true] },
|
|
}),
|
|
responses: {
|
|
...changeResponses,
|
|
400: err("EBADPASSWORD / EBADROLE / EBADFIELD / EBADEMAIL"),
|
|
409: err("ESELF / ENOTOWNED / EOWNERFOLDER"),
|
|
},
|
|
},
|
|
},
|
|
"/api/users/{id}/password": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "resetServerUserPassword",
|
|
summary: "Admin: a new password for that user; every login of theirs ends",
|
|
parameters: [idParam],
|
|
requestBody: changeBody({ password: { type: "string", minLength: 1 } }, ["password"]),
|
|
responses: changeResponses,
|
|
},
|
|
},
|
|
"/api/users/{id}/workspaces": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "assignServerUserWorkspace",
|
|
summary: "Admin: make that user the owner of a workspace (lane)",
|
|
parameters: [idParam],
|
|
requestBody: changeBody({ laneId: { type: "integer" } }, ["laneId"]),
|
|
responses: changeResponses,
|
|
},
|
|
},
|
|
"/api/users/audit": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "listAuditLog",
|
|
summary: "Admin/analyst reads of sessions developers own, newest first",
|
|
parameters: [{ name: "limit", in: "query", schema: { type: "integer", maximum: 1000 } }],
|
|
responses: {
|
|
200: {
|
|
description: "Entries",
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
entries: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "integer" },
|
|
at: { type: "string" },
|
|
method: { type: "string" },
|
|
path: { type: "string" },
|
|
session_id: { type: "string", nullable: true },
|
|
reader: { type: "string", nullable: true },
|
|
owner: { type: "string", nullable: true },
|
|
reader_display: { type: "string", nullable: true },
|
|
owner_display: { type: "string", nullable: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
403: err("EADMIN — a developer"),
|
|
},
|
|
},
|
|
},
|
|
"/api/accounts": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "listPoolAccounts",
|
|
summary:
|
|
"The Claude and Codex account pools as the balancer sees them (never a key or token)",
|
|
parameters: [
|
|
{
|
|
name: "force",
|
|
in: "query",
|
|
schema: { type: "string", enum: ["1"] },
|
|
description: "Re-read every account's quota first",
|
|
},
|
|
],
|
|
responses: {
|
|
200: {
|
|
description: "Accounts with live panes, recent spend and quota",
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
blockPercent: { type: "number" },
|
|
accounts: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
addedAt: { type: "string" },
|
|
expiresAt: { type: "string" },
|
|
expired: { type: "boolean" },
|
|
livePanes: { type: "integer" },
|
|
spend5h: { type: "number" },
|
|
spend7d: { type: "number" },
|
|
tokens5h: { type: "integer" },
|
|
tokens7d: { type: "integer" },
|
|
quota: { type: "object", nullable: true },
|
|
quotaPercent: { type: "number", nullable: true },
|
|
full: { type: "boolean" },
|
|
},
|
|
},
|
|
},
|
|
codex: {
|
|
type: "array",
|
|
description:
|
|
"The Codex account pool (server/lib/codex-pool.js), same fields plus `kind`; no expiry",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
kind: { type: "string", enum: ["apikey", "chatgpt"] },
|
|
addedAt: { type: "string" },
|
|
livePanes: { type: "integer" },
|
|
spend5h: { type: "number" },
|
|
spend7d: { type: "number" },
|
|
tokens5h: { type: "integer" },
|
|
tokens7d: { type: "integer" },
|
|
quota: { type: "object", nullable: true },
|
|
quotaPercent: { type: "number", nullable: true },
|
|
full: { type: "boolean" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "addPoolAccount",
|
|
summary:
|
|
"Admin: add a Claude (saved login, pasted credentials or setup-token) or Codex (API key / auth.json text) account to the pool, with their own password again. The secret is never answered back",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["tool", "currentPassword"],
|
|
properties: {
|
|
tool: { type: "string", enum: ["claude", "codex"] },
|
|
name: { type: "string", description: "Defaults to the profile's name" },
|
|
profile: {
|
|
type: "string",
|
|
description: "Claude: a saved switch-user profile, moved into the pool",
|
|
},
|
|
credentials: {
|
|
type: "string",
|
|
description: "Claude: the text of a .credentials.json",
|
|
},
|
|
token: {
|
|
type: "string",
|
|
description: "Claude: a setup-token; Codex: an API key or auth.json text",
|
|
},
|
|
currentPassword: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
201: { description: "`{account: {name, addedAt, …}}` — no token" },
|
|
400: err("EBADTOOL, EBADNAME, EBADTOKEN"),
|
|
403: err("EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE"),
|
|
409: err(
|
|
"EEXISTS — the name or account is taken; EACTIVE — the host's CLI uses that profile"
|
|
),
|
|
502: err("EUPSTREAM — Anthropic could not be reached"),
|
|
429: err("ETOOMANY — too many wrong passwords"),
|
|
},
|
|
},
|
|
},
|
|
"/api/accounts/profiles": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "listPoolProfiles",
|
|
summary: "Saved switch-user profiles an admin may move into the Claude pool (no token)",
|
|
responses: {
|
|
200: { description: "`{profiles: [{name, label, email, subscriptionType, active}]}`" },
|
|
},
|
|
},
|
|
},
|
|
"/api/accounts/{name}": {
|
|
delete: {
|
|
tags: ["Auth"],
|
|
operationId: "removePoolAccount",
|
|
summary: "Admin: remove an account from the pool, with their own password again",
|
|
parameters: [{ name: "name", in: "path", required: true, schema: { type: "string" } }],
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["tool", "currentPassword"],
|
|
properties: {
|
|
tool: { type: "string", enum: ["claude", "codex"] },
|
|
currentPassword: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: { description: "`{ok: true}`" },
|
|
403: err("EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE"),
|
|
404: err("ENOACCOUNT"),
|
|
429: err("ETOOMANY — too many wrong passwords"),
|
|
},
|
|
},
|
|
},
|
|
"/api/auth/me": {
|
|
get: {
|
|
tags: ["Auth"],
|
|
operationId: "authMe",
|
|
summary: "The dashboard's mode and, in server mode, who is logged in",
|
|
responses: {
|
|
200: {
|
|
description: "Mode and user",
|
|
content: { "application/json": { schema: { $ref: "#/components/schemas/AuthMe" } } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/api/auth/login": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "authLogin",
|
|
summary: "Log in; sets the httpOnly, SameSite=Strict cookie agenthub_sid",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
required: ["username", "password"],
|
|
properties: { username: { type: "string" }, password: { type: "string" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "Logged in",
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: { user: { $ref: "#/components/schemas/AuthUser" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
401: err("EBADLOGIN — wrong username or password, or a disabled user"),
|
|
403: err("EBADORIGIN — a cross-origin request"),
|
|
404: err("ENOTSERVER — the dashboard is in local mode"),
|
|
429: err("ETOOMANY — too many failed logins from this address (15 minutes)"),
|
|
},
|
|
},
|
|
},
|
|
"/api/auth/logout": {
|
|
post: {
|
|
tags: ["Auth"],
|
|
operationId: "authLogout",
|
|
summary: "End this login and clear its cookie",
|
|
responses: {
|
|
200: {
|
|
description: "Logged out",
|
|
content: {
|
|
"application/json": {
|
|
schema: { type: "object", properties: { ok: { type: "boolean" } } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
module.exports = { tags, schemas, paths };
|