{"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../../../src/auth/oauth/meta.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAA4C,MAAM,aAAa,CAAC;AAoLvF,eAAO,MAAM,SAAS,EAAE,SAYvB,CAAC","sourcesContent":["/**\n * Meta Model API OAuth flow\n *\n * RFC 8628 device authorization grant against https://auth.meta.com (JSON\n * responses). Meta splits identity from API access: the resulting identity\n * token is not accepted for inference, so it is exchanged for a Model API\n * key via the Muse Code key-mint endpoint (minted keys live about a day).\n * The identity token is stored as `refresh` and the minted key as `access`,\n * so the standard OAuth scheduler re-mints the key when it expires with no\n * bespoke renewal machinery. The identity token itself is not renewable\n * (auth.meta.com answers grant_type=refresh_token with 404 and issues no\n * refresh_token), so a 401/403 from mint means the session is dead and the\n * user must sign in again.\n */\n\nimport type { OAuthAuth, OAuthCredential, ProviderAuthInteraction } from \"../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\n\n// Muse Code CLI client id.\nconst CLIENT_ID = \"1031625952748946\";\nconst AUTH_HOST = \"https://auth.meta.com\";\nconst DEVICE_AUTHORIZATION_URL = `${AUTH_HOST}/oidc/device/authorization/`;\nconst DEVICE_TOKEN_URL = `${AUTH_HOST}/oidc/device/token/`;\nconst API_KEY_MINT_URL = \"https://api.meta.ai/muse-code/key\";\nconst API_KEY_LIFETIME_MS = 24 * 60 * 60 * 1000;\nconst REQUEST_TIMEOUT_MS = 30 * 1000;\n\ntype DeviceAuthorization = {\n\tdeviceCode: string;\n\tuserCode: string;\n\tverificationUri: string;\n\tintervalSeconds?: number;\n\texpiresInSeconds?: number;\n};\n\nfunction requestSignal(signal: AbortSignal): AbortSignal {\n\treturn AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), signal]);\n}\n\nasync function readJson(response: Response): Promise<Record<string, unknown> | null> {\n\ttry {\n\t\tconst json = await response.json();\n\t\treturn json && typeof json === \"object\" ? (json as Record<string, unknown>) : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction errorDetail(json: Record<string, unknown> | null): string {\n\tfor (const key of [\"error_description\", \"detail\", \"message\", \"error\"]) {\n\t\tconst value = json?.[key];\n\t\tif (typeof value === \"string\" && value.trim()) return `: ${value.trim()}`;\n\t}\n\treturn \"\";\n}\n\n/** The verification URI is opened in the user's browser; only http(s) URLs are trusted. */\nfunction trustedHttpUrl(value: unknown): string | null {\n\tif (typeof value !== \"string\" || !value) return null;\n\ttry {\n\t\tconst url = new URL(value);\n\t\tif (url.protocol !== \"https:\" && url.protocol !== \"http:\") return null;\n\t\treturn url.href;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction positiveNumber(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\nasync function startDeviceAuthorization(signal: AbortSignal): Promise<DeviceAuthorization> {\n\tconst response = await fetch(DEVICE_AUTHORIZATION_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\tAccept: \"application/json\",\n\t\t},\n\t\tbody: new URLSearchParams({ client_id: CLIENT_ID }).toString(),\n\t\tsignal: requestSignal(signal),\n\t});\n\tconst json = await readJson(response);\n\tif (!response.ok) {\n\t\tthrow new Error(`Meta device authorization failed with status ${response.status}${errorDetail(json)}`);\n\t}\n\tconst deviceCode = json?.device_code;\n\tconst userCode = json?.user_code;\n\tconst verificationUri = trustedHttpUrl(json?.verification_uri_complete) ?? trustedHttpUrl(json?.verification_uri);\n\tif (typeof deviceCode !== \"string\" || !deviceCode || typeof userCode !== \"string\" || !userCode || !verificationUri) {\n\t\tthrow new Error(`Invalid Meta device authorization response: ${JSON.stringify(json)}`);\n\t}\n\treturn {\n\t\tdeviceCode,\n\t\tuserCode,\n\t\tverificationUri,\n\t\tintervalSeconds: positiveNumber(json?.interval),\n\t\texpiresInSeconds: positiveNumber(json?.expires_in),\n\t};\n}\n\nasync function pollForIdentityToken(device: DeviceAuthorization, signal: AbortSignal): Promise<string> {\n\treturn pollOAuthDeviceCodeFlow<string>({\n\t\tintervalSeconds: device.intervalSeconds,\n\t\texpiresInSeconds: device.expiresInSeconds,\n\t\twaitBeforeFirstPoll: true,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(DEVICE_TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t\t\tdevice_code: device.deviceCode,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t}).toString(),\n\t\t\t\tsignal: requestSignal(signal),\n\t\t\t});\n\t\t\tconst json = await readJson(response);\n\t\t\tif (response.ok && typeof json?.access_token === \"string\" && json.access_token) {\n\t\t\t\treturn { status: \"complete\", value: json.access_token };\n\t\t\t}\n\t\t\tswitch (json?.error) {\n\t\t\t\tcase \"authorization_pending\":\n\t\t\t\t\treturn { status: \"pending\" };\n\t\t\t\tcase \"slow_down\":\n\t\t\t\t\treturn { status: \"slow_down\", intervalSeconds: positiveNumber(json?.interval) };\n\t\t\t\tcase \"access_denied\":\n\t\t\t\t\treturn { status: \"failed\", message: \"Meta login was denied.\" };\n\t\t\t\tcase \"expired_token\":\n\t\t\t\t\treturn { status: \"failed\", message: \"Meta device authorization expired. Please restart login.\" };\n\t\t\t\tdefault:\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\tmessage: `Meta device token request failed with status ${response.status}${errorDetail(json)}`,\n\t\t\t\t\t};\n\t\t\t}\n\t\t},\n\t});\n}\n\n/** Exchange an identity token for a Model API key. Keys are valid for about a day. */\nasync function mintApiKey(identityToken: string, signal: AbortSignal): Promise<OAuthCredential> {\n\tconst response = await fetch(API_KEY_MINT_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${identityToken}`,\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"x-api-version\": \"1.0.0\",\n\t\t},\n\t\tbody: \"{}\",\n\t\tsignal: requestSignal(signal),\n\t});\n\tconst json = await readJson(response);\n\tif (response.status === 401 || response.status === 403) {\n\t\t// Identity token is not renewable (see file header); only a fresh device flow helps.\n\t\tthrow new Error(\n\t\t\t`Meta session expired (status ${response.status}). Run \\`/login meta\\` to sign in again.${errorDetail(json)}`,\n\t\t);\n\t}\n\tif (!response.ok) {\n\t\tthrow new Error(`Meta API key mint failed with status ${response.status}${errorDetail(json)}`);\n\t}\n\tconst apiKey = json?.api_key;\n\tif (typeof apiKey !== \"string\" || !apiKey) {\n\t\tconst actionUrl = trustedHttpUrl(json?.action_url);\n\t\tthrow new Error(`Meta did not issue an API key.${actionUrl ? ` Complete setup at ${actionUrl}` : \"\"}`);\n\t}\n\treturn { type: \"oauth\", refresh: identityToken, access: apiKey, expires: Date.now() + API_KEY_LIFETIME_MS };\n}\n\nasync function loginMeta(interaction: ProviderAuthInteraction): Promise<OAuthCredential> {\n\ttry {\n\t\tconst device = await startDeviceAuthorization(interaction.signal);\n\t\tinteraction.notify({\n\t\t\ttype: \"device_code\",\n\t\t\tuserCode: device.userCode,\n\t\t\tverificationUri: device.verificationUri,\n\t\t\tintervalSeconds: device.intervalSeconds,\n\t\t\texpiresInSeconds: device.expiresInSeconds,\n\t\t});\n\t\tconst identityToken = await pollForIdentityToken(device, interaction.signal);\n\t\tinteraction.notify({ type: \"progress\", message: \"Enabling Meta Model API access...\" });\n\t\treturn await mintApiKey(identityToken, interaction.signal);\n\t} catch (e) {\n\t\t// An in-flight fetch rejects with a DOMException on abort; the login UI matches on this message.\n\t\tif (interaction.signal.aborted) throw new Error(\"Login cancelled\");\n\t\tthrow e;\n\t}\n}\n\nexport const metaOAuth: OAuthAuth = {\n\tname: \"Meta (Muse subscription)\",\n\tisSubscription: true,\n\tloginLabel: \"Sign in with Meta\",\n\n\tlogin: loginMeta,\n\n\trefresh: (credential, signal) => mintApiKey(credential.refresh, signal),\n\n\tasync toAuth(credential) {\n\t\treturn { apiKey: credential.access };\n\t},\n};\n"]}