Files
2026-07-13 13:39:12 +08:00

119 lines
4.1 KiB
JavaScript

import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { printSuccess, printError, printInfo } from "../io.mjs";
import { t } from "../i18n.mjs";
/**
* `omniroute tokens` — manage scoped CLI access tokens on the active (usually
* remote) server. Requires an `admin` credential — the commands hit
* /api/cli/tokens which is admin-only. Uses the active context's auth via
* apiFetch automatically.
*/
async function readErrorMessage(res) {
try {
const body = await res.json();
return body?.error?.message || body?.error || `HTTP ${res.status}`;
} catch {
return `HTTP ${res.status}`;
}
}
export function registerTokens(program) {
const tokens = program
.command("tokens")
.description(t("tokens.description") || "Manage scoped CLI access tokens (remote mode)");
tokens
.command("create")
.description("Create a new access token (requires admin scope)")
.requiredOption("--name <name>", "Human-readable token name")
.option("--scope <scope>", "Scope: read | write | admin", "read")
.option("--expires <days>", "Expire after N days (default: never)")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const body = { name: opts.name, scope: opts.scope };
if (opts.expires) {
const days = Number(opts.expires);
if (!Number.isFinite(days) || days <= 0) {
printError("--expires must be a positive number of days.");
process.exit(2);
}
body.expiresInDays = days;
}
const res = await apiFetch("/api/cli/tokens", {
...globalOpts,
method: "POST",
body,
acceptNotOk: true,
});
if (!res.ok) {
printError(`Could not create token: ${await readErrorMessage(res)}`);
process.exit(res.exitCode || 1);
}
const b = await res.json();
printSuccess(`Token '${b.name}' created (scope: ${b.scope}).`);
printInfo("Copy it now — it will NOT be shown again:");
process.stdout.write(`${b.token}\n`);
});
tokens
.command("list")
.description("List access tokens (masked)")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/cli/tokens", { ...globalOpts, acceptNotOk: true });
if (!res.ok) {
printError(`Could not list tokens: ${await readErrorMessage(res)}`);
process.exit(res.exitCode || 1);
}
const b = await res.json();
const rows = (b.tokens || []).map((tk) => ({
id: tk.id,
name: tk.name,
scope: tk.scope,
prefix: tk.tokenPrefix,
created: tk.createdAt,
lastUsed: tk.lastUsedAt || "",
expires: tk.expiresAt || "",
status: tk.revokedAt ? "revoked" : "active",
}));
emit(rows, globalOpts, [
{ key: "id", header: "ID" },
{ key: "name", header: "Name" },
{ key: "scope", header: "Scope" },
{ key: "prefix", header: "Prefix" },
{ key: "status", header: "Status" },
{ key: "lastUsed", header: "Last Used" },
{ key: "expires", header: "Expires" },
]);
});
tokens
.command("revoke <idOrPrefix>")
.description("Revoke an access token by id or display prefix")
.action(async (idOrPrefix, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch(`/api/cli/tokens/${encodeURIComponent(idOrPrefix)}`, {
...globalOpts,
method: "DELETE",
acceptNotOk: true,
});
if (!res.ok) {
printError(`Could not revoke token: ${await readErrorMessage(res)}`);
process.exit(res.exitCode || 1);
}
printSuccess(`Revoked ${idOrPrefix}.`);
});
tokens
.command("scopes")
.description("Explain the three access-token scopes")
.action(() => {
printInfo("Access-token scopes (admin ⊃ write ⊃ read):");
process.stdout.write(" read list/inspect only (models, status, logs, usage)\n");
process.stdout.write(" write read + configure/apply (setup-codex, keys add, config set)\n");
process.stdout.write(" admin write + manage (tokens, providers add, services, policy)\n");
});
}