Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152import type { CentralCommand } from "./centralArgs";import { DEFAULT_PORT } from "./centralArgs";
/** * The operator commands (`npm run central -- device add ...`, `job create ...`): each is one call to the running service's admin API. Everything that touches the * outside world is passed in, so this is tested against a real service over HTTP without a terminal. Device tokens are handed to `writeSecret` and never printed. */
export interface OperatorIo { fetch: typeof fetch; adminToken: string; print(text: string): void; error(text: string): void; /** Writes a secret to a file only its owner can read. */ writeSecret(path: string, text: string): void; writeFile(path: string, bytes: Uint8Array): void; readFile(path: string): string;}
export async function runOperator(c: Exclude<CentralCommand, { kind: "serve" }>, io: OperatorIo): Promise<number> { const base = c.url ?? `http://127.0.0.1:${DEFAULT_PORT}`; const call = async (method: string, path: string, body?: unknown) => { let raw: Response; try { raw = await io.fetch(`${base}/admin${path}`, { method, headers: { authorization: `Bearer ${io.adminToken}`, ...(body !== undefined ? { "content-type": "application/json" } : {}) }, ...(body !== undefined ? { body: JSON.stringify(body) } : {}) }); } catch (err) { return { status: 0, body: { error: `cannot reach ${base}: ${err instanceof Error ? err.message : String(err)}` }, raw: undefined }; } const isJson = raw.headers.get("content-type")?.includes("json") === true; return { status: raw.status, body: isJson ? ((await raw.clone().json().catch(() => undefined)) as any) : undefined, raw }; }; const show = (v: unknown) => io.print(JSON.stringify(v, null, 2)); const failed = (r: { status: number; body: any }): number => { io.error(`the service answered ${r.status || "nothing"}: ${r.body?.error ?? "no reason given"}`); return 1; }; const okStatus = (r: { status: number }) => r.status === 200 || r.status === 201;
switch (c.kind) { case "key": { const r = await call("GET", "/key"); if (!okStatus(r)) return failed(r); io.print(`release key (bundles and the revocation list): ${r.body.publicKey}`); io.print(`dispatch key (jobs): ${r.body.dispatchKey}`); return 0; } case "bundle-import": case "denylist-import": { let doc: unknown; try { doc = JSON.parse(io.readFile(c.file)); } catch (err) { io.error(`cannot read ${c.file}: ${err instanceof Error ? err.message : String(err)}`); return 1; } const r = await call("POST", c.kind === "bundle-import" ? "/bundles/import" : "/denylist", doc); if (!okStatus(r)) return failed(r); show(r.body); return 0; } case "bundle-revoke": { const r = await call("POST", "/bundles/revoke", { hashes: c.hashes }); if (!okStatus(r)) return failed(r); show(r.body); return 0; } case "enrol-create": { const r = await call("POST", "/enrolments", { person: c.person, origins: c.origins, ...(c.name !== undefined ? { name: c.name } : {}), ...(c.ttlMinutes !== undefined ? { ttlMs: c.ttlMinutes * 60_000 } : {}) }); if (!okStatus(r)) return failed(r); io.writeSecret(c.codeOut, `${r.body.code}\n`); io.print(`one-time enrolment code for ${c.person} is in ${c.codeOut}; valid until ${new Date(r.body.expiresAt).toISOString()}`); return 0; } case "device-list": { const r = await call("GET", "/devices"); if (!okStatus(r)) return failed(r); show(r.body); return 0; } case "device-revoke": { const r = await call("POST", `/devices/${encodeURIComponent(c.id)}/revoke`, {}); if (!okStatus(r)) return failed(r); io.print(`revoked ${c.id}`); return 0; } case "job-create": { const { kind: _k, dir: _d, url: _u, ...rest } = c; const r = await call("POST", "/jobs", rest); if (!okStatus(r)) return failed(r); show(r.body); return 0; } case "dispatch": { const { kind: _k, dir: _d, url: _u, ...rest } = c; const r = await call("POST", "/dispatch", rest); if (!okStatus(r)) { if (r.status === 409 && r.body?.considered !== undefined) { io.error(`no device matches: ${(r.body.considered as Array<{ name: string; why?: string }>).map((x) => `${x.name}: ${x.why}`).join("; ") || "no device is enrolled"}`); return 1; } return failed(r); } show(r.body); return 0; } case "stop": { const r = await call("POST", "/stop", { reason: c.reason }); if (!okStatus(r)) return failed(r); io.print(`stopped: ${c.reason}. Nothing is offered or taken, and jobs in progress were ended; devices find out at their next heartbeat.`); return 0; } case "resume": { const r = await call("POST", "/resume", {}); if (!okStatus(r)) return failed(r); io.print("resumed: waiting jobs were rung again"); return 0; } case "status": { const r = await call("GET", "/status"); if (!okStatus(r)) return failed(r); show(r.body); return 0; } case "job-list": { const q = new URLSearchParams({ ...(c.state ? { state: c.state } : {}), ...(c.device ? { device: c.device } : {}) }).toString(); const r = await call("GET", `/jobs${q ? `?${q}` : ""}`); if (!okStatus(r)) return failed(r); show(r.body); return 0; } case "job-show": { const r = await call("GET", `/jobs/${encodeURIComponent(c.id)}`); if (!okStatus(r)) return failed(r); show(r.body); return 0; } case "job-cancel": { const r = await call("POST", `/jobs/${encodeURIComponent(c.id)}/cancel`, {}); if (!okStatus(r)) return failed(r); io.print(`cancelled ${c.id}`); return 0; } case "job-archive": { const r = await call("GET", `/jobs/${encodeURIComponent(c.id)}/archive`); if (!okStatus(r) || r.raw === undefined) return failed(r); io.writeFile(c.out, new Uint8Array(await r.raw.arrayBuffer())); io.print(`wrote ${c.out}`); return 0; } }}