// What holds the module to its own token (token.ts; hq issue 100, the forge's tools): minted with // the delivered admin account on the first call and kept at 0600, reused on the next start, minted // afresh when the forge rejects it or the kept file is gone, and a refused admin account reported in // plain words rather than crash-looped. A configured token still wins. And the tools register once // there is a way to a token at all — before one exists. // // The forge is a fake: the four routes the module touches, with the same status codes gitea gives. // Run against the compiled module (npm test builds first), the way the runtime loads it. import { test, after } from "node:test"; import assert from "node:assert/strict"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { collectTools } from "@novox/mesh-sdk/tools"; import { AdminRefused, MintedToken, TOKEN_SCOPES } from "../dist/token.js"; import { GiteaClient } from "../dist/client.js"; import "../dist/tools/index.js"; const ADMIN = "mesh-admin"; const PASSWORD = "the-vault-minted-this"; // ---- A fake forge: what the module sends, and what gitea would answer. ---- interface Forge { url: string; mints: number; lastScopes: string[] | null; tokens: Map; admins: Map; close(): Promise; } function fakeForge(): Promise { const forge = { mints: 0, lastScopes: null as string[] | null, tokens: new Map(), // name -> value scopesOf: new Map(), // value -> scopes, so a route can enforce them like gitea does admins: new Map([[ADMIN, PASSWORD]]), }; // write:X implies read:X — gitea's own rule (models/auth/access_token_scope.go). const covers = (scopes: string[], required: string): boolean => scopes.includes(required) || scopes.includes(`write:${required.split(":")[1]}`); const json = (res: ServerResponse, status: number, body: unknown): void => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(body === null ? "" : JSON.stringify(body)); }; const body = (req: IncomingMessage): Promise => new Promise((resolve) => { let text = ""; req.on("data", (c) => (text += c)); req.on("end", () => resolve(text ? JSON.parse(text) : null)); }); const basic = (req: IncomingMessage): string | null => { const h = req.headers.authorization ?? ""; if (!h.startsWith("Basic ")) return null; const [user, pass] = Buffer.from(h.slice(6), "base64").toString().split(":"); return forge.admins.get(user) === pass ? user : null; }; const server = createServer(async (req, res) => { const url = new URL(req.url ?? "/", "http://fake"); const tokens = url.pathname.match(/^\/api\/v1\/users\/([^/]+)\/tokens(?:\/([^/]+))?$/); if (tokens) { const user = basic(req); if (user === null || user !== decodeURIComponent(tokens[1])) return json(res, 401, { message: "auth required" }); if (req.method === "POST") { const { name, scopes } = await body(req); if (forge.tokens.has(name)) return json(res, 400, { message: "token name has already been used" }); forge.mints++; forge.lastScopes = scopes; const sha1 = `minted-${forge.mints}-${Math.random().toString(36).slice(2)}`; forge.tokens.set(name, sha1); forge.scopesOf.set(sha1, scopes); return json(res, 201, { id: forge.mints, name, sha1, scopes, token_last_eight: sha1.slice(-8) }); } if (req.method === "DELETE" && tokens[2]) { const name = decodeURIComponent(tokens[2]); if (!forge.tokens.has(name)) return json(res, 404, { message: "token not found" }); forge.tokens.delete(name); return json(res, 204, null); } return json(res, 405, { message: "method not allowed" }); } if (url.pathname === "/api/v1/user/repos") { const h = req.headers.authorization ?? ""; const value = h.startsWith("token ") ? h.slice(6) : ""; if (![...forge.tokens.values()].includes(value)) return json(res, 401, { message: "token is required" }); // gitea 1.27.3: GET /user/repos sits under the `user` scope category, not `repository` — // confirmed against the live forge. A token without read:user (or write:user) is refused here. const scopes = forge.scopesOf.get(value) ?? []; if (!covers(scopes, "read:user")) { return json(res, 403, { message: `token does not have at least one of required scope(s), required=[read:user]`, }); } return json(res, 200, [ { full_name: "novox/hq", name: "hq", owner: { login: "novox" }, private: true, html_url: "http://fake/novox/hq" }, ]); } return json(res, 404, { message: "no such route in the fake" }); }); return new Promise((resolve) => { server.listen(0, "127.0.0.1", () => { const { port } = server.address() as { port: number }; resolve({ url: `http://127.0.0.1:${port}`, get mints() { return forge.mints; }, get lastScopes() { return forge.lastScopes; }, tokens: forge.tokens, admins: forge.admins, close: () => new Promise((r) => server.close(() => r())), }); }); }); } // ---- What the runtime's environment gives the module. ---- async function delivered(forge: Forge): Promise<{ env: NodeJS.ProcessEnv; file: string; logs: string[] }> { const dir = await mkdtemp(join(tmpdir(), "gitea-")); const passwordFile = join(dir, "admin.secret"); await writeFile(passwordFile, PASSWORD + "\n", { mode: 0o600 }); const state = join(dir, "state"); return { env: { MESH_GITEA_URL: forge.url, MESH_GITEA_ADMIN_USER: ADMIN, MESH_GITEA_ADMIN_PASSWORD_FILE: passwordFile, MESH_GITEA_STATE_DIR: state, }, file: join(state, "token"), logs: [], }; } /** A client as a fresh process would build it: a new source over the kept file, its log captured. */ function minted(env: NodeJS.ProcessEnv, logs: string[]): GiteaClient { const source = new MintedToken({ url: env.MESH_GITEA_URL!, admin: env.MESH_GITEA_ADMIN_USER!, passwordFile: env.MESH_GITEA_ADMIN_PASSWORD_FILE!, file: join(env.MESH_GITEA_STATE_DIR!, "token"), log: (l) => logs.push(l), }); return new GiteaClient(env.MESH_GITEA_URL!, source); } const forge = await fakeForge(); after(() => forge.close()); test("first start: mints with the admin account, keeps the token at 0600, asks for two scopes only", async () => { const { env, file, logs } = await delivered(forge); const repos = await minted(env, logs).listRepos(); assert.equal(repos[0]?.full_name, "novox/hq"); assert.equal(forge.mints, 1); assert.deepEqual(forge.lastScopes, ["write:repository", "write:issue", "read:user"]); assert.deepEqual(forge.lastScopes, [...TOKEN_SCOPES]); const token = forge.tokens.get("mesh-tools")!; assert.equal(await readFile(file, "utf8"), token + "\n"); assert.equal((await stat(file)).mode & 0o777, 0o600); // Said that it minted, and where it keeps it — never what it is. assert.ok(logs.some((l) => l.startsWith("minted a token")), logs.join("\n")); assert.ok(logs.every((l) => !l.includes(token) && !l.includes(PASSWORD)), logs.join("\n")); }); test("second start: reuses the kept token, mints nothing", async () => { const { env, logs } = await delivered(forge); await minted(env, logs).listRepos(); const before = forge.mints; const again: string[] = []; await minted(env, again).listRepos(); assert.equal(forge.mints, before); assert.ok(again.some((l) => l.startsWith("reusing the token kept at")), again.join("\n")); assert.ok(again.every((l) => !l.includes(forge.tokens.get("mesh-tools")!)), again.join("\n")); }); test("the forge rejects the kept token (its data was restored): minted afresh, once, and the call goes through", async () => { const { env, file, logs } = await delivered(forge); const client = minted(env, logs); await client.listRepos(); const before = forge.mints; forge.tokens.clear(); // the forge no longer knows any token — a restore from the predecessor const repos = await client.listRepos(); assert.equal(repos.length, 1); assert.equal(forge.mints, before + 1); assert.equal(await readFile(file, "utf8"), forge.tokens.get("mesh-tools") + "\n"); assert.ok(logs.some((l) => l.startsWith("the forge rejected the kept token")), logs.join("\n")); }); test("the kept file is gone but the forge still holds a token by that name: replaced, not refused", async () => { const { env, file, logs } = await delivered(forge); await minted(env, logs).listRepos(); const before = forge.mints; await rm(file); const repos = await minted(env, logs).listRepos(); assert.equal(repos.length, 1); assert.equal(forge.mints, before + 1); assert.equal([...forge.tokens.keys()].filter((n) => n === "mesh-tools").length, 1); assert.ok(logs.some((l) => l.includes('already holds a token named "mesh-tools"')), logs.join("\n")); }); test("concurrent first calls share one mint", async () => { const { env, logs } = await delivered(forge); const client = minted(env, logs); const before = forge.mints; await Promise.all([client.listRepos(), client.listRepos(), client.listRepos()]); assert.equal(forge.mints, before + 1); }); test("the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing", async () => { const { env, file, logs } = await delivered(forge); forge.admins.delete(ADMIN); // the forge's data came from a predecessor; mesh-admin was never created there try { const client = minted(env, logs); const before = forge.mints; await assert.rejects(client.listRepos(), (err: unknown) => { assert.ok(err instanceof AdminRefused, String(err)); assert.match(err.message, /refused the admin account "mesh-admin" \(401\)/); assert.match(err.message, /admin-bootstrap step creates it/); assert.match(err.message, /came from a predecessor/); assert.ok(!err.message.includes(PASSWORD)); return true; }); await assert.rejects(client.listRepos(), AdminRefused); assert.equal(forge.mints, before); await assert.rejects(stat(file), /ENOENT/); // The account appears (the operator created it): the very next call mints and works. forge.admins.set(ADMIN, PASSWORD); assert.equal((await client.listRepos()).length, 1); assert.equal(forge.mints, before + 1); } finally { forge.admins.set(ADMIN, PASSWORD); } }); test("one process shares one source per kept file — the watcher and the tools never renew against each other", async () => { const { env } = await delivered(forge); assert.equal(MintedToken.fromEnv(env.MESH_GITEA_URL!, env), MintedToken.fromEnv(env.MESH_GITEA_URL!, env)); }); test("a second process finds the token the first renewed, and reuses it instead of minting over it", async () => { const { env, logs } = await delivered(forge); const first = minted(env, logs); const second = minted(env, logs); await first.listRepos(); await second.listRepos(); // both hold the same kept token const before = forge.mints; forge.tokens.clear(); await first.listRepos(); // renews: one mint await second.listRepos(); // rejected too — but the kept file already carries the renewed one assert.equal(forge.mints, before + 1); assert.ok(logs.some((l) => l.includes("is newer — reusing it")), logs.join("\n")); }); test("a configured token wins, and is reported rather than minted over when the forge rejects it", async () => { const { env } = await delivered(forge); const before = forge.mints; const client = GiteaClient.fromEnv({ ...env, MESH_GITEA_TOKEN: "one-somebody-pasted-in" }); await assert.rejects(client.listRepos(), /rejected the configured Gitea token \(401\)/); assert.equal(forge.mints, before); }); test("nothing to mint with and no token: the client says what is missing", async () => { assert.throws( () => GiteaClient.fromEnv({ MESH_GITEA_URL: forge.url, MESH_GITEA_ADMIN_USER: ADMIN }), /set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR/, ); }); test("the tools register once there is a way to a token, and the first call mints it", async () => { const { env } = await delivered(forge); const before = forge.mints; const withAdmin = collectTools(env).find((c) => c.module === "gitea")!; const withNothing = collectTools({}).find((c) => c.module === "gitea")!; assert.equal(withNothing.tools.length, 0); assert.deepEqual( withAdmin.tools.map((t) => t.name), [ "gitea_list_repos", "gitea_create_repo", "gitea_delete_repo", "gitea_list_issues", "gitea_get_issue", "gitea_create_issue", "gitea_close_issue", "gitea_add_comment", "gitea_list_pull_requests", "gitea_get_pull_request", "gitea_create_pull_request", "gitea_merge_pull_request", "gitea_list_labels", "gitea_create_label", "gitea_api", ], ); assert.equal(forge.mints, before, "registering must not mint — the forge may not be up yet"); const result = (await withAdmin.tools.find((t) => t.name === "gitea_list_repos")!.run({})) as { repos: unknown[] }; assert.equal(result.repos.length, 1); assert.equal(forge.mints, before + 1); });