{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../src/astro/routes/api/admin/api-tokens/index.ts"],"sourcesContent":["/**\n * API token management endpoints\n *\n * GET  /_emdash/api/admin/api-tokens — List tokens for current user\n * POST /_emdash/api/admin/api-tokens — Create a new token\n *\n * Tokens are minted from policies. A token can only carry policies its\n * owner's role holds (an admin whose grants are unrestricted may mint any),\n * and at authentication time its grants are additionally intersected with\n * the owner's, so a later narrowing of the role narrows every token too.\n *\n * Tokens cannot be managed while authenticated *by* a token. A leaked token\n * must not be able to mint successors or revoke its siblings — that is a\n * session-only operation.\n */\n\nimport type { APIRoute } from \"astro\";\nimport { z } from \"zod\";\n\nimport { requirePerm } from \"#api/authorize.js\";\nimport { apiError, handleError, unwrapResult } from \"#api/error.js\";\nimport { handleApiTokenCreate, handleApiTokenList } from \"#api/handlers/api-tokens.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { AuthzRepository } from \"#db/repositories/authz.js\";\n\nexport const prerender = false;\n\nconst createTokenSchema = z.object({\n\tname: z.string().min(1).max(100),\n\t/** Policy slugs the token is minted from — at least one. */\n\tpolicies: z.array(z.string().min(1)).min(1).max(100),\n\texpiresAt: z.string().datetime().optional(),\n\t/** Opt the token into CORS: browser code on any origin may attach it. */\n\tcors: z.boolean().optional(),\n});\n\n/** Refuse token management when the caller is itself a token. */\nfunction rejectTokenAuth(locals: App.Locals): Response | null {\n\tif (!locals.tokenAuth) return null;\n\treturn apiError(\n\t\t\"TOKEN_AUTH_FORBIDDEN\",\n\t\t\"API tokens cannot be created or revoked using an API token. Sign in to the admin to manage tokens.\",\n\t\t403,\n\t);\n}\n\n/**\n * List API tokens for the current user.\n */\nexport const GET: APIRoute = async ({ locals }) => {\n\tconst { emdash, user } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\tconst denied = requirePerm(user, \"api_tokens:manage\");\n\tif (denied) return denied;\n\n\tconst result = await handleApiTokenList(emdash.db, user!.id);\n\treturn unwrapResult(result);\n};\n\n/**\n * Create a new API token.\n * Returns the raw token once — it cannot be retrieved again.\n */\nexport const POST: APIRoute = async ({ request, locals }) => {\n\tconst { emdash, user, authz } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\tconst tokenAuth = rejectTokenAuth(locals);\n\tif (tokenAuth) return tokenAuth;\n\n\tconst denied = requirePerm(user, \"api_tokens:manage\");\n\tif (denied) return denied;\n\n\ttry {\n\t\tconst body = await parseBody(request, createTokenSchema);\n\t\tif (isParseError(body)) return body;\n\n\t\t{\n\t\t\tif (!authz) {\n\t\t\t\treturn apiError(\"NOT_CONFIGURED\", \"Authorization was not resolved for this request\", 500);\n\t\t\t}\n\n\t\t\t// A token may only carry policies its owner holds, unless the owner\n\t\t\t// is unrestricted. Reject rather than silently clamp: an operator\n\t\t\t// should learn that the policy they asked for is not theirs to give.\n\t\t\tconst unrestricted = authz.ownerGrants.permissions.has(\"*\");\n\t\t\tconst held = new Set(authz.rolePolicies);\n\t\t\tconst notHeld = unrestricted ? [] : body.policies.filter((slug) => !held.has(slug));\n\t\t\tif (notHeld.length > 0) {\n\t\t\t\treturn apiError(\n\t\t\t\t\t\"POLICY_NOT_HELD\",\n\t\t\t\t\t`Your role does not hold: ${notHeld.join(\", \")}. A token cannot carry policies its owner lacks.`,\n\t\t\t\t\t403,\n\t\t\t\t\t{ notHeld },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst repo = new AuthzRepository(emdash.db);\n\t\t\tconst policies = await repo.policiesBySlugs(body.policies);\n\t\t\tconst unknown = body.policies.filter((slug) => !policies.some((p) => p.slug === slug));\n\t\t\tif (unknown.length > 0) {\n\t\t\t\treturn apiError(\"UNKNOWN_POLICY\", `Unknown policy: ${unknown.join(\", \")}`, 400, {\n\t\t\t\t\tunknown,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst result = await handleApiTokenCreate(emdash.db, user!.id, {\n\t\t\t\tname: body.name,\n\t\t\t\tpolicies: body.policies,\n\t\t\t\texpiresAt: body.expiresAt,\n\t\t\t\tcors: body.cors,\n\t\t\t});\n\t\t\treturn unwrapResult(result, 201);\n\t\t}\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to create API token\", \"TOKEN_CREATE_ERROR\");\n\t}\n};\n"],"mappings":";;;;;;;;;;;AAyBA,MAAa,YAAY;AAEzB,MAAM,oBAAoB,EAAE,OAAO;CAClC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI;CAEhC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI;CACpD,WAAW,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU;CAE3C,MAAM,EAAE,SAAS,CAAC,UAAU;CAC5B,CAAC;;AAGF,SAAS,gBAAgB,QAAqC;AAC7D,KAAI,CAAC,OAAO,UAAW,QAAO;AAC9B,QAAO,SACN,wBACA,sGACA,IACA;;;;;AAMF,MAAa,MAAgB,OAAO,EAAE,aAAa;CAClD,MAAM,EAAE,QAAQ,SAAS;AAEzB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAGpE,MAAM,SAAS,YAAY,MAAM,oBAAoB;AACrD,KAAI,OAAQ,QAAO;AAGnB,QAAO,aADQ,MAAM,mBAAmB,OAAO,IAAI,KAAM,GAAG,CACjC;;;;;;AAO5B,MAAa,OAAiB,OAAO,EAAE,SAAS,aAAa;CAC5D,MAAM,EAAE,QAAQ,MAAM,UAAU;AAEhC,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;CAGpE,MAAM,YAAY,gBAAgB,OAAO;AACzC,KAAI,UAAW,QAAO;CAEtB,MAAM,SAAS,YAAY,MAAM,oBAAoB;AACrD,KAAI,OAAQ,QAAO;AAEnB,KAAI;EACH,MAAM,OAAO,MAAM,UAAU,SAAS,kBAAkB;AACxD,MAAI,aAAa,KAAK,CAAE,QAAO;EAE/B;AACC,OAAI,CAAC,MACJ,QAAO,SAAS,kBAAkB,mDAAmD,IAAI;GAM1F,MAAM,eAAe,MAAM,YAAY,YAAY,IAAI,IAAI;GAC3D,MAAM,OAAO,IAAI,IAAI,MAAM,aAAa;GACxC,MAAM,UAAU,eAAe,EAAE,GAAG,KAAK,SAAS,QAAQ,SAAS,CAAC,KAAK,IAAI,KAAK,CAAC;AACnF,OAAI,QAAQ,SAAS,EACpB,QAAO,SACN,mBACA,4BAA4B,QAAQ,KAAK,KAAK,CAAC,mDAC/C,KACA,EAAE,SAAS,CACX;GAIF,MAAM,WAAW,MADJ,IAAI,gBAAgB,OAAO,GAAG,CACf,gBAAgB,KAAK,SAAS;GAC1D,MAAM,UAAU,KAAK,SAAS,QAAQ,SAAS,CAAC,SAAS,MAAM,MAAM,EAAE,SAAS,KAAK,CAAC;AACtF,OAAI,QAAQ,SAAS,EACpB,QAAO,SAAS,kBAAkB,mBAAmB,QAAQ,KAAK,KAAK,IAAI,KAAK,EAC/E,SACA,CAAC;AASH,UAAO,aANQ,MAAM,qBAAqB,OAAO,IAAI,KAAM,IAAI;IAC9D,MAAM,KAAK;IACX,UAAU,KAAK;IACf,WAAW,KAAK;IAChB,MAAM,KAAK;IACX,CAAC,EAC0B,IAAI;;UAEzB,OAAO;AACf,SAAO,YAAY,OAAO,8BAA8B,qBAAqB"}