@bastani/pi-ai 0.9.40-alpha.2 → 0.9.41-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +28 -0
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +7 -10
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/azure-openai-responses.js +2 -2
- package/dist/api/azure-openai-responses.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +12 -0
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/classifier-shared.d.ts.map +1 -1
- package/dist/api/classifier-shared.js +4 -3
- package/dist/api/classifier-shared.js.map +1 -1
- package/dist/api/google-generative-ai.js +4 -4
- package/dist/api/google-generative-ai.js.map +1 -1
- package/dist/api/google-vertex.d.ts.map +1 -1
- package/dist/api/google-vertex.js +9 -9
- package/dist/api/google-vertex.js.map +1 -1
- package/dist/api/llama-cpp-classify.d.ts.map +1 -1
- package/dist/api/llama-cpp-classify.js +2 -1
- package/dist/api/llama-cpp-classify.js.map +1 -1
- package/dist/api/mistral-conversations.js +2 -2
- package/dist/api/mistral-conversations.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +12 -9
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +4 -4
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +15 -7
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/openrouter-images.d.ts.map +1 -1
- package/dist/api/openrouter-images.js +4 -3
- package/dist/api/openrouter-images.js.map +1 -1
- package/dist/api/pi-messages.d.ts.map +1 -1
- package/dist/api/pi-messages.js +3 -3
- package/dist/api/pi-messages.js.map +1 -1
- package/dist/api/simple-options.d.ts.map +1 -1
- package/dist/api/simple-options.js +1 -0
- package/dist/api/simple-options.js.map +1 -1
- package/dist/auth/oauth/anthropic.d.ts.map +1 -1
- package/dist/auth/oauth/anthropic.js +15 -2
- package/dist/auth/oauth/anthropic.js.map +1 -1
- package/dist/auth/oauth/github-copilot.d.ts.map +1 -1
- package/dist/auth/oauth/github-copilot.js +7 -2
- package/dist/auth/oauth/github-copilot.js.map +1 -1
- package/dist/auth/oauth/kimi-coding.d.ts.map +1 -1
- package/dist/auth/oauth/kimi-coding.js +24 -21
- package/dist/auth/oauth/kimi-coding.js.map +1 -1
- package/dist/auth/oauth/meta.d.ts.map +1 -1
- package/dist/auth/oauth/meta.js +7 -2
- package/dist/auth/oauth/meta.js.map +1 -1
- package/dist/auth/oauth/openai-chatgpt.d.ts.map +1 -1
- package/dist/auth/oauth/openai-chatgpt.js +10 -0
- package/dist/auth/oauth/openai-chatgpt.js.map +1 -1
- package/dist/auth/oauth/openai-codex.d.ts.map +1 -1
- package/dist/auth/oauth/openai-codex.js +11 -2
- package/dist/auth/oauth/openai-codex.js.map +1 -1
- package/dist/auth/oauth/radius.d.ts.map +1 -1
- package/dist/auth/oauth/radius.js +24 -8
- package/dist/auth/oauth/radius.js.map +1 -1
- package/dist/auth/oauth/xai.d.ts.map +1 -1
- package/dist/auth/oauth/xai.js +11 -0
- package/dist/auth/oauth/xai.js.map +1 -1
- package/dist/auth/oauth-refresh-errors.d.ts +46 -0
- package/dist/auth/oauth-refresh-errors.d.ts.map +1 -0
- package/dist/auth/oauth-refresh-errors.js +150 -0
- package/dist/auth/oauth-refresh-errors.js.map +1 -0
- package/dist/auth/resolve.d.ts +23 -0
- package/dist/auth/resolve.d.ts.map +1 -1
- package/dist/auth/resolve.js +194 -5
- package/dist/auth/resolve.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +4 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +15 -4
- package/dist/models.js.map +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/amazon-bedrock.json +1 -1
- package/dist/providers/data/baseten.json +1 -1
- package/dist/providers/data/cerebras.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/groq.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/together.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/identity.d.ts +65 -0
- package/dist/utils/identity.d.ts.map +1 -0
- package/dist/utils/identity.js +142 -0
- package/dist/utils/identity.js.map +1 -0
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +9 -8
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +3 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +1 -1
- package/dist/utils/pi-user-agent.d.ts +0 -2
- package/dist/utils/pi-user-agent.d.ts.map +0 -1
- package/dist/utils/pi-user-agent.js +0 -12
- package/dist/utils/pi-user-agent.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xai.js","sourceRoot":"","sources":["../../../src/auth/oauth/xai.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * xAI OAuth device-code flow.\n */\n\nimport type { OAuthAuth, OAuthCredential, ProviderAuthInteraction } from \"../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\n\nconst XAI_CLIENT_ID = \"b1a00492-073a-47ea-816f-4c329264a828\";\nconst XAI_SCOPE = \"openid profile email offline_access grok-cli:access api:access\";\nconst XAI_DEVICE_CODE_URL = \"https://auth.x.ai/oauth2/device/code\";\nconst XAI_TOKEN_URL = \"https://auth.x.ai/oauth2/token\";\n// Refresh slightly before the reported expiry to avoid using a token that dies mid-request.\nconst REFRESH_SKEW_MS = 5 * 60 * 1000;\nconst DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;\n\ntype JsonObject = Record<string, unknown>;\n\ntype OAuthHttpResponse = {\n\tok: boolean;\n\tstatus: number;\n\tbody: JsonObject;\n};\n\ntype XaiDeviceCode = {\n\tdeviceCode: string;\n\tuserCode: string;\n\tverificationUri: string;\n\tverificationUriComplete?: string;\n\tintervalSeconds?: number;\n\texpiresInSeconds: number;\n};\n\nfunction requiredString(body: JsonObject, field: string): string {\n\tconst value = body[field];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Invalid xAI OAuth response field: ${field}`);\n\t}\n\treturn value;\n}\n\nfunction positiveNumber(body: JsonObject, field: string): number {\n\tconst value = body[field];\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) {\n\t\tthrow new Error(`Invalid xAI OAuth response field: ${field}`);\n\t}\n\treturn value;\n}\n\n// The verification URI is opened in the user's browser; force it to be an https URL\n// so a malicious response cannot make `open` launch something else.\nfunction validateVerificationUri(raw: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(raw);\n\t} catch {\n\t\tthrow new Error(\"Untrusted verification URI in xAI OAuth response\");\n\t}\n\tif (url.protocol !== \"https:\") {\n\t\tthrow new Error(\"Untrusted verification URI in xAI OAuth response\");\n\t}\n\treturn url.href;\n}\n\nasync function postForm(url: string, fields: Record<string, string>, signal: AbortSignal): Promise<OAuthHttpResponse> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t},\n\t\t\tbody: new URLSearchParams(fields),\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\tif (signal.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\t\tthrow error;\n\t}\n\n\tlet body: JsonObject;\n\ttry {\n\t\tconst parsed = (await response.json()) as unknown;\n\t\tbody = parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? (parsed as JsonObject) : {};\n\t} catch {\n\t\tif (signal.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\t\tthrow new Error(`xAI OAuth returned invalid JSON (HTTP ${response.status})`);\n\t}\n\treturn {\n\t\tok: response.ok,\n\t\tstatus: response.status,\n\t\tbody,\n\t};\n}\n\nfunction requestFailure(action: string, response: OAuthHttpResponse): Error {\n\tconst error = typeof response.body.error === \"string\" ? response.body.error : undefined;\n\tconst description =\n\t\ttypeof response.body.error_description === \"string\" ? response.body.error_description : undefined;\n\tconst detail = [error, description].filter(Boolean).join(\": \");\n\treturn new Error(`xAI OAuth ${action} failed (HTTP ${response.status})${detail ? `: ${detail}` : \"\"}`);\n}\n\nfunction parseDeviceCode(body: JsonObject): XaiDeviceCode {\n\t// RFC 8628 allows interval 0 (no minimum wait); fall back to the poller's\n\t// default instead of failing on non-positive or malformed values.\n\tconst interval = body.interval;\n\tconst intervalSeconds =\n\t\ttypeof interval === \"number\" && Number.isFinite(interval) && interval > 0 ? interval : undefined;\n\tconst verificationUriComplete =\n\t\ttypeof body.verification_uri_complete === \"string\" && body.verification_uri_complete.length > 0\n\t\t\t? validateVerificationUri(body.verification_uri_complete)\n\t\t\t: undefined;\n\treturn {\n\t\tdeviceCode: requiredString(body, \"device_code\"),\n\t\tuserCode: requiredString(body, \"user_code\"),\n\t\tverificationUri: validateVerificationUri(requiredString(body, \"verification_uri\")),\n\t\tverificationUriComplete,\n\t\tintervalSeconds,\n\t\texpiresInSeconds: positiveNumber(body, \"expires_in\"),\n\t};\n}\n\nfunction credentialsFromTokenResponse(body: JsonObject, previousRefreshToken?: string): OAuthCredential {\n\tconst access = requiredString(body, \"access_token\");\n\t// xAI may omit refresh_token on refresh when the token is not rotated.\n\tconst refresh =\n\t\tbody.refresh_token === undefined && previousRefreshToken\n\t\t\t? previousRefreshToken\n\t\t\t: requiredString(body, \"refresh_token\");\n\tconst expiresInSeconds =\n\t\tbody.expires_in === undefined ? DEFAULT_TOKEN_LIFETIME_SECONDS : positiveNumber(body, \"expires_in\");\n\treturn {\n\t\ttype: \"oauth\",\n\t\taccess,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - REFRESH_SKEW_MS,\n\t};\n}\n\nasync function requestDeviceCode(signal: AbortSignal): Promise<XaiDeviceCode> {\n\tconst response = await postForm(\n\t\tXAI_DEVICE_CODE_URL,\n\t\t{\n\t\t\tclient_id: XAI_CLIENT_ID,\n\t\t\tscope: XAI_SCOPE,\n\t\t\treferrer: \"pi\",\n\t\t},\n\t\tsignal,\n\t);\n\tif (!response.ok) {\n\t\tthrow requestFailure(\"device authorization\", response);\n\t}\n\treturn parseDeviceCode(response.body);\n}\n\nasync function pollForTokens(device: XaiDeviceCode, signal: AbortSignal): Promise<OAuthCredential> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredential>({\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 postForm(\n\t\t\t\tXAI_TOKEN_URL,\n\t\t\t\t{\n\t\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t\t\tclient_id: XAI_CLIENT_ID,\n\t\t\t\t\tdevice_code: device.deviceCode,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\n\t\t\tif (response.ok) {\n\t\t\t\treturn { status: \"complete\", value: credentialsFromTokenResponse(response.body) };\n\t\t\t}\n\n\t\t\tconst error = response.body.error;\n\t\t\tif (error === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (error === \"slow_down\") {\n\t\t\t\tconst interval = response.body.interval;\n\t\t\t\treturn { status: \"slow_down\", intervalSeconds: typeof interval === \"number\" ? interval : undefined };\n\t\t\t}\n\t\t\tif (error === \"access_denied\" || error === \"authorization_denied\") {\n\t\t\t\treturn { status: \"failed\", message: \"xAI device authorization was denied\" };\n\t\t\t}\n\t\t\tif (error === \"expired_token\") {\n\t\t\t\treturn { status: \"failed\", message: \"xAI device code expired\" };\n\t\t\t}\n\t\t\treturn { status: \"failed\", message: requestFailure(\"device token polling\", response).message };\n\t\t},\n\t});\n}\n\nasync function loginXai(interaction: ProviderAuthInteraction): Promise<OAuthCredential> {\n\tconst device = await requestDeviceCode(interaction.signal);\n\tinteraction.notify({\n\t\ttype: \"device_code\",\n\t\tuserCode: device.userCode,\n\t\tverificationUri: device.verificationUriComplete ?? device.verificationUri,\n\t\tintervalSeconds: device.intervalSeconds,\n\t\texpiresInSeconds: device.expiresInSeconds,\n\t});\n\treturn pollForTokens(device, interaction.signal);\n}\n\nasync function refreshXaiToken(refreshToken: string, signal: AbortSignal): Promise<OAuthCredential> {\n\tconst response = await postForm(\n\t\tXAI_TOKEN_URL,\n\t\t{\n\t\t\tgrant_type: \"refresh_token\",\n\t\t\tclient_id: XAI_CLIENT_ID,\n\t\t\trefresh_token: refreshToken,\n\t\t},\n\t\tsignal,\n\t);\n\tif (!response.ok) {\n\t\tthrow requestFailure(\"token refresh\", response);\n\t}\n\treturn credentialsFromTokenResponse(response.body, refreshToken);\n}\n\nexport const xaiOAuth: OAuthAuth = {\n\tname: \"xAI (Grok/X subscription)\",\n\tisSubscription: true,\n\tloginLabel: \"Sign in with SuperGrok or X Premium\",\n\tlogin: loginXai,\n\trefresh: (credential, signal) => refreshXaiToken(credential.refresh, signal),\n\n\tasync toAuth(credential) {\n\t\treturn { apiKey: credential.access };\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"xai.js","sourceRoot":"","sources":["../../../src/auth/oauth/xai.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * xAI OAuth device-code flow.\n */\n\nimport { isPermanentRefreshRejection, OAuthRefreshRejectedError } from \"../oauth-refresh-errors.ts\";\nimport type { OAuthAuth, OAuthCredential, ProviderAuthInteraction } from \"../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\n\nconst XAI_CLIENT_ID = \"b1a00492-073a-47ea-816f-4c329264a828\";\nconst XAI_SCOPE = \"openid profile email offline_access grok-cli:access api:access\";\nconst XAI_DEVICE_CODE_URL = \"https://auth.x.ai/oauth2/device/code\";\nconst XAI_TOKEN_URL = \"https://auth.x.ai/oauth2/token\";\n// Refresh slightly before the reported expiry to avoid using a token that dies mid-request.\nconst REFRESH_SKEW_MS = 5 * 60 * 1000;\nconst DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;\n\ntype JsonObject = Record<string, unknown>;\n\ntype OAuthHttpResponse = {\n\tok: boolean;\n\tstatus: number;\n\tbody: JsonObject;\n};\n\ntype XaiDeviceCode = {\n\tdeviceCode: string;\n\tuserCode: string;\n\tverificationUri: string;\n\tverificationUriComplete?: string;\n\tintervalSeconds?: number;\n\texpiresInSeconds: number;\n};\n\nfunction requiredString(body: JsonObject, field: string): string {\n\tconst value = body[field];\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Invalid xAI OAuth response field: ${field}`);\n\t}\n\treturn value;\n}\n\nfunction positiveNumber(body: JsonObject, field: string): number {\n\tconst value = body[field];\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) {\n\t\tthrow new Error(`Invalid xAI OAuth response field: ${field}`);\n\t}\n\treturn value;\n}\n\n// The verification URI is opened in the user's browser; force it to be an https URL\n// so a malicious response cannot make `open` launch something else.\nfunction validateVerificationUri(raw: string): string {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(raw);\n\t} catch {\n\t\tthrow new Error(\"Untrusted verification URI in xAI OAuth response\");\n\t}\n\tif (url.protocol !== \"https:\") {\n\t\tthrow new Error(\"Untrusted verification URI in xAI OAuth response\");\n\t}\n\treturn url.href;\n}\n\nasync function postForm(url: string, fields: Record<string, string>, signal: AbortSignal): Promise<OAuthHttpResponse> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t},\n\t\t\tbody: new URLSearchParams(fields),\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\tif (signal.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\t\tthrow error;\n\t}\n\n\tlet body: JsonObject;\n\ttry {\n\t\tconst parsed = (await response.json()) as unknown;\n\t\tbody = parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? (parsed as JsonObject) : {};\n\t} catch {\n\t\tif (signal.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\t\tthrow new Error(`xAI OAuth returned invalid JSON (HTTP ${response.status})`);\n\t}\n\treturn {\n\t\tok: response.ok,\n\t\tstatus: response.status,\n\t\tbody,\n\t};\n}\n\nfunction requestFailure(action: string, response: OAuthHttpResponse): Error {\n\tconst error = typeof response.body.error === \"string\" ? response.body.error : undefined;\n\tconst description =\n\t\ttypeof response.body.error_description === \"string\" ? response.body.error_description : undefined;\n\tconst detail = [error, description].filter(Boolean).join(\": \");\n\treturn new Error(`xAI OAuth ${action} failed (HTTP ${response.status})${detail ? `: ${detail}` : \"\"}`);\n}\n\nfunction parseDeviceCode(body: JsonObject): XaiDeviceCode {\n\t// RFC 8628 allows interval 0 (no minimum wait); fall back to the poller's\n\t// default instead of failing on non-positive or malformed values.\n\tconst interval = body.interval;\n\tconst intervalSeconds =\n\t\ttypeof interval === \"number\" && Number.isFinite(interval) && interval > 0 ? interval : undefined;\n\tconst verificationUriComplete =\n\t\ttypeof body.verification_uri_complete === \"string\" && body.verification_uri_complete.length > 0\n\t\t\t? validateVerificationUri(body.verification_uri_complete)\n\t\t\t: undefined;\n\treturn {\n\t\tdeviceCode: requiredString(body, \"device_code\"),\n\t\tuserCode: requiredString(body, \"user_code\"),\n\t\tverificationUri: validateVerificationUri(requiredString(body, \"verification_uri\")),\n\t\tverificationUriComplete,\n\t\tintervalSeconds,\n\t\texpiresInSeconds: positiveNumber(body, \"expires_in\"),\n\t};\n}\n\nfunction credentialsFromTokenResponse(body: JsonObject, previousRefreshToken?: string): OAuthCredential {\n\tconst access = requiredString(body, \"access_token\");\n\t// xAI may omit refresh_token on refresh when the token is not rotated.\n\tconst refresh =\n\t\tbody.refresh_token === undefined && previousRefreshToken\n\t\t\t? previousRefreshToken\n\t\t\t: requiredString(body, \"refresh_token\");\n\tconst expiresInSeconds =\n\t\tbody.expires_in === undefined ? DEFAULT_TOKEN_LIFETIME_SECONDS : positiveNumber(body, \"expires_in\");\n\treturn {\n\t\ttype: \"oauth\",\n\t\taccess,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - REFRESH_SKEW_MS,\n\t};\n}\n\nasync function requestDeviceCode(signal: AbortSignal): Promise<XaiDeviceCode> {\n\tconst response = await postForm(\n\t\tXAI_DEVICE_CODE_URL,\n\t\t{\n\t\t\tclient_id: XAI_CLIENT_ID,\n\t\t\tscope: XAI_SCOPE,\n\t\t\treferrer: \"pi\",\n\t\t},\n\t\tsignal,\n\t);\n\tif (!response.ok) {\n\t\tthrow requestFailure(\"device authorization\", response);\n\t}\n\treturn parseDeviceCode(response.body);\n}\n\nasync function pollForTokens(device: XaiDeviceCode, signal: AbortSignal): Promise<OAuthCredential> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredential>({\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 postForm(\n\t\t\t\tXAI_TOKEN_URL,\n\t\t\t\t{\n\t\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t\t\tclient_id: XAI_CLIENT_ID,\n\t\t\t\t\tdevice_code: device.deviceCode,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\n\t\t\tif (response.ok) {\n\t\t\t\treturn { status: \"complete\", value: credentialsFromTokenResponse(response.body) };\n\t\t\t}\n\n\t\t\tconst error = response.body.error;\n\t\t\tif (error === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (error === \"slow_down\") {\n\t\t\t\tconst interval = response.body.interval;\n\t\t\t\treturn { status: \"slow_down\", intervalSeconds: typeof interval === \"number\" ? interval : undefined };\n\t\t\t}\n\t\t\tif (error === \"access_denied\" || error === \"authorization_denied\") {\n\t\t\t\treturn { status: \"failed\", message: \"xAI device authorization was denied\" };\n\t\t\t}\n\t\t\tif (error === \"expired_token\") {\n\t\t\t\treturn { status: \"failed\", message: \"xAI device code expired\" };\n\t\t\t}\n\t\t\treturn { status: \"failed\", message: requestFailure(\"device token polling\", response).message };\n\t\t},\n\t});\n}\n\nasync function loginXai(interaction: ProviderAuthInteraction): Promise<OAuthCredential> {\n\tconst device = await requestDeviceCode(interaction.signal);\n\tinteraction.notify({\n\t\ttype: \"device_code\",\n\t\tuserCode: device.userCode,\n\t\tverificationUri: device.verificationUriComplete ?? device.verificationUri,\n\t\tintervalSeconds: device.intervalSeconds,\n\t\texpiresInSeconds: device.expiresInSeconds,\n\t});\n\treturn pollForTokens(device, interaction.signal);\n}\n\nasync function refreshXaiToken(refreshToken: string, signal: AbortSignal): Promise<OAuthCredential> {\n\tconst response = await postForm(\n\t\tXAI_TOKEN_URL,\n\t\t{\n\t\t\tgrant_type: \"refresh_token\",\n\t\t\tclient_id: XAI_CLIENT_ID,\n\t\t\trefresh_token: refreshToken,\n\t\t},\n\t\tsignal,\n\t);\n\tif (!response.ok) {\n\t\tconst rejection = {\n\t\t\tstatus: response.status,\n\t\t\tcode: typeof response.body.error === \"string\" ? response.body.error : undefined,\n\t\t\tdescription:\n\t\t\t\ttypeof response.body.error_description === \"string\" ? response.body.error_description : undefined,\n\t\t};\n\t\tif (isPermanentRefreshRejection(rejection)) {\n\t\t\tthrow new OAuthRefreshRejectedError(`xAI OAuth token refresh failed (HTTP ${response.status})`, rejection, [\n\t\t\t\trefreshToken,\n\t\t\t]);\n\t\t}\n\t\tthrow requestFailure(\"token refresh\", response);\n\t}\n\treturn credentialsFromTokenResponse(response.body, refreshToken);\n}\n\nexport const xaiOAuth: OAuthAuth = {\n\tname: \"xAI (Grok/X subscription)\",\n\tisSubscription: true,\n\tloginLabel: \"Sign in with SuperGrok or X Premium\",\n\tlogin: loginXai,\n\trefresh: (credential, signal) => refreshXaiToken(credential.refresh, signal),\n\n\tasync toAuth(credential) {\n\t\treturn { apiKey: credential.access };\n\t},\n};\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** A token endpoint's error code and description, as the provider sent them. */
|
|
2
|
+
export interface OAuthErrorDetail {
|
|
3
|
+
code?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
/** A token endpoint's answer to a refresh, by HTTP status and the provider's error code and description. */
|
|
7
|
+
export interface OAuthRefreshRejection extends OAuthErrorDetail {
|
|
8
|
+
status: number;
|
|
9
|
+
}
|
|
10
|
+
/** Reads `error` / `error.code` / `code` and `error_description` / `error.message` / `message` from a JSON body. */
|
|
11
|
+
export declare function parseOAuthErrorBody(body: string): OAuthErrorDetail;
|
|
12
|
+
/** Whether a refresh rejection means the stored login is revoked or expired, so only signing in again helps. */
|
|
13
|
+
export declare function isPermanentRefreshRejection(rejection: OAuthRefreshRejection): boolean;
|
|
14
|
+
/** Whether `code` is one of the OAuth error codes that mark a refresh token as permanently unusable. */
|
|
15
|
+
export declare function isPermanentRefreshErrorCode(code: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Finds a permanent refresh rejection in an error message from a refresher that does not throw
|
|
18
|
+
* {@link OAuthRefreshRejectedError}: an embedded JSON error body (`error`, nested `error.code`, or `code`) or a bare
|
|
19
|
+
* permanent code, plus the body's description. Returns nothing if no permanent code appears.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findPermanentRefreshRejectionInText(text: string): OAuthErrorDetail | undefined;
|
|
22
|
+
/** The provider's error code, if it looks like one; anything else could be echoed request data. */
|
|
23
|
+
export declare function safeOAuthErrorCode(code: string | undefined): string | undefined;
|
|
24
|
+
/** The provider's description, unless it echoes a presented secret, carries a URL, or is too long to be one. */
|
|
25
|
+
export declare function safeOAuthErrorDescription(description: string | undefined, presentedSecrets: readonly string[]): string | undefined;
|
|
26
|
+
/** `: code: description` with only the parts that are safe to show, or nothing. */
|
|
27
|
+
export declare function formatOAuthErrorDetail(detail: OAuthErrorDetail, presentedSecrets: readonly string[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* A token endpoint rejected a refresh because the stored login is revoked or expired. Carries only the HTTP status
|
|
30
|
+
* and the provider's error code and description, the latter dropped if it echoes the presented token.
|
|
31
|
+
*/
|
|
32
|
+
export declare class OAuthRefreshRejectedError extends Error {
|
|
33
|
+
readonly name = "OAuthRefreshRejectedError";
|
|
34
|
+
readonly status: number;
|
|
35
|
+
readonly code: string | undefined;
|
|
36
|
+
readonly description: string | undefined;
|
|
37
|
+
constructor(summary: string, rejection: OAuthRefreshRejection, presentedSecrets: readonly string[]);
|
|
38
|
+
}
|
|
39
|
+
/** Recognises the rejection across duplicated module copies (source and built output), not only by identity. */
|
|
40
|
+
export declare function isOAuthRefreshRejectedError(error: Error): error is OAuthRefreshRejectedError;
|
|
41
|
+
/**
|
|
42
|
+
* Whether a failed fetch provably never sent its request (no connection was made). Only then may a refresh token be
|
|
43
|
+
* presented again: after an abort, a timeout, or a connection lost mid-request the server may already have rotated it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function requestNeverReachedServer(error: Error): boolean;
|
|
46
|
+
//# sourceMappingURL=oauth-refresh-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-refresh-errors.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-refresh-errors.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4GAA4G;AAC5G,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC9D,MAAM,EAAE,MAAM,CAAC;CACf;AAyCD,oHAAoH;AACpH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAclE;AAED,gHAAgH;AAChH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,qBAAqB,GAAG,OAAO,CAKrF;AAED,wGAAwG;AACxG,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjE;AAuBD;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAY9F;AAED,mGAAmG;AACnG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAE/E;AAED,gHAAgH;AAChH,wBAAgB,yBAAyB,CACxC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,gBAAgB,EAAE,SAAS,MAAM,EAAE,GACjC,MAAM,GAAG,SAAS,CAIpB;AAED,mFAAmF;AACnF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAI5G;AAED;;;GAGG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IACnD,SAAkB,IAAI,+BAA+B;IACrD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzC,YAAY,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,SAAS,MAAM,EAAE,EAKjG;CACD;AAED,gHAAgH;AAChH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,IAAI,yBAAyB,CAK5F;AAOD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAK/D"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Codes that mean the stored refresh token can never be used again (OAuth 2.0 and OpenAI's refresh_token_*). */
|
|
2
|
+
const PERMANENT_REFRESH_ERROR_CODES = new Set([
|
|
3
|
+
"invalid_grant",
|
|
4
|
+
"refresh_token_expired",
|
|
5
|
+
"refresh_token_reused",
|
|
6
|
+
"refresh_token_invalidated",
|
|
7
|
+
]);
|
|
8
|
+
/** HTTP 401 from a token endpoint: the provider no longer accepts this login, whatever its error code. */
|
|
9
|
+
const UNAUTHORIZED_STATUS = 401;
|
|
10
|
+
/** A longer description is dropped rather than cut, so an echoed token cannot survive as a prefix. */
|
|
11
|
+
const MAX_DESCRIPTION_LENGTH = 300;
|
|
12
|
+
const ERROR_CODE_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
13
|
+
const URL_PATTERN = /\b[a-z][a-z0-9+.-]*:\/\//i;
|
|
14
|
+
/**
|
|
15
|
+
* Network errors raised before a request was sent: the connection was never established, so the server cannot
|
|
16
|
+
* have seen (or rotated) the refresh token. Node reports the code on `cause`; Bun on the error itself.
|
|
17
|
+
*/
|
|
18
|
+
const UNDELIVERED_REQUEST_ERROR_CODES = new Set([
|
|
19
|
+
"ECONNREFUSED",
|
|
20
|
+
"ENOTFOUND",
|
|
21
|
+
"EAI_AGAIN",
|
|
22
|
+
"ENETUNREACH",
|
|
23
|
+
"EHOSTUNREACH",
|
|
24
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
25
|
+
]);
|
|
26
|
+
function textField(value) {
|
|
27
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
28
|
+
}
|
|
29
|
+
function objectField(value) {
|
|
30
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
31
|
+
? value
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Reads `error` / `error.code` / `code` and `error_description` / `error.message` / `message` from a JSON body. */
|
|
35
|
+
export function parseOAuthErrorBody(body) {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(body);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
const json = objectField(parsed);
|
|
44
|
+
if (!json)
|
|
45
|
+
return {};
|
|
46
|
+
const nested = objectField(json.error);
|
|
47
|
+
return {
|
|
48
|
+
code: textField(json.error) ?? textField(nested?.code) ?? textField(json.code),
|
|
49
|
+
description: textField(json.error_description) ?? textField(nested?.message) ?? textField(json.message),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Whether a refresh rejection means the stored login is revoked or expired, so only signing in again helps. */
|
|
53
|
+
export function isPermanentRefreshRejection(rejection) {
|
|
54
|
+
return (rejection.status === UNAUTHORIZED_STATUS ||
|
|
55
|
+
(rejection.code !== undefined && isPermanentRefreshErrorCode(rejection.code)));
|
|
56
|
+
}
|
|
57
|
+
/** Whether `code` is one of the OAuth error codes that mark a refresh token as permanently unusable. */
|
|
58
|
+
export function isPermanentRefreshErrorCode(code) {
|
|
59
|
+
return PERMANENT_REFRESH_ERROR_CODES.has(code.toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
const JSON_STRING = String.raw `"((?:[^"\\]|\\.)*)"`;
|
|
62
|
+
const TEXT_ERROR_CODE_PATTERNS = [
|
|
63
|
+
new RegExp(String.raw `"error"\s*:\s*${JSON_STRING}`, "g"),
|
|
64
|
+
new RegExp(String.raw `"error"\s*:\s*\{[^{}]*?"code"\s*:\s*${JSON_STRING}`, "g"),
|
|
65
|
+
new RegExp(String.raw `"code"\s*:\s*${JSON_STRING}`, "g"),
|
|
66
|
+
];
|
|
67
|
+
const TEXT_ERROR_DESCRIPTION_PATTERNS = [
|
|
68
|
+
new RegExp(String.raw `"error_description"\s*:\s*${JSON_STRING}`),
|
|
69
|
+
new RegExp(String.raw `"error"\s*:\s*\{[^{}]*?"message"\s*:\s*${JSON_STRING}`),
|
|
70
|
+
];
|
|
71
|
+
const BARE_PERMANENT_REFRESH_ERROR_CODE = new RegExp(String.raw `\b(${[...PERMANENT_REFRESH_ERROR_CODES].join("|")})\b`, "i");
|
|
72
|
+
function jsonStringContent(escaped) {
|
|
73
|
+
try {
|
|
74
|
+
const value = JSON.parse(`"${escaped}"`);
|
|
75
|
+
return typeof value === "string" ? value : undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Finds a permanent refresh rejection in an error message from a refresher that does not throw
|
|
83
|
+
* {@link OAuthRefreshRejectedError}: an embedded JSON error body (`error`, nested `error.code`, or `code`) or a bare
|
|
84
|
+
* permanent code, plus the body's description. Returns nothing if no permanent code appears.
|
|
85
|
+
*/
|
|
86
|
+
export function findPermanentRefreshRejectionInText(text) {
|
|
87
|
+
const embeddedCodes = TEXT_ERROR_CODE_PATTERNS.flatMap((pattern) => [...text.matchAll(pattern)].map((match) => jsonStringContent(match[1] ?? "")));
|
|
88
|
+
const code = embeddedCodes.find((candidate) => candidate !== undefined && isPermanentRefreshErrorCode(candidate)) ??
|
|
89
|
+
BARE_PERMANENT_REFRESH_ERROR_CODE.exec(text)?.[1];
|
|
90
|
+
if (code === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
const description = TEXT_ERROR_DESCRIPTION_PATTERNS.map((pattern) => pattern.exec(text)?.[1])
|
|
93
|
+
.map((escaped) => (escaped === undefined ? undefined : jsonStringContent(escaped)))
|
|
94
|
+
.find((candidate) => candidate !== undefined);
|
|
95
|
+
return { code, description };
|
|
96
|
+
}
|
|
97
|
+
/** The provider's error code, if it looks like one; anything else could be echoed request data. */
|
|
98
|
+
export function safeOAuthErrorCode(code) {
|
|
99
|
+
return code !== undefined && ERROR_CODE_PATTERN.test(code) ? code : undefined;
|
|
100
|
+
}
|
|
101
|
+
/** The provider's description, unless it echoes a presented secret, carries a URL, or is too long to be one. */
|
|
102
|
+
export function safeOAuthErrorDescription(description, presentedSecrets) {
|
|
103
|
+
const text = description?.replace(/\s+/g, " ").trim();
|
|
104
|
+
if (!text || text.length > MAX_DESCRIPTION_LENGTH || URL_PATTERN.test(text))
|
|
105
|
+
return undefined;
|
|
106
|
+
return presentedSecrets.some((secret) => secret.length > 0 && text.includes(secret)) ? undefined : text;
|
|
107
|
+
}
|
|
108
|
+
/** `: code: description` with only the parts that are safe to show, or nothing. */
|
|
109
|
+
export function formatOAuthErrorDetail(detail, presentedSecrets) {
|
|
110
|
+
const parts = [safeOAuthErrorCode(detail.code), safeOAuthErrorDescription(detail.description, presentedSecrets)];
|
|
111
|
+
const shown = parts.filter((part) => part !== undefined);
|
|
112
|
+
return shown.length > 0 ? `: ${shown.join(": ")}` : "";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A token endpoint rejected a refresh because the stored login is revoked or expired. Carries only the HTTP status
|
|
116
|
+
* and the provider's error code and description, the latter dropped if it echoes the presented token.
|
|
117
|
+
*/
|
|
118
|
+
export class OAuthRefreshRejectedError extends Error {
|
|
119
|
+
name = "OAuthRefreshRejectedError";
|
|
120
|
+
status;
|
|
121
|
+
code;
|
|
122
|
+
description;
|
|
123
|
+
constructor(summary, rejection, presentedSecrets) {
|
|
124
|
+
super(`${summary}${formatOAuthErrorDetail(rejection, presentedSecrets)}`);
|
|
125
|
+
this.status = rejection.status;
|
|
126
|
+
this.code = safeOAuthErrorCode(rejection.code);
|
|
127
|
+
this.description = safeOAuthErrorDescription(rejection.description, presentedSecrets);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Recognises the rejection across duplicated module copies (source and built output), not only by identity. */
|
|
131
|
+
export function isOAuthRefreshRejectedError(error) {
|
|
132
|
+
return (error instanceof OAuthRefreshRejectedError ||
|
|
133
|
+
(error.name === "OAuthRefreshRejectedError" && "status" in error && typeof error.status === "number"));
|
|
134
|
+
}
|
|
135
|
+
function errorCode(error) {
|
|
136
|
+
const code = "code" in error ? error.code : undefined;
|
|
137
|
+
return typeof code === "string" ? code : undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Whether a failed fetch provably never sent its request (no connection was made). Only then may a refresh token be
|
|
141
|
+
* presented again: after an abort, a timeout, or a connection lost mid-request the server may already have rotated it.
|
|
142
|
+
*/
|
|
143
|
+
export function requestNeverReachedServer(error) {
|
|
144
|
+
if (error.name === "AbortError" || error.name === "TimeoutError")
|
|
145
|
+
return false;
|
|
146
|
+
const cause = error.cause instanceof Error ? error.cause : undefined;
|
|
147
|
+
const code = errorCode(error) ?? (cause ? errorCode(cause) : undefined);
|
|
148
|
+
return code !== undefined && UNDELIVERED_REQUEST_ERROR_CODES.has(code);
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=oauth-refresh-errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-refresh-errors.js","sourceRoot":"","sources":["../../src/auth/oauth-refresh-errors.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonValue } from \"../types.ts\";\n\n/** A token endpoint's error code and description, as the provider sent them. */\nexport interface OAuthErrorDetail {\n\tcode?: string;\n\tdescription?: string;\n}\n\n/** A token endpoint's answer to a refresh, by HTTP status and the provider's error code and description. */\nexport interface OAuthRefreshRejection extends OAuthErrorDetail {\n\tstatus: number;\n}\n\n/** Codes that mean the stored refresh token can never be used again (OAuth 2.0 and OpenAI's refresh_token_*). */\nconst PERMANENT_REFRESH_ERROR_CODES = new Set([\n\t\"invalid_grant\",\n\t\"refresh_token_expired\",\n\t\"refresh_token_reused\",\n\t\"refresh_token_invalidated\",\n]);\n\n/** HTTP 401 from a token endpoint: the provider no longer accepts this login, whatever its error code. */\nconst UNAUTHORIZED_STATUS = 401;\n\n/** A longer description is dropped rather than cut, so an echoed token cannot survive as a prefix. */\nconst MAX_DESCRIPTION_LENGTH = 300;\nconst ERROR_CODE_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;\nconst URL_PATTERN = /\\b[a-z][a-z0-9+.-]*:\\/\\//i;\n\n/**\n * Network errors raised before a request was sent: the connection was never established, so the server cannot\n * have seen (or rotated) the refresh token. Node reports the code on `cause`; Bun on the error itself.\n */\nconst UNDELIVERED_REQUEST_ERROR_CODES = new Set([\n\t\"ECONNREFUSED\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"ENETUNREACH\",\n\t\"EHOSTUNREACH\",\n\t\"UND_ERR_CONNECT_TIMEOUT\",\n]);\n\nfunction textField(value: JsonValue | undefined): string | undefined {\n\treturn typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction objectField(value: JsonValue | undefined): { readonly [key: string]: JsonValue } | undefined {\n\treturn value !== null && typeof value === \"object\" && !Array.isArray(value)\n\t\t? (value as { readonly [key: string]: JsonValue })\n\t\t: undefined;\n}\n\n/** Reads `error` / `error.code` / `code` and `error_description` / `error.message` / `message` from a JSON body. */\nexport function parseOAuthErrorBody(body: string): OAuthErrorDetail {\n\tlet parsed: JsonValue;\n\ttry {\n\t\tparsed = JSON.parse(body);\n\t} catch {\n\t\treturn {};\n\t}\n\tconst json = objectField(parsed);\n\tif (!json) return {};\n\tconst nested = objectField(json.error);\n\treturn {\n\t\tcode: textField(json.error) ?? textField(nested?.code) ?? textField(json.code),\n\t\tdescription: textField(json.error_description) ?? textField(nested?.message) ?? textField(json.message),\n\t};\n}\n\n/** Whether a refresh rejection means the stored login is revoked or expired, so only signing in again helps. */\nexport function isPermanentRefreshRejection(rejection: OAuthRefreshRejection): boolean {\n\treturn (\n\t\trejection.status === UNAUTHORIZED_STATUS ||\n\t\t(rejection.code !== undefined && isPermanentRefreshErrorCode(rejection.code))\n\t);\n}\n\n/** Whether `code` is one of the OAuth error codes that mark a refresh token as permanently unusable. */\nexport function isPermanentRefreshErrorCode(code: string): boolean {\n\treturn PERMANENT_REFRESH_ERROR_CODES.has(code.toLowerCase());\n}\n\nconst JSON_STRING = String.raw`\"((?:[^\"\\\\]|\\\\.)*)\"`;\nconst TEXT_ERROR_CODE_PATTERNS = [\n\tnew RegExp(String.raw`\"error\"\\s*:\\s*${JSON_STRING}`, \"g\"),\n\tnew RegExp(String.raw`\"error\"\\s*:\\s*\\{[^{}]*?\"code\"\\s*:\\s*${JSON_STRING}`, \"g\"),\n\tnew RegExp(String.raw`\"code\"\\s*:\\s*${JSON_STRING}`, \"g\"),\n];\nconst TEXT_ERROR_DESCRIPTION_PATTERNS = [\n\tnew RegExp(String.raw`\"error_description\"\\s*:\\s*${JSON_STRING}`),\n\tnew RegExp(String.raw`\"error\"\\s*:\\s*\\{[^{}]*?\"message\"\\s*:\\s*${JSON_STRING}`),\n];\nconst BARE_PERMANENT_REFRESH_ERROR_CODE = new RegExp(String.raw`\\b(${[...PERMANENT_REFRESH_ERROR_CODES].join(\"|\")})\\b`, \"i\");\n\nfunction jsonStringContent(escaped: string): string | undefined {\n\ttry {\n\t\tconst value: JsonValue = JSON.parse(`\"${escaped}\"`);\n\t\treturn typeof value === \"string\" ? value : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Finds a permanent refresh rejection in an error message from a refresher that does not throw\n * {@link OAuthRefreshRejectedError}: an embedded JSON error body (`error`, nested `error.code`, or `code`) or a bare\n * permanent code, plus the body's description. Returns nothing if no permanent code appears.\n */\nexport function findPermanentRefreshRejectionInText(text: string): OAuthErrorDetail | undefined {\n\tconst embeddedCodes = TEXT_ERROR_CODE_PATTERNS.flatMap((pattern) =>\n\t\t[...text.matchAll(pattern)].map((match) => jsonStringContent(match[1] ?? \"\")),\n\t);\n\tconst code =\n\t\tembeddedCodes.find((candidate) => candidate !== undefined && isPermanentRefreshErrorCode(candidate)) ??\n\t\tBARE_PERMANENT_REFRESH_ERROR_CODE.exec(text)?.[1];\n\tif (code === undefined) return undefined;\n\tconst description = TEXT_ERROR_DESCRIPTION_PATTERNS.map((pattern) => pattern.exec(text)?.[1])\n\t\t.map((escaped) => (escaped === undefined ? undefined : jsonStringContent(escaped)))\n\t\t.find((candidate) => candidate !== undefined);\n\treturn { code, description };\n}\n\n/** The provider's error code, if it looks like one; anything else could be echoed request data. */\nexport function safeOAuthErrorCode(code: string | undefined): string | undefined {\n\treturn code !== undefined && ERROR_CODE_PATTERN.test(code) ? code : undefined;\n}\n\n/** The provider's description, unless it echoes a presented secret, carries a URL, or is too long to be one. */\nexport function safeOAuthErrorDescription(\n\tdescription: string | undefined,\n\tpresentedSecrets: readonly string[],\n): string | undefined {\n\tconst text = description?.replace(/\\s+/g, \" \").trim();\n\tif (!text || text.length > MAX_DESCRIPTION_LENGTH || URL_PATTERN.test(text)) return undefined;\n\treturn presentedSecrets.some((secret) => secret.length > 0 && text.includes(secret)) ? undefined : text;\n}\n\n/** `: code: description` with only the parts that are safe to show, or nothing. */\nexport function formatOAuthErrorDetail(detail: OAuthErrorDetail, presentedSecrets: readonly string[]): string {\n\tconst parts = [safeOAuthErrorCode(detail.code), safeOAuthErrorDescription(detail.description, presentedSecrets)];\n\tconst shown = parts.filter((part): part is string => part !== undefined);\n\treturn shown.length > 0 ? `: ${shown.join(\": \")}` : \"\";\n}\n\n/**\n * A token endpoint rejected a refresh because the stored login is revoked or expired. Carries only the HTTP status\n * and the provider's error code and description, the latter dropped if it echoes the presented token.\n */\nexport class OAuthRefreshRejectedError extends Error {\n\toverride readonly name = \"OAuthRefreshRejectedError\";\n\treadonly status: number;\n\treadonly code: string | undefined;\n\treadonly description: string | undefined;\n\n\tconstructor(summary: string, rejection: OAuthRefreshRejection, presentedSecrets: readonly string[]) {\n\t\tsuper(`${summary}${formatOAuthErrorDetail(rejection, presentedSecrets)}`);\n\t\tthis.status = rejection.status;\n\t\tthis.code = safeOAuthErrorCode(rejection.code);\n\t\tthis.description = safeOAuthErrorDescription(rejection.description, presentedSecrets);\n\t}\n}\n\n/** Recognises the rejection across duplicated module copies (source and built output), not only by identity. */\nexport function isOAuthRefreshRejectedError(error: Error): error is OAuthRefreshRejectedError {\n\treturn (\n\t\terror instanceof OAuthRefreshRejectedError ||\n\t\t(error.name === \"OAuthRefreshRejectedError\" && \"status\" in error && typeof error.status === \"number\")\n\t);\n}\n\nfunction errorCode(error: Error): string | undefined {\n\tconst code = \"code\" in error ? error.code : undefined;\n\treturn typeof code === \"string\" ? code : undefined;\n}\n\n/**\n * Whether a failed fetch provably never sent its request (no connection was made). Only then may a refresh token be\n * presented again: after an abort, a timeout, or a connection lost mid-request the server may already have rotated it.\n */\nexport function requestNeverReachedServer(error: Error): boolean {\n\tif (error.name === \"AbortError\" || error.name === \"TimeoutError\") return false;\n\tconst cause = error.cause instanceof Error ? error.cause : undefined;\n\tconst code = errorCode(error) ?? (cause ? errorCode(cause) : undefined);\n\treturn code !== undefined && UNDELIVERED_REQUEST_ERROR_CODES.has(code);\n}\n"]}
|
package/dist/auth/resolve.d.ts
CHANGED
|
@@ -23,6 +23,29 @@ export declare function resolveProviderAuth(provider: {
|
|
|
23
23
|
id: string;
|
|
24
24
|
auth: ProviderAuth;
|
|
25
25
|
}, credentials: CredentialStore, authContext: AuthContext, overrides?: AuthResolutionOverrides): Promise<AuthResult | undefined>;
|
|
26
|
+
/** How long a refresh keeps its caller waiting for one token exchange. */
|
|
27
|
+
export declare const OAUTH_REFRESH_TIMEOUT_MS = 60000;
|
|
28
|
+
/**
|
|
29
|
+
* How long a refresh keeps the credential lock while its token exchange is still running, so no other refresh
|
|
30
|
+
* presents a token the exchange may already have rotated. A response after this is still persisted, lock-free.
|
|
31
|
+
*/
|
|
32
|
+
export declare const OAUTH_REFRESH_LOCK_HOLD_MS: number;
|
|
33
|
+
/**
|
|
34
|
+
* How long a graceful shutdown waits for token exchanges still in flight, and for persisting their results, before
|
|
35
|
+
* the process exits (see {@link drainPendingOAuthRotations}).
|
|
36
|
+
*
|
|
37
|
+
* Abrupt termination (SIGKILL, a crash, power loss) cannot wait. If the provider already rotated the refresh token
|
|
38
|
+
* when that happens, its response never reaches the process and the stored login keeps the replaced token; the
|
|
39
|
+
* next refresh then fails and `/login <provider>` signs in again.
|
|
40
|
+
*/
|
|
41
|
+
export declare const OAUTH_SHUTDOWN_DRAIN_MS = 3000;
|
|
42
|
+
/**
|
|
43
|
+
* Waits for every started OAuth token exchange to settle and its result to be persisted, at most `timeoutMs`.
|
|
44
|
+
* Callers stop waiting for an exchange after {@link OAUTH_REFRESH_TIMEOUT_MS} while it keeps running, so a process
|
|
45
|
+
* that exits gracefully calls this first: the provider may already have rotated the token. Resolves `true` once none
|
|
46
|
+
* is in flight, `false` when the bound passes first; never rejects.
|
|
47
|
+
*/
|
|
48
|
+
export declare function drainPendingOAuthRotations(timeoutMs?: number): Promise<boolean>;
|
|
26
49
|
/** Cancellation stops waiting for the credential lock, not an already started token rotation. */
|
|
27
50
|
export declare function refreshStoredOAuthCredential(credentials: CredentialStore, providerId: string, oauth: OAuthAuth, needsRefresh: (credential: OAuthCredential) => boolean, signal: AbortSignal): Promise<OAuthCredential | undefined>;
|
|
28
51
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/auth/resolve.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/auth/resolve.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAW/C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE7E,OAAO,KAAK,EAGX,WAAW,EACX,UAAU,EACV,UAAU,EACV,eAAe,EACf,SAAS,EACT,eAAe,EACf,YAAY,EACZ,MAAM,YAAY,CAAC;AAEpB,6FAA6F;AAC7F,eAAO,MAAM,mCAAmC,QAAS,CAAC;AAE1D,qHAAqH;AACrH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,MAAM,WAAW,uBAAuB;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,kFAAkF;IAClF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,EAC5C,WAAW,EAAE,eAAe,EAC5B,WAAW,EAAE,WAAW,EACxB,SAAS,CAAC,EAAE,uBAAuB,GACjC,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAwBjC;AA2DD,0EAA0E;AAC1E,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,0BAA0B,QAA+B,CAAC;AAQvE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAiB7C;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,SAAS,GAAE,MAAgC,GAAG,OAAO,CAAC,OAAO,CAAC,CAgB9G;AA2JD,iGAAiG;AACjG,wBAAsB,4BAA4B,CACjD,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,SAAS,EAChB,YAAY,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,EACtD,MAAM,EAAE,WAAW,GACjB,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CA4CtC;AAiDD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC3C,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE,EAC5C,MAAM,EAAE,WAAW,GACjB,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAQjC"}
|
package/dist/auth/resolve.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { normalizeProviderId } from "../provider-id.js";
|
|
2
2
|
import { operationSignal, raceWithAbortSignal } from "../utils/abort.js";
|
|
3
|
+
import { formatThrownValue } from "../utils/diagnostics.js";
|
|
3
4
|
import { ModelsError } from "../utils/models-error.js";
|
|
5
|
+
import { findPermanentRefreshRejectionInText, isOAuthRefreshRejectedError, safeOAuthErrorCode, safeOAuthErrorDescription, } from "./oauth-refresh-errors.js";
|
|
4
6
|
export { ModelsError } from "../utils/models-error.js";
|
|
5
7
|
/** Bound for pre-transport request-auth work: credential read, OAuth refresh, and toAuth. */
|
|
6
8
|
export const REQUEST_AUTH_PREPARATION_TIMEOUT_MS = 15_000;
|
|
@@ -67,7 +69,180 @@ function overlayEnvAuthContext(base, env) {
|
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
const DEFAULT_OAUTH_MINIMUM_VALIDITY_MS = 5 * 60 * 1000;
|
|
70
|
-
|
|
72
|
+
/** How long a refresh keeps its caller waiting for one token exchange. */
|
|
73
|
+
export const OAUTH_REFRESH_TIMEOUT_MS = 60_000;
|
|
74
|
+
/**
|
|
75
|
+
* How long a refresh keeps the credential lock while its token exchange is still running, so no other refresh
|
|
76
|
+
* presents a token the exchange may already have rotated. A response after this is still persisted, lock-free.
|
|
77
|
+
*/
|
|
78
|
+
export const OAUTH_REFRESH_LOCK_HOLD_MS = 2 * OAUTH_REFRESH_TIMEOUT_MS;
|
|
79
|
+
/**
|
|
80
|
+
* Signal handed to a provider's token exchange. Nothing aborts it: once sent, the provider may already have
|
|
81
|
+
* rotated the refresh token, so only the exchange's own response can tell us the replacement.
|
|
82
|
+
*/
|
|
83
|
+
const TOKEN_EXCHANGE_SIGNAL = new AbortController().signal;
|
|
84
|
+
/**
|
|
85
|
+
* How long a graceful shutdown waits for token exchanges still in flight, and for persisting their results, before
|
|
86
|
+
* the process exits (see {@link drainPendingOAuthRotations}).
|
|
87
|
+
*
|
|
88
|
+
* Abrupt termination (SIGKILL, a crash, power loss) cannot wait. If the provider already rotated the refresh token
|
|
89
|
+
* when that happens, its response never reaches the process and the stored login keeps the replaced token; the
|
|
90
|
+
* next refresh then fails and `/login <provider>` signs in again.
|
|
91
|
+
*/
|
|
92
|
+
export const OAUTH_SHUTDOWN_DRAIN_MS = 3_000;
|
|
93
|
+
/** One entry per started token exchange, settled once its result is persisted or given up on. */
|
|
94
|
+
const pendingTokenExchanges = new Set();
|
|
95
|
+
function trackTokenExchange() {
|
|
96
|
+
let settle;
|
|
97
|
+
const pending = new Promise((resolve) => {
|
|
98
|
+
settle = resolve;
|
|
99
|
+
});
|
|
100
|
+
pendingTokenExchanges.add(pending);
|
|
101
|
+
return () => {
|
|
102
|
+
pendingTokenExchanges.delete(pending);
|
|
103
|
+
settle();
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Waits for every started OAuth token exchange to settle and its result to be persisted, at most `timeoutMs`.
|
|
108
|
+
* Callers stop waiting for an exchange after {@link OAUTH_REFRESH_TIMEOUT_MS} while it keeps running, so a process
|
|
109
|
+
* that exits gracefully calls this first: the provider may already have rotated the token. Resolves `true` once none
|
|
110
|
+
* is in flight, `false` when the bound passes first; never rejects.
|
|
111
|
+
*/
|
|
112
|
+
export async function drainPendingOAuthRotations(timeoutMs = OAUTH_SHUTDOWN_DRAIN_MS) {
|
|
113
|
+
const deadline = Date.now() + timeoutMs;
|
|
114
|
+
while (pendingTokenExchanges.size > 0) {
|
|
115
|
+
const remainingMs = deadline - Date.now();
|
|
116
|
+
if (remainingMs <= 0)
|
|
117
|
+
return false;
|
|
118
|
+
let timer;
|
|
119
|
+
const expired = new Promise((resolve) => {
|
|
120
|
+
timer = setTimeout(resolve, remainingMs);
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
await Promise.race([Promise.all(pendingTokenExchanges), expired]);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const MAX_ERROR_CHAIN_DEPTH = 5;
|
|
132
|
+
/**
|
|
133
|
+
* The provider's error code (or HTTP status) and description for a refresh it rejected because the stored login is
|
|
134
|
+
* revoked or expired: an {@link OAuthRefreshRejectedError} anywhere in the cause chain, or a permanent OAuth error
|
|
135
|
+
* code in a custom refresher's message. Never request bodies, URLs, or tokens.
|
|
136
|
+
*/
|
|
137
|
+
function describeRejectedLogin(error, presented) {
|
|
138
|
+
const secrets = [presented.refresh, presented.access];
|
|
139
|
+
const describe = (code, description) => {
|
|
140
|
+
const safeDescription = safeOAuthErrorDescription(description, secrets);
|
|
141
|
+
return safeDescription ? `${code} (${safeDescription})` : code;
|
|
142
|
+
};
|
|
143
|
+
let current = error;
|
|
144
|
+
for (let depth = 0; depth < MAX_ERROR_CHAIN_DEPTH && current !== undefined && current !== null; depth++) {
|
|
145
|
+
if (current instanceof Error && isOAuthRefreshRejectedError(current)) {
|
|
146
|
+
return describe(safeOAuthErrorCode(current.code) ?? `HTTP ${current.status}`, current.description);
|
|
147
|
+
}
|
|
148
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
149
|
+
}
|
|
150
|
+
current = error;
|
|
151
|
+
for (let depth = 0; depth < MAX_ERROR_CHAIN_DEPTH && current !== undefined && current !== null; depth++) {
|
|
152
|
+
const rejection = findPermanentRefreshRejectionInText(formatThrownValue(current));
|
|
153
|
+
const code = safeOAuthErrorCode(rejection?.code);
|
|
154
|
+
if (code !== undefined)
|
|
155
|
+
return describe(code, rejection?.description);
|
|
156
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Per credential store and provider, why the last late rotation could not be saved: only the save error's `code` or
|
|
162
|
+
* `name`, never its message, which may contain paths or tokens. Keyed by store so one store's lost save is never
|
|
163
|
+
* reported against another's login. Cleared by a later save or rotation, or once a refresh failure reports it.
|
|
164
|
+
*/
|
|
165
|
+
const unsavedLateRotations = new WeakMap();
|
|
166
|
+
function unsavedLateRotationsFor(credentials) {
|
|
167
|
+
let byProvider = unsavedLateRotations.get(credentials);
|
|
168
|
+
if (byProvider === undefined) {
|
|
169
|
+
byProvider = new Map();
|
|
170
|
+
unsavedLateRotations.set(credentials, byProvider);
|
|
171
|
+
}
|
|
172
|
+
return byProvider;
|
|
173
|
+
}
|
|
174
|
+
function safeSaveErrorReason(error) {
|
|
175
|
+
if (error instanceof Error) {
|
|
176
|
+
const code = "code" in error ? error.code : undefined;
|
|
177
|
+
return typeof code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(code) ? code : error.name;
|
|
178
|
+
}
|
|
179
|
+
return "unknown error";
|
|
180
|
+
}
|
|
181
|
+
/** The sentence explaining a lost late rotation for `providerId` in `credentials`, reported once. */
|
|
182
|
+
function takeUnsavedLateRotationNote(credentials, providerId) {
|
|
183
|
+
const byProvider = unsavedLateRotations.get(credentials);
|
|
184
|
+
const reason = byProvider?.get(providerId);
|
|
185
|
+
if (reason === undefined)
|
|
186
|
+
return "";
|
|
187
|
+
byProvider?.delete(providerId);
|
|
188
|
+
return ` An earlier refresh for ${providerId} succeeded, but its new login could not be saved (${reason}).`;
|
|
189
|
+
}
|
|
190
|
+
function oauthRefreshFailure(credentials, providerId, error, presented) {
|
|
191
|
+
const rejectedLogin = describeRejectedLogin(error, presented);
|
|
192
|
+
const unsavedNote = takeUnsavedLateRotationNote(credentials, providerId);
|
|
193
|
+
if (rejectedLogin === undefined) {
|
|
194
|
+
const message = unsavedNote
|
|
195
|
+
? `OAuth refresh failed for ${providerId}.${unsavedNote}`
|
|
196
|
+
: `OAuth refresh failed for ${providerId}`;
|
|
197
|
+
return new ModelsError("oauth", message, { cause: error });
|
|
198
|
+
}
|
|
199
|
+
return new ModelsError("oauth", `OAuth refresh failed for ${providerId}: ${rejectedLogin}. The provider rejected the stored login; run /login ${providerId} to sign in again.${unsavedNote}`);
|
|
200
|
+
}
|
|
201
|
+
/** A token exchange that settles after its timeout already rotated the token server-side, so keep its result. */
|
|
202
|
+
function persistLateRotation(credentials, providerId, rotatedFrom, rotation) {
|
|
203
|
+
const settled = trackTokenExchange();
|
|
204
|
+
void rotation
|
|
205
|
+
.then((late) => credentials
|
|
206
|
+
.modify(providerId, async (current) => current?.type === "oauth" && current.refresh === rotatedFrom.refresh ? late : undefined)
|
|
207
|
+
.then(() => {
|
|
208
|
+
unsavedLateRotations.get(credentials)?.delete(providerId);
|
|
209
|
+
}, (saveError) => {
|
|
210
|
+
unsavedLateRotationsFor(credentials).set(providerId, safeSaveErrorReason(saveError));
|
|
211
|
+
}),
|
|
212
|
+
// The exchange itself failed: nothing was rotated that could be lost.
|
|
213
|
+
() => undefined)
|
|
214
|
+
.finally(settled);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Runs one token exchange under the credential lock. The caller stops waiting after {@link OAUTH_REFRESH_TIMEOUT_MS};
|
|
218
|
+
* the lock is kept until the exchange settles or {@link OAUTH_REFRESH_LOCK_HOLD_MS} passes, after which a late
|
|
219
|
+
* response is still persisted.
|
|
220
|
+
*/
|
|
221
|
+
async function rotateOAuthCredential(credentials, providerId, oauth, current, stopCallerWaiting) {
|
|
222
|
+
const rotation = oauth.refresh(current, TOKEN_EXCHANGE_SIGNAL);
|
|
223
|
+
const callerWait = AbortSignal.timeout(OAUTH_REFRESH_TIMEOUT_MS);
|
|
224
|
+
const onCallerWaitExpired = () => {
|
|
225
|
+
stopCallerWaiting(new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: callerWait.reason }));
|
|
226
|
+
};
|
|
227
|
+
callerWait.addEventListener("abort", onCallerWaitExpired, { once: true });
|
|
228
|
+
const lockHold = AbortSignal.timeout(OAUTH_REFRESH_LOCK_HOLD_MS);
|
|
229
|
+
try {
|
|
230
|
+
return await raceWithAbortSignal(rotation, lockHold);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (lockHold.aborted)
|
|
234
|
+
persistLateRotation(credentials, providerId, current, rotation);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
callerWait.removeEventListener("abort", onCallerWaitExpired);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** A login another process saved after this refresh read the store, e.g. its own rotation of the same token. */
|
|
242
|
+
async function readNewerOAuthCredential(credentials, providerId, rejected) {
|
|
243
|
+
const latest = await credentials.read(providerId).catch(() => undefined);
|
|
244
|
+
return latest?.type === "oauth" && latest.refresh !== rejected.refresh ? latest : undefined;
|
|
245
|
+
}
|
|
71
246
|
/** Cancellation stops waiting for the credential lock, not an already started token rotation. */
|
|
72
247
|
export async function refreshStoredOAuthCredential(credentials, providerId, oauth, needsRefresh, signal) {
|
|
73
248
|
let post;
|
|
@@ -76,8 +251,14 @@ export async function refreshStoredOAuthCredential(credentials, providerId, oaut
|
|
|
76
251
|
signal.addEventListener("abort", cancelLockWait, { once: true });
|
|
77
252
|
if (signal.aborted)
|
|
78
253
|
cancelLockWait();
|
|
254
|
+
let stopCallerWaiting;
|
|
255
|
+
/** Set once the token exchange starts; settles its pending entry when the transaction that commits it ends. */
|
|
256
|
+
let exchangeSettled;
|
|
257
|
+
const callerWaitExpired = new Promise((_, reject) => {
|
|
258
|
+
stopCallerWaiting = reject;
|
|
259
|
+
});
|
|
79
260
|
try {
|
|
80
|
-
|
|
261
|
+
const transaction = credentials.modify(providerId, async (current) => {
|
|
81
262
|
signal.removeEventListener("abort", cancelLockWait);
|
|
82
263
|
signal.throwIfAborted();
|
|
83
264
|
if (current?.type !== "oauth")
|
|
@@ -85,13 +266,21 @@ export async function refreshStoredOAuthCredential(credentials, providerId, oaut
|
|
|
85
266
|
if (!needsRefresh(current))
|
|
86
267
|
return undefined;
|
|
87
268
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
269
|
+
exchangeSettled ??= trackTokenExchange();
|
|
270
|
+
const rotated = await rotateOAuthCredential(credentials, providerId, oauth, current, stopCallerWaiting);
|
|
271
|
+
unsavedLateRotations.get(credentials)?.delete(providerId);
|
|
272
|
+
return rotated;
|
|
90
273
|
}
|
|
91
274
|
catch (error) {
|
|
92
|
-
|
|
275
|
+
const newer = await readNewerOAuthCredential(credentials, providerId, current);
|
|
276
|
+
if (newer)
|
|
277
|
+
return newer;
|
|
278
|
+
throw oauthRefreshFailure(credentials, providerId, error, current);
|
|
93
279
|
}
|
|
94
280
|
}, { signal: lockWait.signal });
|
|
281
|
+
const untrackExchange = () => exchangeSettled?.();
|
|
282
|
+
void transaction.then(untrackExchange, untrackExchange);
|
|
283
|
+
post = await Promise.race([transaction, callerWaitExpired]);
|
|
95
284
|
}
|
|
96
285
|
catch (error) {
|
|
97
286
|
if (error instanceof ModelsError)
|