{"version":3,"file":"register.mjs","names":[],"sources":["../../../../../src/astro/routes/api/oauth/register.ts"],"sourcesContent":["/**\n * POST /_emdash/api/oauth/register\n *\n * RFC 7591 Dynamic Client Registration. Public, unauthenticated.\n * MCP clients (e.g. Claude Code) call this to register themselves\n * before starting the OAuth authorization flow.\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { apiError, handleError } from \"#api/error.js\";\nimport { handleOAuthClientCreate } from \"#api/handlers/oauth-clients.js\";\n\nexport const prerender = false;\n\nconst OAUTH_REGISTRATION_HEADERS: HeadersInit = {\n\t\"Cache-Control\": \"no-store\",\n\tPragma: \"no-cache\",\n\t// RFC 7591 dynamic client registration is called cross-origin by MCP clients,\n\t// CLIs, and native apps. The endpoint is anonymous and carries no ambient\n\t// credentials, so CORS `*` is safe.\n\t\"Access-Control-Allow-Origin\": \"*\",\n};\n\nconst OAUTH_PREFLIGHT_HEADERS: HeadersInit = {\n\t\"Access-Control-Allow-Origin\": \"*\",\n\t\"Access-Control-Allow-Methods\": \"POST, OPTIONS\",\n\t\"Access-Control-Allow-Headers\": \"Content-Type\",\n\t\"Access-Control-Max-Age\": \"86400\",\n};\n\nconst SUPPORTED_GRANT_TYPES = new Set([\n\t\"authorization_code\",\n\t\"refresh_token\",\n\t\"urn:ietf:params:oauth:grant-type:device_code\",\n]);\nconst SUPPORTED_RESPONSE_TYPES = new Set([\"code\"]);\n\nfunction registrationError(description: string, status = 400): Response {\n\treturn Response.json(\n\t\t{\n\t\t\terror: \"invalid_client_metadata\",\n\t\t\terror_description: description,\n\t\t},\n\t\t{ status, headers: OAUTH_REGISTRATION_HEADERS },\n\t);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n\treturn Array.isArray(value) && value.every((item) => typeof item === \"string\");\n}\n\nfunction parseScope(value: unknown): string[] | Response | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value === \"string\") {\n\t\tconst scopes = value.split(\" \").filter(Boolean);\n\t\treturn scopes.length > 0 ? scopes : undefined;\n\t}\n\tif (isStringArray(value)) {\n\t\tconst scopes = value.filter(Boolean);\n\t\treturn scopes.length > 0 ? scopes : undefined;\n\t}\n\treturn registrationError(\"scope must be a string or array of strings\");\n}\n\nfunction parseSupportedStringArray(\n\tvalue: unknown,\n\tfield: string,\n\tsupported: ReadonlySet<string>,\n): string[] | Response | undefined {\n\tif (value === undefined) return undefined;\n\tif (!isStringArray(value)) {\n\t\treturn registrationError(`${field} must be an array of strings`);\n\t}\n\tconst invalidValue = value.find((item) => !supported.has(item));\n\tif (invalidValue) {\n\t\treturn registrationError(`${field} contains unsupported value: ${invalidValue}`);\n\t}\n\treturn value;\n}\n\nexport const OPTIONS: APIRoute = () => {\n\treturn new Response(null, { status: 204, headers: OAUTH_PREFLIGHT_HEADERS });\n};\n\nexport const POST: APIRoute = async ({ request, locals }) => {\n\tconst { emdash } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\ttry {\n\t\tlet body: unknown;\n\t\ttry {\n\t\t\tbody = await request.json();\n\t\t} catch {\n\t\t\treturn registrationError(\"Request body must be valid JSON\");\n\t\t}\n\n\t\tif (!isRecord(body)) {\n\t\t\treturn registrationError(\"Request body must be a JSON object\");\n\t\t}\n\n\t\t// redirect_uris is the only required field per RFC 7591 §2\n\t\tif (!isStringArray(body.redirect_uris) || body.redirect_uris.length === 0) {\n\t\t\treturn registrationError(\"redirect_uris must be a non-empty array of strings\");\n\t\t}\n\n\t\tif (\n\t\t\tbody.token_endpoint_auth_method !== undefined &&\n\t\t\tbody.token_endpoint_auth_method !== \"none\"\n\t\t) {\n\t\t\treturn registrationError(\"Only token_endpoint_auth_method=none is supported\");\n\t\t}\n\n\t\tconst grantTypes = parseSupportedStringArray(\n\t\t\tbody.grant_types,\n\t\t\t\"grant_types\",\n\t\t\tSUPPORTED_GRANT_TYPES,\n\t\t);\n\t\tif (grantTypes instanceof Response) {\n\t\t\treturn grantTypes;\n\t\t}\n\n\t\tconst responseTypes = parseSupportedStringArray(\n\t\t\tbody.response_types,\n\t\t\t\"response_types\",\n\t\t\tSUPPORTED_RESPONSE_TYPES,\n\t\t);\n\t\tif (responseTypes instanceof Response) {\n\t\t\treturn responseTypes;\n\t\t}\n\n\t\tconst scopes = parseScope(body.scope);\n\t\tif (scopes instanceof Response) {\n\t\t\treturn scopes;\n\t\t}\n\n\t\tconst clientId = crypto.randomUUID();\n\t\tconst clientName =\n\t\t\ttypeof body.client_name === \"string\" && body.client_name\n\t\t\t\t? body.client_name\n\t\t\t\t: `dynamic-${clientId.slice(0, 8)}`;\n\n\t\tconst result = await handleOAuthClientCreate(emdash.db, {\n\t\t\tid: clientId,\n\t\t\tname: clientName,\n\t\t\tredirectUris: body.redirect_uris,\n\t\t\tscopes,\n\t\t});\n\n\t\tif (!result.success) {\n\t\t\treturn registrationError(result.error.message);\n\t\t}\n\n\t\t// RFC 7591 §3.2.1 response\n\t\treturn Response.json(\n\t\t\t{\n\t\t\t\tclient_id: result.data.id,\n\t\t\t\tclient_id_issued_at: Math.floor(new Date(result.data.createdAt).getTime() / 1000),\n\t\t\t\tredirect_uris: result.data.redirectUris,\n\t\t\t\tclient_name: result.data.name,\n\t\t\t\tgrant_types: grantTypes ?? [\"authorization_code\", \"refresh_token\"],\n\t\t\t\tresponse_types: responseTypes ?? [\"code\"],\n\t\t\t\ttoken_endpoint_auth_method: \"none\",\n\t\t\t\tscope: result.data.scopes ? result.data.scopes.join(\" \") : undefined,\n\t\t\t},\n\t\t\t{ status: 201, headers: OAUTH_REGISTRATION_HEADERS },\n\t\t);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to register OAuth client\", \"CLIENT_REGISTER_ERROR\");\n\t}\n};\n"],"mappings":"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"}