{"version":3,"file":"token.mjs","names":[],"sources":["../../../../../../src/astro/routes/api/oauth/device/token.ts"],"sourcesContent":["/**\n * POST /_emdash/api/oauth/device/token\n *\n * CLI polls this endpoint to exchange a device code for tokens.\n * Returns RFC 8628 error codes during the polling phase.\n * This is an unauthenticated endpoint.\n *\n * Rate limited: 12 requests per minute per IP.\n * Also enforces RFC 8628 slow_down: if polled faster than the interval,\n * responds with { error: \"slow_down\", interval: N } and increases the interval by 5s.\n */\n\nimport type { APIRoute } from \"astro\";\nimport { z } from \"zod\";\n\nimport { apiError, handleError, unwrapResult } from \"#api/error.js\";\nimport { handleDeviceTokenExchange } from \"#api/handlers/device-flow.js\";\nimport { isParseError, parseBody } from \"#api/parse.js\";\nimport { checkRateLimit, getClientIp, rateLimitResponse } from \"#auth/rate-limit.js\";\nimport { getTrustedProxyHeaders } from \"#auth/trusted-proxy.js\";\n\nexport const prerender = false;\n\nconst deviceTokenSchema = z.object({\n\tdevice_code: z.string().min(1),\n\tgrant_type: z.string().min(1),\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\tconst body = await parseBody(request, deviceTokenSchema);\n\t\tif (isParseError(body)) return body;\n\n\t\t// Rate limit: 12 requests per 60 seconds per IP\n\t\tconst ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));\n\t\tconst rateLimit = await checkRateLimit(emdash.db, ip, \"device/token\", 12, 60);\n\t\tif (!rateLimit.allowed) {\n\t\t\treturn rateLimitResponse(60);\n\t\t}\n\n\t\tconst result = await handleDeviceTokenExchange(emdash.db, body);\n\n\t\t// RFC 8628 requires specific error format for device flow errors\n\t\t// RFC 6749 §5.1 requires Cache-Control: no-store + Pragma: no-cache on token responses\n\t\tif (!result.success && result.deviceFlowError) {\n\t\t\tconst errorBody: { error: string; interval?: number } = { error: result.deviceFlowError };\n\t\t\tif (result.deviceFlowInterval !== undefined) {\n\t\t\t\terrorBody.interval = result.deviceFlowInterval;\n\t\t\t}\n\t\t\treturn Response.json(errorBody, {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t\tPragma: \"no-cache\",\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\treturn unwrapResult(result);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to exchange device code\", \"TOKEN_EXCHANGE_ERROR\");\n\t}\n};\n"],"mappings":";;;;;;;;;;;;;;AAqBA,MAAa,YAAY;AAEzB,MAAM,oBAAoB,EAAE,OAAO;CAClC,aAAa,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC9B,YAAY,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC7B,CAAC;AAEF,MAAa,OAAiB,OAAO,EAAE,SAAS,aAAa;CAC5D,MAAM,EAAE,WAAW;AAEnB,KAAI,CAAC,QAAQ,GACZ,QAAO,SAAS,kBAAkB,6BAA6B,IAAI;AAGpE,KAAI;EACH,MAAM,OAAO,MAAM,UAAU,SAAS,kBAAkB;AACxD,MAAI,aAAa,KAAK,CAAE,QAAO;EAG/B,MAAM,KAAK,YAAY,SAAS,uBAAuB,OAAO,OAAO,CAAC;AAEtE,MAAI,EADc,MAAM,eAAe,OAAO,IAAI,IAAI,gBAAgB,IAAI,GAAG,EAC9D,QACd,QAAO,kBAAkB,GAAG;EAG7B,MAAM,SAAS,MAAM,0BAA0B,OAAO,IAAI,KAAK;AAI/D,MAAI,CAAC,OAAO,WAAW,OAAO,iBAAiB;GAC9C,MAAM,YAAkD,EAAE,OAAO,OAAO,iBAAiB;AACzF,OAAI,OAAO,uBAAuB,OACjC,WAAU,WAAW,OAAO;AAE7B,UAAO,SAAS,KAAK,WAAW;IAC/B,QAAQ;IACR,SAAS;KACR,gBAAgB;KAChB,iBAAiB;KACjB,QAAQ;KACR;IACD,CAAC;;AAGH,SAAO,aAAa,OAAO;UACnB,OAAO;AACf,SAAO,YAAY,OAAO,kCAAkC,uBAAuB"}