@atproto/oauth-client 0.7.3 → 0.7.4

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @atproto/oauth-client
2
2
 
3
+ ## 0.7.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5101](https://github.com/bluesky-social/atproto/pull/5101) [`9cc6b33`](https://github.com/bluesky-social/atproto/commit/9cc6b3369878fe46554727f2ae9342376f2e41b6) Thanks [@ryanda9910](https://github.com/ryanda9910)! - Avoid relying on `AbortSignal.timeout`, which is not implemented in every runtime this package targets (notably React Native / Expo) and caused `TypeError: AbortSignal.timeout is not a function` during the OAuth login flow. A `timeoutSignal` helper now feature-detects the native static method and falls back to an `AbortController` + `setTimeout` when it is missing.
8
+
3
9
  ## 0.7.3
4
10
 
5
11
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-server-agent.d.ts","sourceRoot":"","sources":["../src/oauth-server-agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,4BAA4B,EAC5B,gCAAgC,EAChC,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAGlB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAiC,MAAM,qBAAqB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAGxD,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EAEzB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEnD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C,YAAY,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,CAAA;AAE5D,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,UAAU,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,iBAAiB,CAAA;IAExB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAExD,qBAAa,gBAAgB;IAQzB,QAAQ,CAAC,UAAU,EAAE,gBAAgB;IACrC,QAAQ,CAAC,OAAO,EAAE,GAAG;IACrB,QAAQ,CAAC,cAAc,EAAE,gCAAgC;IACzD,QAAQ,CAAC,cAAc,EAAE,cAAc;IACvC,QAAQ,CAAC,UAAU,EAAE,cAAc;IACnC,QAAQ,CAAC,aAAa,EAAE,aAAa;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAd1B,SAAS,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IACnC,SAAS,CAAC,wBAAwB,EAAE,wBAAwB,CAAA;IAE5D;;OAEG;IACH,YACW,UAAU,EAAE,gBAAgB,EAC5B,OAAO,EAAE,GAAG,EACZ,cAAc,EAAE,gCAAgC,EAChD,cAAc,EAAE,cAAc,EAC9B,UAAU,EAAE,cAAc,EAC1B,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,MAAM,YAAA,EACxB,KAAK,CAAC,EAAE,KAAK,EAkBd;IAED,IAAI,MAAM,oVAET;IAEK,MAAM,CAAC,KAAK,EAAE,MAAM,iBAMzB;IAEK,YAAY,CAChB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,gBAAgB,GAC7B,OAAO,CAAC,QAAQ,CAAC,CAuCnB;IAEK,OAAO,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqCnD;IAED;;;;;;;;;OASG;IACH,UAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAe7D;IAEK,OAAO,CAAC,QAAQ,SAAS,iBAAiB,EAC9C,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,QAAQ,SAAS,OAAO,GAC7B,iBAAiB,GACjB,QAAQ,SAAS,8BAA8B,GAC7C,4BAA4B,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CACR,QAAQ,SAAS,OAAO,GACpB,yBAAyB,GACzB,QAAQ,SAAS,8BAA8B,GAC7C,gBAAgB,GAChB,IAAI,CACX,CAAA;CAoCF"}
1
+ {"version":3,"file":"oauth-server-agent.d.ts","sourceRoot":"","sources":["../src/oauth-server-agent.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ import { TokenRefreshError } from './errors/token-refresh-error.js';
4
4
  import { dpopFetchWrapper } from './fetch-dpop.js';
5
5
  import { createClientCredentialsFactory, } from './oauth-client-auth.js';
6
6
  import { OAuthResponseError } from './oauth-response-error.js';
7
+ import { timeoutSignal } from './util.js';
7
8
  export class OAuthServerAgent {
8
9
  /**
9
10
  * @throws see {@link createClientCredentialsFactory}
@@ -117,7 +118,7 @@ export class OAuthServerAgent {
117
118
  const resolved = await this.oauthResolver.resolveFromIdentity(sub, {
118
119
  noCache: true,
119
120
  allowStale: false,
120
- signal: AbortSignal.timeout(10e3),
121
+ signal: timeoutSignal(10e3),
121
122
  });
122
123
  if (this.issuer !== resolved.metadata.issuer) {
123
124
  // Best case scenario; the user switched PDS. Worst case scenario; a bad
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-server-agent.js","sourceRoot":"","sources":["../src/oauth-server-agent.ts"],"names":[],"mappings":"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","sourcesContent":["import { AtprotoDid } from '@atproto/did'\nimport { Key, Keyset } from '@atproto/jwk'\nimport {\n AtprotoOAuthScope,\n AtprotoOAuthTokenResponse,\n OAuthAuthorizationRequestPar,\n OAuthAuthorizationServerMetadata,\n OAuthEndpointName,\n OAuthParResponse,\n OAuthRedirectUri,\n OAuthTokenRequest,\n atprotoOAuthTokenResponseSchema,\n oauthParResponseSchema,\n} from '@atproto/oauth-types'\nimport { Fetch, Json, bindFetch, fetchJsonProcessor } from '@atproto-labs/fetch'\nimport { SimpleStore } from '@atproto-labs/simple-store'\nimport { TokenRefreshError } from './errors/token-refresh-error.js'\nimport { dpopFetchWrapper } from './fetch-dpop.js'\nimport {\n ClientAuthMethod,\n ClientCredentialsFactory,\n createClientCredentialsFactory,\n} from './oauth-client-auth.js'\nimport { OAuthResolver } from './oauth-resolver.js'\nimport { OAuthResponseError } from './oauth-response-error.js'\nimport { Runtime } from './runtime.js'\nimport { ClientMetadata } from './types.js'\n\nexport type { AtprotoOAuthScope, AtprotoOAuthTokenResponse }\n\nexport type TokenSet = {\n iss: string\n sub: AtprotoDid\n aud: string\n scope: AtprotoOAuthScope\n\n refresh_token?: string\n access_token: string\n token_type: 'DPoP'\n /** ISO Date */\n expires_at?: string\n}\n\nexport type DpopNonceCache = SimpleStore<string, string>\n\nexport class OAuthServerAgent {\n protected dpopFetch: Fetch<unknown>\n protected clientCredentialsFactory: ClientCredentialsFactory\n\n /**\n * @throws see {@link createClientCredentialsFactory}\n */\n constructor(\n readonly authMethod: ClientAuthMethod,\n readonly dpopKey: Key,\n readonly serverMetadata: OAuthAuthorizationServerMetadata,\n readonly clientMetadata: ClientMetadata,\n readonly dpopNonces: DpopNonceCache,\n readonly oauthResolver: OAuthResolver,\n readonly runtime: Runtime,\n readonly keyset?: Keyset,\n fetch?: Fetch,\n ) {\n this.clientCredentialsFactory = createClientCredentialsFactory(\n authMethod,\n serverMetadata,\n clientMetadata,\n runtime,\n keyset,\n )\n\n this.dpopFetch = dpopFetchWrapper<void>({\n fetch: bindFetch(fetch),\n key: dpopKey,\n supportedAlgs: serverMetadata.dpop_signing_alg_values_supported,\n sha256: async (v) => runtime.sha256(v),\n nonces: dpopNonces,\n isAuthServer: true,\n })\n }\n\n get issuer() {\n return this.serverMetadata.issuer\n }\n\n async revoke(token: string) {\n try {\n await this.request('revocation', { token })\n } catch {\n // Don't care\n }\n }\n\n async exchangeCode(\n code: string,\n codeVerifier?: string,\n redirectUri?: OAuthRedirectUri,\n ): Promise<TokenSet> {\n const now = Date.now()\n\n const tokenResponse = await this.request('token', {\n grant_type: 'authorization_code',\n // redirectUri should always be passed by the calling code, but if it is\n // not, default to the first redirect_uri registered for the client:\n redirect_uri: redirectUri ?? this.clientMetadata.redirect_uris[0],\n code,\n code_verifier: codeVerifier,\n })\n\n try {\n // /!\\ IMPORTANT /!\\\n //\n // The tokenResponse MUST always be valid before the \"sub\" it contains\n // can be trusted (see Atproto's OAuth spec for details).\n const aud = await this.verifyIssuer(tokenResponse.sub)\n\n return {\n aud,\n sub: tokenResponse.sub,\n iss: this.issuer,\n\n scope: tokenResponse.scope,\n refresh_token: tokenResponse.refresh_token,\n access_token: tokenResponse.access_token,\n token_type: tokenResponse.token_type,\n\n expires_at:\n typeof tokenResponse.expires_in === 'number'\n ? new Date(now + tokenResponse.expires_in * 1000).toISOString()\n : undefined,\n }\n } catch (err) {\n await this.revoke(tokenResponse.access_token)\n\n throw err\n }\n }\n\n async refresh(tokenSet: TokenSet): Promise<TokenSet> {\n if (!tokenSet.refresh_token) {\n throw new TokenRefreshError(tokenSet.sub, 'No refresh token available')\n }\n\n // /!\\ IMPORTANT /!\\\n //\n // The \"sub\" MUST be a DID, whose issuer authority is indeed the server we\n // are trying to obtain credentials from. Note that we are doing this\n // *before* we actually try to refresh the token:\n // 1) To avoid unnecessary refresh\n // 2) So that the refresh is the last async operation, ensuring as few\n // async operations happen before the result gets a chance to be stored.\n const aud = await this.verifyIssuer(tokenSet.sub)\n\n const now = Date.now()\n\n const tokenResponse = await this.request('token', {\n grant_type: 'refresh_token',\n refresh_token: tokenSet.refresh_token,\n })\n\n return {\n aud,\n sub: tokenSet.sub,\n iss: this.issuer,\n\n scope: tokenResponse.scope,\n refresh_token: tokenResponse.refresh_token,\n access_token: tokenResponse.access_token,\n token_type: tokenResponse.token_type,\n\n expires_at:\n typeof tokenResponse.expires_in === 'number'\n ? new Date(now + tokenResponse.expires_in * 1000).toISOString()\n : undefined,\n }\n }\n\n /**\n * VERY IMPORTANT ! Always call this to process token responses.\n *\n * Whenever an OAuth token response is received, we **MUST** verify that the\n * \"sub\" is a DID, whose issuer authority is indeed the server we just\n * obtained credentials from. This check is a critical step to actually be\n * able to use the \"sub\" (DID) as being the actual user's identifier.\n *\n * @returns The user's PDS URL (the resource server for the user)\n */\n protected async verifyIssuer(sub: AtprotoDid): Promise<string> {\n const resolved = await this.oauthResolver.resolveFromIdentity(sub, {\n noCache: true,\n allowStale: false,\n signal: AbortSignal.timeout(10e3),\n })\n\n if (this.issuer !== resolved.metadata.issuer) {\n // Best case scenario; the user switched PDS. Worst case scenario; a bad\n // actor is trying to impersonate a user. In any case, we must not allow\n // this token to be used.\n throw new TypeError('Issuer mismatch')\n }\n\n return resolved.pds.href\n }\n\n async request<Endpoint extends OAuthEndpointName>(\n endpoint: Endpoint,\n payload: Endpoint extends 'token'\n ? OAuthTokenRequest\n : Endpoint extends 'pushed_authorization_request'\n ? OAuthAuthorizationRequestPar\n : Record<string, unknown>,\n ): Promise<\n Endpoint extends 'token'\n ? AtprotoOAuthTokenResponse\n : Endpoint extends 'pushed_authorization_request'\n ? OAuthParResponse\n : Json\n >\n async request(\n endpoint: OAuthEndpointName,\n payload: Record<string, unknown>,\n ): Promise<unknown> {\n const url = this.serverMetadata[`${endpoint}_endpoint`]\n if (!url) throw new Error(`No ${endpoint} endpoint available`)\n\n const auth = await this.clientCredentialsFactory()\n\n // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-3.2.2\n // https://datatracker.ietf.org/doc/html/rfc7009#section-2.1\n // https://datatracker.ietf.org/doc/html/rfc7662#section-2.1\n // https://datatracker.ietf.org/doc/html/rfc9126#section-2\n const { response, json } = await this.dpopFetch(url, {\n method: 'POST',\n headers: {\n ...auth.headers,\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: wwwFormUrlEncode({ ...payload, ...auth.payload }),\n }).then(fetchJsonProcessor())\n\n if (response.ok) {\n switch (endpoint) {\n case 'token':\n return atprotoOAuthTokenResponseSchema.parse(json)\n case 'pushed_authorization_request':\n return oauthParResponseSchema.parse(json)\n default:\n return json\n }\n } else {\n throw new OAuthResponseError(response, json)\n }\n }\n}\n\nfunction wwwFormUrlEncode(payload: Record<string, undefined | unknown>) {\n return new URLSearchParams(\n Object.entries(payload)\n .filter(entryHasDefinedValue)\n .map(stringifyEntryValue),\n ).toString()\n}\n\nfunction entryHasDefinedValue(\n entry: [string, unknown],\n): entry is [string, null | NonNullable<unknown>] {\n return entry[1] !== undefined\n}\n\nfunction stringifyEntryValue(entry: [string, unknown]): [string, string] {\n const name = entry[0]\n const value = entry[1]\n\n switch (typeof value) {\n case 'string':\n return [name, value]\n case 'number':\n case 'boolean':\n return [name, String(value)]\n default: {\n const enc = JSON.stringify(value)\n if (enc === undefined) {\n throw new Error(`Unsupported value type for ${name}: ${String(value)}`)\n }\n return [name, enc]\n }\n }\n}\n"]}
1
+ {"version":3,"file":"oauth-server-agent.js","sourceRoot":"","sources":["../src/oauth-server-agent.ts"],"names":[],"mappings":"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","sourcesContent":["import { AtprotoDid } from '@atproto/did'\nimport { Key, Keyset } from '@atproto/jwk'\nimport {\n AtprotoOAuthScope,\n AtprotoOAuthTokenResponse,\n OAuthAuthorizationRequestPar,\n OAuthAuthorizationServerMetadata,\n OAuthEndpointName,\n OAuthParResponse,\n OAuthRedirectUri,\n OAuthTokenRequest,\n atprotoOAuthTokenResponseSchema,\n oauthParResponseSchema,\n} from '@atproto/oauth-types'\nimport { Fetch, Json, bindFetch, fetchJsonProcessor } from '@atproto-labs/fetch'\nimport { SimpleStore } from '@atproto-labs/simple-store'\nimport { TokenRefreshError } from './errors/token-refresh-error.js'\nimport { dpopFetchWrapper } from './fetch-dpop.js'\nimport {\n ClientAuthMethod,\n ClientCredentialsFactory,\n createClientCredentialsFactory,\n} from './oauth-client-auth.js'\nimport { OAuthResolver } from './oauth-resolver.js'\nimport { OAuthResponseError } from './oauth-response-error.js'\nimport { Runtime } from './runtime.js'\nimport { ClientMetadata } from './types.js'\nimport { timeoutSignal } from './util.js'\n\nexport type { AtprotoOAuthScope, AtprotoOAuthTokenResponse }\n\nexport type TokenSet = {\n iss: string\n sub: AtprotoDid\n aud: string\n scope: AtprotoOAuthScope\n\n refresh_token?: string\n access_token: string\n token_type: 'DPoP'\n /** ISO Date */\n expires_at?: string\n}\n\nexport type DpopNonceCache = SimpleStore<string, string>\n\nexport class OAuthServerAgent {\n protected dpopFetch: Fetch<unknown>\n protected clientCredentialsFactory: ClientCredentialsFactory\n\n /**\n * @throws see {@link createClientCredentialsFactory}\n */\n constructor(\n readonly authMethod: ClientAuthMethod,\n readonly dpopKey: Key,\n readonly serverMetadata: OAuthAuthorizationServerMetadata,\n readonly clientMetadata: ClientMetadata,\n readonly dpopNonces: DpopNonceCache,\n readonly oauthResolver: OAuthResolver,\n readonly runtime: Runtime,\n readonly keyset?: Keyset,\n fetch?: Fetch,\n ) {\n this.clientCredentialsFactory = createClientCredentialsFactory(\n authMethod,\n serverMetadata,\n clientMetadata,\n runtime,\n keyset,\n )\n\n this.dpopFetch = dpopFetchWrapper<void>({\n fetch: bindFetch(fetch),\n key: dpopKey,\n supportedAlgs: serverMetadata.dpop_signing_alg_values_supported,\n sha256: async (v) => runtime.sha256(v),\n nonces: dpopNonces,\n isAuthServer: true,\n })\n }\n\n get issuer() {\n return this.serverMetadata.issuer\n }\n\n async revoke(token: string) {\n try {\n await this.request('revocation', { token })\n } catch {\n // Don't care\n }\n }\n\n async exchangeCode(\n code: string,\n codeVerifier?: string,\n redirectUri?: OAuthRedirectUri,\n ): Promise<TokenSet> {\n const now = Date.now()\n\n const tokenResponse = await this.request('token', {\n grant_type: 'authorization_code',\n // redirectUri should always be passed by the calling code, but if it is\n // not, default to the first redirect_uri registered for the client:\n redirect_uri: redirectUri ?? this.clientMetadata.redirect_uris[0],\n code,\n code_verifier: codeVerifier,\n })\n\n try {\n // /!\\ IMPORTANT /!\\\n //\n // The tokenResponse MUST always be valid before the \"sub\" it contains\n // can be trusted (see Atproto's OAuth spec for details).\n const aud = await this.verifyIssuer(tokenResponse.sub)\n\n return {\n aud,\n sub: tokenResponse.sub,\n iss: this.issuer,\n\n scope: tokenResponse.scope,\n refresh_token: tokenResponse.refresh_token,\n access_token: tokenResponse.access_token,\n token_type: tokenResponse.token_type,\n\n expires_at:\n typeof tokenResponse.expires_in === 'number'\n ? new Date(now + tokenResponse.expires_in * 1000).toISOString()\n : undefined,\n }\n } catch (err) {\n await this.revoke(tokenResponse.access_token)\n\n throw err\n }\n }\n\n async refresh(tokenSet: TokenSet): Promise<TokenSet> {\n if (!tokenSet.refresh_token) {\n throw new TokenRefreshError(tokenSet.sub, 'No refresh token available')\n }\n\n // /!\\ IMPORTANT /!\\\n //\n // The \"sub\" MUST be a DID, whose issuer authority is indeed the server we\n // are trying to obtain credentials from. Note that we are doing this\n // *before* we actually try to refresh the token:\n // 1) To avoid unnecessary refresh\n // 2) So that the refresh is the last async operation, ensuring as few\n // async operations happen before the result gets a chance to be stored.\n const aud = await this.verifyIssuer(tokenSet.sub)\n\n const now = Date.now()\n\n const tokenResponse = await this.request('token', {\n grant_type: 'refresh_token',\n refresh_token: tokenSet.refresh_token,\n })\n\n return {\n aud,\n sub: tokenSet.sub,\n iss: this.issuer,\n\n scope: tokenResponse.scope,\n refresh_token: tokenResponse.refresh_token,\n access_token: tokenResponse.access_token,\n token_type: tokenResponse.token_type,\n\n expires_at:\n typeof tokenResponse.expires_in === 'number'\n ? new Date(now + tokenResponse.expires_in * 1000).toISOString()\n : undefined,\n }\n }\n\n /**\n * VERY IMPORTANT ! Always call this to process token responses.\n *\n * Whenever an OAuth token response is received, we **MUST** verify that the\n * \"sub\" is a DID, whose issuer authority is indeed the server we just\n * obtained credentials from. This check is a critical step to actually be\n * able to use the \"sub\" (DID) as being the actual user's identifier.\n *\n * @returns The user's PDS URL (the resource server for the user)\n */\n protected async verifyIssuer(sub: AtprotoDid): Promise<string> {\n const resolved = await this.oauthResolver.resolveFromIdentity(sub, {\n noCache: true,\n allowStale: false,\n signal: timeoutSignal(10e3),\n })\n\n if (this.issuer !== resolved.metadata.issuer) {\n // Best case scenario; the user switched PDS. Worst case scenario; a bad\n // actor is trying to impersonate a user. In any case, we must not allow\n // this token to be used.\n throw new TypeError('Issuer mismatch')\n }\n\n return resolved.pds.href\n }\n\n async request<Endpoint extends OAuthEndpointName>(\n endpoint: Endpoint,\n payload: Endpoint extends 'token'\n ? OAuthTokenRequest\n : Endpoint extends 'pushed_authorization_request'\n ? OAuthAuthorizationRequestPar\n : Record<string, unknown>,\n ): Promise<\n Endpoint extends 'token'\n ? AtprotoOAuthTokenResponse\n : Endpoint extends 'pushed_authorization_request'\n ? OAuthParResponse\n : Json\n >\n async request(\n endpoint: OAuthEndpointName,\n payload: Record<string, unknown>,\n ): Promise<unknown> {\n const url = this.serverMetadata[`${endpoint}_endpoint`]\n if (!url) throw new Error(`No ${endpoint} endpoint available`)\n\n const auth = await this.clientCredentialsFactory()\n\n // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-3.2.2\n // https://datatracker.ietf.org/doc/html/rfc7009#section-2.1\n // https://datatracker.ietf.org/doc/html/rfc7662#section-2.1\n // https://datatracker.ietf.org/doc/html/rfc9126#section-2\n const { response, json } = await this.dpopFetch(url, {\n method: 'POST',\n headers: {\n ...auth.headers,\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: wwwFormUrlEncode({ ...payload, ...auth.payload }),\n }).then(fetchJsonProcessor())\n\n if (response.ok) {\n switch (endpoint) {\n case 'token':\n return atprotoOAuthTokenResponseSchema.parse(json)\n case 'pushed_authorization_request':\n return oauthParResponseSchema.parse(json)\n default:\n return json\n }\n } else {\n throw new OAuthResponseError(response, json)\n }\n }\n}\n\nfunction wwwFormUrlEncode(payload: Record<string, undefined | unknown>) {\n return new URLSearchParams(\n Object.entries(payload)\n .filter(entryHasDefinedValue)\n .map(stringifyEntryValue),\n ).toString()\n}\n\nfunction entryHasDefinedValue(\n entry: [string, unknown],\n): entry is [string, null | NonNullable<unknown>] {\n return entry[1] !== undefined\n}\n\nfunction stringifyEntryValue(entry: [string, unknown]): [string, string] {\n const name = entry[0]\n const value = entry[1]\n\n switch (typeof value) {\n case 'string':\n return [name, value]\n case 'number':\n case 'boolean':\n return [name, String(value)]\n default: {\n const enc = JSON.stringify(value)\n if (enc === undefined) {\n throw new Error(`Unsupported value type for ${name}: ${String(value)}`)\n }\n return [name, enc]\n }\n }\n}\n"]}
@@ -56,7 +56,7 @@ import { TokenInvalidError } from './errors/token-invalid-error.js';
56
56
  import { TokenRefreshError } from './errors/token-refresh-error.js';
57
57
  import { TokenRevokedError } from './errors/token-revoked-error.js';
58
58
  import { OAuthResponseError } from './oauth-response-error.js';
59
- import { combineSignals } from './util.js';
59
+ import { combineSignals, timeoutSignal } from './util.js';
60
60
  export function isExpectedSessionError(err) {
61
61
  return (err instanceof TokenRefreshError ||
62
62
  err instanceof TokenRevokedError ||
@@ -227,7 +227,7 @@ export class SessionGetter extends CachedGetter {
227
227
  try {
228
228
  // Make sure, even if there is no signal in the options, that the
229
229
  // request will be cancelled after at most 30 seconds.
230
- const signal = AbortSignal.timeout(30e3);
230
+ const signal = timeoutSignal(30e3);
231
231
  const abortController = __addDisposableResource(env_1, combineSignals([options?.signal, signal]), false);
232
232
  return await super.get(sub, {
233
233
  ...options,
@@ -1 +1 @@
1
- {"version":3,"file":"session-getter.js","sourceRoot":"","sources":["../src/session-getter.ts"],"names":[],"mappings":"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","sourcesContent":["import { AtprotoDid } from '@atproto/did'\nimport { Key } from '@atproto/jwk'\nimport {\n CachedGetter,\n GetCachedOptions,\n GetOptions,\n SimpleStore,\n} from '@atproto-labs/simple-store'\nimport { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'\nimport { TokenInvalidError } from './errors/token-invalid-error.js'\nimport { TokenRefreshError } from './errors/token-refresh-error.js'\nimport { TokenRevokedError } from './errors/token-revoked-error.js'\nimport { ClientAuthMethod } from './oauth-client-auth.js'\nimport { OAuthResponseError } from './oauth-response-error.js'\nimport { TokenSet } from './oauth-server-agent.js'\nimport { OAuthServerFactory } from './oauth-server-factory.js'\nimport { Runtime } from './runtime.js'\nimport { combineSignals } from './util.js'\n\nexport type Session = {\n dpopKey: Key\n authMethod: ClientAuthMethod\n tokenSet: TokenSet\n}\n\nexport type SessionStore = SimpleStore<string, Session>\n\nexport type SessionHooks = {\n onUpdate?: (sub: AtprotoDid, session: Session) => void\n onDelete?: (\n sub: AtprotoDid,\n cause: TokenRefreshError | TokenRevokedError | TokenInvalidError | unknown,\n ) => void\n}\n\nexport function isExpectedSessionError(err: unknown) {\n return (\n err instanceof TokenRefreshError ||\n err instanceof TokenRevokedError ||\n err instanceof TokenInvalidError ||\n err instanceof AuthMethodUnsatisfiableError ||\n // The stored session is invalid (e.g. missing properties) and cannot\n // be used properly\n err instanceof TypeError\n )\n}\n\n/**\n * There are several advantages to wrapping the sessionStore in a (single)\n * CachedGetter, the main of which is that the cached getter will ensure that at\n * most one fresh call is ever being made. Another advantage, is that it\n * contains the logic for reading from the cache which, if the cache is based on\n * localStorage/indexedDB, will sync across multiple tabs (for a given sub).\n */\nexport class SessionGetter extends CachedGetter<AtprotoDid, Session> {\n constructor(\n sessionStore: SessionStore,\n serverFactory: OAuthServerFactory,\n private readonly runtime: Runtime,\n private readonly hooks: SessionHooks = {},\n ) {\n super(\n async (sub, { signal }, storedSession) => {\n // There needs to be a previous session to be able to refresh. If\n // storedSession is undefined, it means that the store does not contain\n // a session for the given sub.\n if (storedSession === undefined) {\n // Because the session is not in the store, this.delStored() method\n // will not be called by the CachedGetter class (because there is\n // nothing to delete). This would typically happen if there is no\n // synchronization mechanism between instances of this class. Let's\n // make sure an event is dispatched here if this occurs.\n const msg = 'The session was deleted by another process'\n const cause = new TokenRefreshError(sub, msg)\n await hooks.onDelete?.call(null, sub, cause)\n throw cause\n }\n\n // @NOTE Throwing a TokenRefreshError (or any other error class defined\n // in the deleteOnError options) will result in this.delStored() being\n // called.\n\n const { dpopKey, authMethod, tokenSet } = storedSession\n\n if (sub !== tokenSet.sub) {\n // Fool-proofing (e.g. against invalid session storage)\n throw new TokenRefreshError(sub, 'Stored session sub mismatch')\n }\n\n if (!tokenSet.refresh_token) {\n throw new TokenRefreshError(sub, 'No refresh token available')\n }\n\n const server = await serverFactory.fromIssuer(\n tokenSet.iss,\n authMethod,\n dpopKey,\n )\n\n // Because refresh tokens can only be used once, we must not use the\n // \"signal\" to abort the refresh, or throw any abort error beyond this\n // point. Any thrown error beyond this point will prevent the\n // TokenGetter from obtaining, and storing, the new token set,\n // effectively rendering the currently saved session unusable.\n signal?.throwIfAborted()\n\n try {\n const newTokenSet = await server.refresh(tokenSet)\n\n if (sub !== newTokenSet.sub) {\n // The server returned another sub. Was the tokenSet manipulated?\n throw new TokenRefreshError(sub, 'Token set sub mismatch')\n }\n\n return {\n dpopKey,\n tokenSet: newTokenSet,\n authMethod: server.authMethod,\n }\n } catch (cause) {\n // Since refresh tokens can only be used once, we might run into\n // concurrency issues if multiple instances (e.g. browser tabs) are\n // trying to refresh the same token simultaneously. The chances of\n // this happening when multiple instances are started simultaneously\n // is reduced by randomizing the expiry time (see isStale() below).\n // The best solution is to use a mutex/lock to ensure that only one\n // instance is refreshing the token at a time (runtime.usingLock) but\n // that is not always possible. Let's try to recover from concurrency\n // issues, or force the session to be deleted by throwing a\n // TokenRefreshError.\n if (\n cause instanceof OAuthResponseError &&\n cause.status === 400 &&\n cause.error === 'invalid_grant'\n ) {\n // In case there is no lock implementation in the runtime, we will\n // wait for a short time to give the other concurrent instances a\n // chance to finish their refreshing of the token. If a concurrent\n // refresh did occur, we will pretend that this one succeeded.\n if (!runtime.hasImplementationLock) {\n await new Promise((r) => setTimeout(r, 1000))\n\n const stored = await this.getStored(sub)\n if (stored === undefined) {\n // A concurrent refresh occurred and caused the session to be\n // deleted (for a reason we can't know at this point).\n\n // Using a distinct error message mainly for debugging\n // purposes. Also, throwing a TokenRefreshError to trigger\n // deletion through the deleteOnError callback.\n const msg = 'The session was deleted by another process'\n throw new TokenRefreshError(sub, msg, { cause })\n } else if (\n stored.tokenSet.access_token !== tokenSet.access_token ||\n stored.tokenSet.refresh_token !== tokenSet.refresh_token\n ) {\n // A concurrent refresh occurred. Pretend this one succeeded.\n return stored\n } else {\n // There were no concurrent refresh. The token is (likely)\n // simply no longer valid.\n }\n }\n\n // Make sure the session gets deleted from the store\n const msg = cause.errorDescription ?? 'The session was revoked'\n throw new TokenRefreshError(sub, msg, { cause })\n }\n\n throw cause\n }\n },\n sessionStore,\n {\n isStale: (sub, { tokenSet }) => {\n return (\n tokenSet.expires_at != null &&\n new Date(tokenSet.expires_at).getTime() <\n Date.now() +\n // Add some lee way to ensure the token is not expired when it\n // reaches the server.\n 10e3 +\n // Add some randomness to reduce the chances of multiple\n // instances trying to refresh the token at the same.\n 30e3 * Math.random()\n )\n },\n onStoreError: async (err, sub, { tokenSet, dpopKey, authMethod }) => {\n // If the token data cannot be stored, let's revoke it\n try {\n const server = await serverFactory.fromIssuer(\n tokenSet.iss,\n authMethod,\n dpopKey,\n )\n await server.revoke(tokenSet.refresh_token ?? tokenSet.access_token)\n } catch {\n // At least we tried...\n }\n\n // Attempt to delete the session from the store. Note that this might\n // fail if the store is not available, which is fine.\n try {\n await this.delStored(sub, err)\n } catch {\n // Ignore (better to propagate the original storage error)\n }\n\n throw err\n },\n deleteOnError: isExpectedSessionError,\n },\n )\n }\n\n override async getStored(\n sub: AtprotoDid,\n options?: GetOptions,\n ): Promise<Session | undefined> {\n return super.getStored(sub, options)\n }\n\n override async setStored(sub: AtprotoDid, session: Session) {\n // Prevent tampering with the stored value\n if (sub !== session.tokenSet.sub) {\n throw new TypeError('Token set does not match the expected sub')\n }\n await super.setStored(sub, session)\n await this.hooks.onUpdate?.call(null, sub, session)\n }\n\n override async delStored(sub: AtprotoDid, cause?: unknown): Promise<void> {\n await super.delStored(sub, cause)\n await this.hooks.onDelete?.call(null, sub, cause)\n }\n\n /**\n * @deprecated Use {@link getSession} instead\n * @internal (not really deprecated)\n */\n override async get(\n sub: AtprotoDid,\n options?: GetCachedOptions,\n ): Promise<Session> {\n const session = await this.runtime.usingLock(\n `@atproto-oauth-client-${sub}`,\n async () => {\n // Make sure, even if there is no signal in the options, that the\n // request will be cancelled after at most 30 seconds.\n const signal = AbortSignal.timeout(30e3)\n\n using abortController = combineSignals([options?.signal, signal])\n\n return await super.get(sub, {\n ...options,\n signal: abortController.signal,\n })\n },\n )\n\n if (sub !== session.tokenSet.sub) {\n // Fool-proofing (e.g. against invalid session storage)\n throw new Error('Token set does not match the expected sub')\n }\n\n return session\n }\n\n /**\n * @param refresh When `true`, the credentials will be refreshed even if they\n * are not expired. When `false`, the credentials will not be refreshed even\n * if they are expired. When `undefined`, the credentials will be refreshed\n * if, and only if, they are (about to be) expired. Defaults to `undefined`.\n */\n async getSession(sub: AtprotoDid, refresh: boolean | 'auto' = 'auto') {\n return this.get(sub, {\n noCache: refresh === true,\n allowStale: refresh === false,\n })\n }\n}\n"]}
1
+ {"version":3,"file":"session-getter.js","sourceRoot":"","sources":["../src/session-getter.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,OAAO,EACL,YAAY,GAIb,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,4BAA4B,EAAE,MAAM,6CAA6C,CAAA;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA;AAEnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAI9D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAkBzD,MAAM,UAAU,sBAAsB,CAAC,GAAY;IACjD,OAAO,CACL,GAAG,YAAY,iBAAiB;QAChC,GAAG,YAAY,iBAAiB;QAChC,GAAG,YAAY,iBAAiB;QAChC,GAAG,YAAY,4BAA4B;QAC3C,qEAAqE;QACrE,mBAAmB;QACnB,GAAG,YAAY,SAAS,CACzB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,aAAc,SAAQ,YAAiC;IAClE,YACE,YAA0B,EAC1B,aAAiC,EAChB,OAAgB,EAChB,KAAK,GAAiB,EAAE;QAEzC,KAAK,CACH,KAAK,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,EAAE;YACvC,iEAAiE;YACjE,uEAAuE;YACvE,+BAA+B;YAC/B,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;gBAChC,mEAAmE;gBACnE,iEAAiE;gBACjE,iEAAiE;gBACjE,mEAAmE;gBACnE,wDAAwD;gBACxD,MAAM,GAAG,GAAG,4CAA4C,CAAA;gBACxD,MAAM,KAAK,GAAG,IAAI,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;gBAC7C,MAAM,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;gBAC5C,MAAM,KAAK,CAAA;YACb,CAAC;YAED,uEAAuE;YACvE,sEAAsE;YACtE,UAAU;YAEV,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAA;YAEvD,IAAI,GAAG,KAAK,QAAQ,CAAC,GAAG,EAAE,CAAC;gBACzB,uDAAuD;gBACvD,MAAM,IAAI,iBAAiB,CAAC,GAAG,EAAE,6BAA6B,CAAC,CAAA;YACjE,CAAC;YAED,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;gBAC5B,MAAM,IAAI,iBAAiB,CAAC,GAAG,EAAE,4BAA4B,CAAC,CAAA;YAChE,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,UAAU,CAC3C,QAAQ,CAAC,GAAG,EACZ,UAAU,EACV,OAAO,CACR,CAAA;YAED,oEAAoE;YACpE,sEAAsE;YACtE,6DAA6D;YAC7D,8DAA8D;YAC9D,8DAA8D;YAC9D,MAAM,EAAE,cAAc,EAAE,CAAA;YAExB,IAAI,CAAC;gBACH,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;gBAElD,IAAI,GAAG,KAAK,WAAW,CAAC,GAAG,EAAE,CAAC;oBAC5B,iEAAiE;oBACjE,MAAM,IAAI,iBAAiB,CAAC,GAAG,EAAE,wBAAwB,CAAC,CAAA;gBAC5D,CAAC;gBAED,OAAO;oBACL,OAAO;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,MAAM,CAAC,UAAU;iBAC9B,CAAA;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,gEAAgE;gBAChE,mEAAmE;gBACnE,kEAAkE;gBAClE,oEAAoE;gBACpE,mEAAmE;gBACnE,mEAAmE;gBACnE,qEAAqE;gBACrE,qEAAqE;gBACrE,2DAA2D;gBAC3D,qBAAqB;gBACrB,IACE,KAAK,YAAY,kBAAkB;oBACnC,KAAK,CAAC,MAAM,KAAK,GAAG;oBACpB,KAAK,CAAC,KAAK,KAAK,eAAe,EAC/B,CAAC;oBACD,kEAAkE;oBAClE,iEAAiE;oBACjE,kEAAkE;oBAClE,8DAA8D;oBAC9D,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC;wBACnC,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAA;wBAE7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;wBACxC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;4BACzB,6DAA6D;4BAC7D,sDAAsD;4BAEtD,sDAAsD;4BACtD,0DAA0D;4BAC1D,+CAA+C;4BAC/C,MAAM,GAAG,GAAG,4CAA4C,CAAA;4BACxD,MAAM,IAAI,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;wBAClD,CAAC;6BAAM,IACL,MAAM,CAAC,QAAQ,CAAC,YAAY,KAAK,QAAQ,CAAC,YAAY;4BACtD,MAAM,CAAC,QAAQ,CAAC,aAAa,KAAK,QAAQ,CAAC,aAAa,EACxD,CAAC;4BACD,6DAA6D;4BAC7D,OAAO,MAAM,CAAA;wBACf,CAAC;6BAAM,CAAC;4BACN,0DAA0D;4BAC1D,0BAA0B;wBAC5B,CAAC;oBACH,CAAC;oBAED,oDAAoD;oBACpD,MAAM,GAAG,GAAG,KAAK,CAAC,gBAAgB,IAAI,yBAAyB,CAAA;oBAC/D,MAAM,IAAI,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;gBAClD,CAAC;gBAED,MAAM,KAAK,CAAA;YACb,CAAC;QACH,CAAC,EACD,YAAY,EACZ;YACE,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;gBAC7B,OAAO,CACL,QAAQ,CAAC,UAAU,IAAI,IAAI;oBAC3B,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;wBACrC,IAAI,CAAC,GAAG,EAAE;4BACR,8DAA8D;4BAC9D,sBAAsB;4BACtB,IAAI;4BACJ,wDAAwD;4BACxD,qDAAqD;4BACrD,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CACzB,CAAA;YACH,CAAC;YACD,YAAY,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE;gBAClE,sDAAsD;gBACtD,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,UAAU,CAC3C,QAAQ,CAAC,GAAG,EACZ,UAAU,EACV,OAAO,CACR,CAAA;oBACD,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,IAAI,QAAQ,CAAC,YAAY,CAAC,CAAA;gBACtE,CAAC;gBAAC,MAAM,CAAC;oBACP,uBAAuB;gBACzB,CAAC;gBAED,qEAAqE;gBACrE,qDAAqD;gBACrD,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;gBAChC,CAAC;gBAAC,MAAM,CAAC;oBACP,0DAA0D;gBAC5D,CAAC;gBAED,MAAM,GAAG,CAAA;YACX,CAAC;YACD,aAAa,EAAE,sBAAsB;SACtC,CACF,CAAA;uBA1JgB,OAAO;qBACP,KAAK;IA0JxB,CAAC;IAEQ,KAAK,CAAC,SAAS,CACtB,GAAe,EACf,OAAoB;QAEpB,OAAO,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IACtC,CAAC;IAEQ,KAAK,CAAC,SAAS,CAAC,GAAe,EAAE,OAAgB;QACxD,0CAA0C;QAC1C,IAAI,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACjC,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAA;QAClE,CAAC;QACD,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QACnC,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;IACrD,CAAC;IAEQ,KAAK,CAAC,SAAS,CAAC,GAAe,EAAE,KAAe;QACvD,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QACjC,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;IACnD,CAAC;IAED;;;OAGG;IACM,KAAK,CAAC,GAAG,CAChB,GAAe,EACf,OAA0B;QAE1B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAC1C,yBAAyB,GAAG,EAAE,EAC9B,KAAK,IAAI,EAAE;;;gBACT,iEAAiE;gBACjE,sDAAsD;gBACtD,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;gBAElC,MAAM,eAAe,kCAAG,cAAc,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,QAAA,CAAA;gBAEjE,OAAO,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;oBAC1B,GAAG,OAAO;oBACV,MAAM,EAAE,eAAe,CAAC,MAAM;iBAC/B,CAAC,CAAA;;;;;;;;;QACJ,CAAC,CACF,CAAA;QAED,IAAI,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACjC,uDAAuD;YACvD,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;QAC9D,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,UAAU,CAAC,GAAe,EAAE,OAAO,GAAqB,MAAM;QAClE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;YACnB,OAAO,EAAE,OAAO,KAAK,IAAI;YACzB,UAAU,EAAE,OAAO,KAAK,KAAK;SAC9B,CAAC,CAAA;IACJ,CAAC;CACF","sourcesContent":["import { AtprotoDid } from '@atproto/did'\nimport { Key } from '@atproto/jwk'\nimport {\n CachedGetter,\n GetCachedOptions,\n GetOptions,\n SimpleStore,\n} from '@atproto-labs/simple-store'\nimport { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'\nimport { TokenInvalidError } from './errors/token-invalid-error.js'\nimport { TokenRefreshError } from './errors/token-refresh-error.js'\nimport { TokenRevokedError } from './errors/token-revoked-error.js'\nimport { ClientAuthMethod } from './oauth-client-auth.js'\nimport { OAuthResponseError } from './oauth-response-error.js'\nimport { TokenSet } from './oauth-server-agent.js'\nimport { OAuthServerFactory } from './oauth-server-factory.js'\nimport { Runtime } from './runtime.js'\nimport { combineSignals, timeoutSignal } from './util.js'\n\nexport type Session = {\n dpopKey: Key\n authMethod: ClientAuthMethod\n tokenSet: TokenSet\n}\n\nexport type SessionStore = SimpleStore<string, Session>\n\nexport type SessionHooks = {\n onUpdate?: (sub: AtprotoDid, session: Session) => void\n onDelete?: (\n sub: AtprotoDid,\n cause: TokenRefreshError | TokenRevokedError | TokenInvalidError | unknown,\n ) => void\n}\n\nexport function isExpectedSessionError(err: unknown) {\n return (\n err instanceof TokenRefreshError ||\n err instanceof TokenRevokedError ||\n err instanceof TokenInvalidError ||\n err instanceof AuthMethodUnsatisfiableError ||\n // The stored session is invalid (e.g. missing properties) and cannot\n // be used properly\n err instanceof TypeError\n )\n}\n\n/**\n * There are several advantages to wrapping the sessionStore in a (single)\n * CachedGetter, the main of which is that the cached getter will ensure that at\n * most one fresh call is ever being made. Another advantage, is that it\n * contains the logic for reading from the cache which, if the cache is based on\n * localStorage/indexedDB, will sync across multiple tabs (for a given sub).\n */\nexport class SessionGetter extends CachedGetter<AtprotoDid, Session> {\n constructor(\n sessionStore: SessionStore,\n serverFactory: OAuthServerFactory,\n private readonly runtime: Runtime,\n private readonly hooks: SessionHooks = {},\n ) {\n super(\n async (sub, { signal }, storedSession) => {\n // There needs to be a previous session to be able to refresh. If\n // storedSession is undefined, it means that the store does not contain\n // a session for the given sub.\n if (storedSession === undefined) {\n // Because the session is not in the store, this.delStored() method\n // will not be called by the CachedGetter class (because there is\n // nothing to delete). This would typically happen if there is no\n // synchronization mechanism between instances of this class. Let's\n // make sure an event is dispatched here if this occurs.\n const msg = 'The session was deleted by another process'\n const cause = new TokenRefreshError(sub, msg)\n await hooks.onDelete?.call(null, sub, cause)\n throw cause\n }\n\n // @NOTE Throwing a TokenRefreshError (or any other error class defined\n // in the deleteOnError options) will result in this.delStored() being\n // called.\n\n const { dpopKey, authMethod, tokenSet } = storedSession\n\n if (sub !== tokenSet.sub) {\n // Fool-proofing (e.g. against invalid session storage)\n throw new TokenRefreshError(sub, 'Stored session sub mismatch')\n }\n\n if (!tokenSet.refresh_token) {\n throw new TokenRefreshError(sub, 'No refresh token available')\n }\n\n const server = await serverFactory.fromIssuer(\n tokenSet.iss,\n authMethod,\n dpopKey,\n )\n\n // Because refresh tokens can only be used once, we must not use the\n // \"signal\" to abort the refresh, or throw any abort error beyond this\n // point. Any thrown error beyond this point will prevent the\n // TokenGetter from obtaining, and storing, the new token set,\n // effectively rendering the currently saved session unusable.\n signal?.throwIfAborted()\n\n try {\n const newTokenSet = await server.refresh(tokenSet)\n\n if (sub !== newTokenSet.sub) {\n // The server returned another sub. Was the tokenSet manipulated?\n throw new TokenRefreshError(sub, 'Token set sub mismatch')\n }\n\n return {\n dpopKey,\n tokenSet: newTokenSet,\n authMethod: server.authMethod,\n }\n } catch (cause) {\n // Since refresh tokens can only be used once, we might run into\n // concurrency issues if multiple instances (e.g. browser tabs) are\n // trying to refresh the same token simultaneously. The chances of\n // this happening when multiple instances are started simultaneously\n // is reduced by randomizing the expiry time (see isStale() below).\n // The best solution is to use a mutex/lock to ensure that only one\n // instance is refreshing the token at a time (runtime.usingLock) but\n // that is not always possible. Let's try to recover from concurrency\n // issues, or force the session to be deleted by throwing a\n // TokenRefreshError.\n if (\n cause instanceof OAuthResponseError &&\n cause.status === 400 &&\n cause.error === 'invalid_grant'\n ) {\n // In case there is no lock implementation in the runtime, we will\n // wait for a short time to give the other concurrent instances a\n // chance to finish their refreshing of the token. If a concurrent\n // refresh did occur, we will pretend that this one succeeded.\n if (!runtime.hasImplementationLock) {\n await new Promise((r) => setTimeout(r, 1000))\n\n const stored = await this.getStored(sub)\n if (stored === undefined) {\n // A concurrent refresh occurred and caused the session to be\n // deleted (for a reason we can't know at this point).\n\n // Using a distinct error message mainly for debugging\n // purposes. Also, throwing a TokenRefreshError to trigger\n // deletion through the deleteOnError callback.\n const msg = 'The session was deleted by another process'\n throw new TokenRefreshError(sub, msg, { cause })\n } else if (\n stored.tokenSet.access_token !== tokenSet.access_token ||\n stored.tokenSet.refresh_token !== tokenSet.refresh_token\n ) {\n // A concurrent refresh occurred. Pretend this one succeeded.\n return stored\n } else {\n // There were no concurrent refresh. The token is (likely)\n // simply no longer valid.\n }\n }\n\n // Make sure the session gets deleted from the store\n const msg = cause.errorDescription ?? 'The session was revoked'\n throw new TokenRefreshError(sub, msg, { cause })\n }\n\n throw cause\n }\n },\n sessionStore,\n {\n isStale: (sub, { tokenSet }) => {\n return (\n tokenSet.expires_at != null &&\n new Date(tokenSet.expires_at).getTime() <\n Date.now() +\n // Add some lee way to ensure the token is not expired when it\n // reaches the server.\n 10e3 +\n // Add some randomness to reduce the chances of multiple\n // instances trying to refresh the token at the same.\n 30e3 * Math.random()\n )\n },\n onStoreError: async (err, sub, { tokenSet, dpopKey, authMethod }) => {\n // If the token data cannot be stored, let's revoke it\n try {\n const server = await serverFactory.fromIssuer(\n tokenSet.iss,\n authMethod,\n dpopKey,\n )\n await server.revoke(tokenSet.refresh_token ?? tokenSet.access_token)\n } catch {\n // At least we tried...\n }\n\n // Attempt to delete the session from the store. Note that this might\n // fail if the store is not available, which is fine.\n try {\n await this.delStored(sub, err)\n } catch {\n // Ignore (better to propagate the original storage error)\n }\n\n throw err\n },\n deleteOnError: isExpectedSessionError,\n },\n )\n }\n\n override async getStored(\n sub: AtprotoDid,\n options?: GetOptions,\n ): Promise<Session | undefined> {\n return super.getStored(sub, options)\n }\n\n override async setStored(sub: AtprotoDid, session: Session) {\n // Prevent tampering with the stored value\n if (sub !== session.tokenSet.sub) {\n throw new TypeError('Token set does not match the expected sub')\n }\n await super.setStored(sub, session)\n await this.hooks.onUpdate?.call(null, sub, session)\n }\n\n override async delStored(sub: AtprotoDid, cause?: unknown): Promise<void> {\n await super.delStored(sub, cause)\n await this.hooks.onDelete?.call(null, sub, cause)\n }\n\n /**\n * @deprecated Use {@link getSession} instead\n * @internal (not really deprecated)\n */\n override async get(\n sub: AtprotoDid,\n options?: GetCachedOptions,\n ): Promise<Session> {\n const session = await this.runtime.usingLock(\n `@atproto-oauth-client-${sub}`,\n async () => {\n // Make sure, even if there is no signal in the options, that the\n // request will be cancelled after at most 30 seconds.\n const signal = timeoutSignal(30e3)\n\n using abortController = combineSignals([options?.signal, signal])\n\n return await super.get(sub, {\n ...options,\n signal: abortController.signal,\n })\n },\n )\n\n if (sub !== session.tokenSet.sub) {\n // Fool-proofing (e.g. against invalid session storage)\n throw new Error('Token set does not match the expected sub')\n }\n\n return session\n }\n\n /**\n * @param refresh When `true`, the credentials will be refreshed even if they\n * are not expired. When `false`, the credentials will not be refreshed even\n * if they are expired. When `undefined`, the credentials will be refreshed\n * if, and only if, they are (about to be) expired. Defaults to `undefined`.\n */\n async getSession(sub: AtprotoDid, refresh: boolean | 'auto' = 'auto') {\n return this.get(sub, {\n noCache: refresh === true,\n allowStale: refresh === false,\n })\n }\n}\n"]}
package/dist/util.d.ts CHANGED
@@ -4,5 +4,17 @@ export type Simplify<T> = {
4
4
  } & NonNullable<unknown>;
5
5
  export declare const ifString: <V>(v: V) => (V & string) | undefined;
6
6
  export declare function contentMime(headers: Headers): string | undefined;
7
+ /**
8
+ * Returns an {@link AbortSignal} that aborts after `ms` milliseconds.
9
+ *
10
+ * Uses the native {@link AbortSignal.timeout} when available, and otherwise
11
+ * falls back to an {@link AbortController} + `setTimeout`. The static
12
+ * `AbortSignal.timeout` method is not implemented in every runtime this package
13
+ * targets (notably React Native / Expo), so relying on it directly throws a
14
+ * `TypeError: AbortSignal.timeout is not a function` at runtime.
15
+ *
16
+ * @see {@link https://github.com/facebook/react-native/issues/42042}
17
+ */
18
+ export declare function timeoutSignal(ms: number): AbortSignal;
7
19
  export declare function combineSignals(signals: readonly (AbortSignal | undefined)[]): AbortController & Disposable;
8
20
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;AAC7C,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;AAEzE,eAAO,MAAM,QAAQ,GAAI,CAAC,KAAK,CAAC,6BAA4C,CAAA;AAE5E,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEhE;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,SAAS,CAAC,EAAE,GAC5C,eAAe,GAAG,UAAU,CAwB9B"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;AAC7C,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;AAEzE,eAAO,MAAM,QAAQ,GAAI,CAAC,KAAK,CAAC,6BAA4C,CAAA;AAE5E,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,CAQrD;AAgBD,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,SAAS,CAAC,EAAE,GAC5C,eAAe,GAAG,UAAU,CAwB9B"}
package/dist/util.js CHANGED
@@ -2,6 +2,38 @@ export const ifString = (v) => (typeof v === 'string' ? v : undefined);
2
2
  export function contentMime(headers) {
3
3
  return headers.get('content-type')?.split(';')[0].trim();
4
4
  }
5
+ /**
6
+ * Returns an {@link AbortSignal} that aborts after `ms` milliseconds.
7
+ *
8
+ * Uses the native {@link AbortSignal.timeout} when available, and otherwise
9
+ * falls back to an {@link AbortController} + `setTimeout`. The static
10
+ * `AbortSignal.timeout` method is not implemented in every runtime this package
11
+ * targets (notably React Native / Expo), so relying on it directly throws a
12
+ * `TypeError: AbortSignal.timeout is not a function` at runtime.
13
+ *
14
+ * @see {@link https://github.com/facebook/react-native/issues/42042}
15
+ */
16
+ export function timeoutSignal(ms) {
17
+ if (typeof AbortSignal.timeout === 'function') {
18
+ return AbortSignal.timeout(ms);
19
+ }
20
+ const controller = new AbortController();
21
+ setTimeout(() => controller.abort(timeoutError(ms)), ms);
22
+ return controller.signal;
23
+ }
24
+ /**
25
+ * Builds the reason used to abort a {@link timeoutSignal} fallback. Mirrors the
26
+ * native `AbortSignal.timeout` behaviour (a `TimeoutError` `DOMException`) when
27
+ * `DOMException` is available, and degrades to a plain `Error` in runtimes that
28
+ * lack it.
29
+ */
30
+ function timeoutError(ms) {
31
+ const message = `The operation timed out after ${ms} ms`;
32
+ if (typeof DOMException === 'function') {
33
+ return new DOMException(message, 'TimeoutError');
34
+ }
35
+ return new Error(message);
36
+ }
5
37
  export function combineSignals(signals) {
6
38
  const controller = new DisposableAbortController();
7
39
  const onAbort = function (_event) {
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAI,CAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAE5E,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAA;AAC3D,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,OAA6C;IAE7C,MAAM,UAAU,GAAG,IAAI,yBAAyB,EAAE,CAAA;IAElD,MAAM,OAAO,GAAG,UAA6B,MAAa;QACxD,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,4BAA4B,EAAE;YACrD,KAAK,EAAE,IAAI,CAAC,MAAM;SACnB,CAAC,CAAA;QAEF,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAC1B,CAAC,CAAA;IAED,IAAI,CAAC;QACH,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,cAAc,EAAE,CAAA;gBACpB,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAA;YACvE,CAAC;QACH,CAAC;QAED,OAAO,UAAU,CAAA;IACnB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACrB,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,yBAA0B,SAAQ,eAAe;IACrD,CAAC,MAAM,CAAC,OAAO,CAAC;QACd,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC,CAAA;IACvD,CAAC;CACF","sourcesContent":["export type Awaitable<T> = T | PromiseLike<T>\nexport type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\n\nexport const ifString = <V>(v: V) => (typeof v === 'string' ? v : undefined)\n\nexport function contentMime(headers: Headers): string | undefined {\n return headers.get('content-type')?.split(';')[0]!.trim()\n}\n\nexport function combineSignals(\n signals: readonly (AbortSignal | undefined)[],\n): AbortController & Disposable {\n const controller = new DisposableAbortController()\n\n const onAbort = function (this: AbortSignal, _event: Event) {\n const reason = new Error('This operation was aborted', {\n cause: this.reason,\n })\n\n controller.abort(reason)\n }\n\n try {\n for (const sig of signals) {\n if (sig) {\n sig.throwIfAborted()\n sig.addEventListener('abort', onAbort, { signal: controller.signal })\n }\n }\n\n return controller\n } catch (err) {\n controller.abort(err)\n throw err\n }\n}\n\n/**\n * Allows using {@link AbortController} with the `using` keyword, in order to\n * automatically abort them once the execution block ends.\n */\nclass DisposableAbortController extends AbortController implements Disposable {\n [Symbol.dispose]() {\n this.abort(new Error('AbortController was disposed'))\n }\n}\n"]}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAI,CAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAE5E,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAA;AAC3D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,EAAU;IACtC,IAAI,OAAO,WAAW,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QAC9C,OAAO,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAChC,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACxD,OAAO,UAAU,CAAC,MAAM,CAAA;AAC1B,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,EAAU;IAC9B,MAAM,OAAO,GAAG,iCAAiC,EAAE,KAAK,CAAA;IACxD,IAAI,OAAO,YAAY,KAAK,UAAU,EAAE,CAAC;QACvC,OAAO,IAAI,YAAY,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;IAClD,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,CAAA;AAC3B,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,OAA6C;IAE7C,MAAM,UAAU,GAAG,IAAI,yBAAyB,EAAE,CAAA;IAElD,MAAM,OAAO,GAAG,UAA6B,MAAa;QACxD,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,4BAA4B,EAAE;YACrD,KAAK,EAAE,IAAI,CAAC,MAAM;SACnB,CAAC,CAAA;QAEF,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAC1B,CAAC,CAAA;IAED,IAAI,CAAC;QACH,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,cAAc,EAAE,CAAA;gBACpB,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAA;YACvE,CAAC;QACH,CAAC;QAED,OAAO,UAAU,CAAA;IACnB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACrB,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,yBAA0B,SAAQ,eAAe;IACrD,CAAC,MAAM,CAAC,OAAO,CAAC;QACd,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC,CAAA;IACvD,CAAC;CACF","sourcesContent":["export type Awaitable<T> = T | PromiseLike<T>\nexport type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\n\nexport const ifString = <V>(v: V) => (typeof v === 'string' ? v : undefined)\n\nexport function contentMime(headers: Headers): string | undefined {\n return headers.get('content-type')?.split(';')[0]!.trim()\n}\n\n/**\n * Returns an {@link AbortSignal} that aborts after `ms` milliseconds.\n *\n * Uses the native {@link AbortSignal.timeout} when available, and otherwise\n * falls back to an {@link AbortController} + `setTimeout`. The static\n * `AbortSignal.timeout` method is not implemented in every runtime this package\n * targets (notably React Native / Expo), so relying on it directly throws a\n * `TypeError: AbortSignal.timeout is not a function` at runtime.\n *\n * @see {@link https://github.com/facebook/react-native/issues/42042}\n */\nexport function timeoutSignal(ms: number): AbortSignal {\n if (typeof AbortSignal.timeout === 'function') {\n return AbortSignal.timeout(ms)\n }\n\n const controller = new AbortController()\n setTimeout(() => controller.abort(timeoutError(ms)), ms)\n return controller.signal\n}\n\n/**\n * Builds the reason used to abort a {@link timeoutSignal} fallback. Mirrors the\n * native `AbortSignal.timeout` behaviour (a `TimeoutError` `DOMException`) when\n * `DOMException` is available, and degrades to a plain `Error` in runtimes that\n * lack it.\n */\nfunction timeoutError(ms: number): unknown {\n const message = `The operation timed out after ${ms} ms`\n if (typeof DOMException === 'function') {\n return new DOMException(message, 'TimeoutError')\n }\n return new Error(message)\n}\n\nexport function combineSignals(\n signals: readonly (AbortSignal | undefined)[],\n): AbortController & Disposable {\n const controller = new DisposableAbortController()\n\n const onAbort = function (this: AbortSignal, _event: Event) {\n const reason = new Error('This operation was aborted', {\n cause: this.reason,\n })\n\n controller.abort(reason)\n }\n\n try {\n for (const sig of signals) {\n if (sig) {\n sig.throwIfAborted()\n sig.addEventListener('abort', onAbort, { signal: controller.signal })\n }\n }\n\n return controller\n } catch (err) {\n controller.abort(err)\n throw err\n }\n}\n\n/**\n * Allows using {@link AbortController} with the `using` keyword, in order to\n * automatically abort them once the execution block ends.\n */\nclass DisposableAbortController extends AbortController implements Disposable {\n [Symbol.dispose]() {\n this.abort(new Error('AbortController was disposed'))\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/oauth-client",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -29,19 +29,22 @@
29
29
  "core-js": "^3",
30
30
  "multiformats": "^13.0.0",
31
31
  "zod": "^3.23.8",
32
- "@atproto-labs/did-resolver": "^0.3.2",
33
- "@atproto-labs/handle-resolver": "^0.4.2",
34
32
  "@atproto-labs/fetch": "^0.3.1",
35
- "@atproto-labs/identity-resolver": "^0.4.1",
33
+ "@atproto-labs/did-resolver": "^0.3.2",
36
34
  "@atproto-labs/simple-store": "^0.4.1",
37
35
  "@atproto-labs/simple-store-memory": "^0.2.1",
36
+ "@atproto-labs/handle-resolver": "^0.4.2",
38
37
  "@atproto/did": "^0.5.1",
38
+ "@atproto-labs/identity-resolver": "^0.4.1",
39
39
  "@atproto/jwk": "^0.7.1",
40
40
  "@atproto/oauth-types": "^0.7.2",
41
41
  "@atproto/xrpc": "^0.8.1"
42
42
  },
43
- "devDependencies": {},
43
+ "devDependencies": {
44
+ "vitest": "^4.0.16"
45
+ },
44
46
  "scripts": {
45
- "build": "tsgo --build tsconfig.build.json"
47
+ "build": "tsgo --build tsconfig.build.json",
48
+ "test": "vitest run"
46
49
  }
47
50
  }
@@ -25,6 +25,7 @@ import { OAuthResolver } from './oauth-resolver.js'
25
25
  import { OAuthResponseError } from './oauth-response-error.js'
26
26
  import { Runtime } from './runtime.js'
27
27
  import { ClientMetadata } from './types.js'
28
+ import { timeoutSignal } from './util.js'
28
29
 
29
30
  export type { AtprotoOAuthScope, AtprotoOAuthTokenResponse }
30
31
 
@@ -189,7 +190,7 @@ export class OAuthServerAgent {
189
190
  const resolved = await this.oauthResolver.resolveFromIdentity(sub, {
190
191
  noCache: true,
191
192
  allowStale: false,
192
- signal: AbortSignal.timeout(10e3),
193
+ signal: timeoutSignal(10e3),
193
194
  })
194
195
 
195
196
  if (this.issuer !== resolved.metadata.issuer) {
@@ -15,7 +15,7 @@ import { OAuthResponseError } from './oauth-response-error.js'
15
15
  import { TokenSet } from './oauth-server-agent.js'
16
16
  import { OAuthServerFactory } from './oauth-server-factory.js'
17
17
  import { Runtime } from './runtime.js'
18
- import { combineSignals } from './util.js'
18
+ import { combineSignals, timeoutSignal } from './util.js'
19
19
 
20
20
  export type Session = {
21
21
  dpopKey: Key
@@ -247,7 +247,7 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
247
247
  async () => {
248
248
  // Make sure, even if there is no signal in the options, that the
249
249
  // request will be cancelled after at most 30 seconds.
250
- const signal = AbortSignal.timeout(30e3)
250
+ const signal = timeoutSignal(30e3)
251
251
 
252
252
  using abortController = combineSignals([options?.signal, signal])
253
253
 
@@ -0,0 +1,86 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { timeoutSignal } from './util.js'
3
+
4
+ describe(timeoutSignal, () => {
5
+ describe('with native AbortSignal.timeout', () => {
6
+ it('delegates to the native implementation when available', () => {
7
+ using spy = vi.spyOn(AbortSignal, 'timeout')
8
+
9
+ const signal = timeoutSignal(1000)
10
+
11
+ expect(spy).toHaveBeenCalledOnce()
12
+ expect(spy).toHaveBeenCalledWith(1000)
13
+ expect(signal).toBeInstanceOf(AbortSignal)
14
+ expect(signal.aborted).toBe(false)
15
+ })
16
+
17
+ it('returns a signal that actually aborts after the timeout', async () => {
18
+ const signal = timeoutSignal(5)
19
+ expect(signal.aborted).toBe(false)
20
+
21
+ await new Promise((resolve) => setTimeout(resolve, 20))
22
+ expect(signal.aborted).toBe(true)
23
+ })
24
+ })
25
+
26
+ describe('without native AbortSignal.timeout (e.g. React Native)', () => {
27
+ let original: typeof AbortSignal.timeout
28
+
29
+ beforeEach(() => {
30
+ vi.useFakeTimers()
31
+ original = AbortSignal.timeout
32
+ // Simulate a runtime that does not implement the static method.
33
+ // @ts-expect-error intentionally removing a built-in to emulate RN
34
+ AbortSignal.timeout = undefined
35
+ })
36
+
37
+ afterEach(() => {
38
+ AbortSignal.timeout = original
39
+ vi.useRealTimers()
40
+ })
41
+
42
+ it('does not throw and returns a usable AbortSignal', () => {
43
+ const signal = timeoutSignal(1000)
44
+ expect(signal).toBeInstanceOf(AbortSignal)
45
+ expect(signal.aborted).toBe(false)
46
+ })
47
+
48
+ it('aborts the signal once the timeout elapses', () => {
49
+ const signal = timeoutSignal(1000)
50
+ const onAbort = vi.fn()
51
+ signal.addEventListener('abort', onAbort)
52
+
53
+ vi.advanceTimersByTime(999)
54
+ expect(signal.aborted).toBe(false)
55
+ expect(onAbort).not.toHaveBeenCalled()
56
+
57
+ vi.advanceTimersByTime(1)
58
+ expect(signal.aborted).toBe(true)
59
+ expect(onAbort).toHaveBeenCalledOnce()
60
+ })
61
+
62
+ it('aborts with a TimeoutError reason', () => {
63
+ const signal = timeoutSignal(1000)
64
+ vi.advanceTimersByTime(1000)
65
+
66
+ expect(signal.reason).toBeInstanceOf(DOMException)
67
+ expect(signal.reason.name).toBe('TimeoutError')
68
+ })
69
+
70
+ it('falls back to a plain Error when DOMException is unavailable', () => {
71
+ const originalDomException = globalThis.DOMException
72
+ try {
73
+ // @ts-expect-error intentionally removing a built-in to emulate RN
74
+ globalThis.DOMException = undefined
75
+
76
+ const signal = timeoutSignal(1000)
77
+ vi.advanceTimersByTime(1000)
78
+
79
+ expect(signal.aborted).toBe(true)
80
+ expect(signal.reason).toBeInstanceOf(Error)
81
+ } finally {
82
+ globalThis.DOMException = originalDomException
83
+ }
84
+ })
85
+ })
86
+ })
package/src/util.ts CHANGED
@@ -7,6 +7,41 @@ export function contentMime(headers: Headers): string | undefined {
7
7
  return headers.get('content-type')?.split(';')[0]!.trim()
8
8
  }
9
9
 
10
+ /**
11
+ * Returns an {@link AbortSignal} that aborts after `ms` milliseconds.
12
+ *
13
+ * Uses the native {@link AbortSignal.timeout} when available, and otherwise
14
+ * falls back to an {@link AbortController} + `setTimeout`. The static
15
+ * `AbortSignal.timeout` method is not implemented in every runtime this package
16
+ * targets (notably React Native / Expo), so relying on it directly throws a
17
+ * `TypeError: AbortSignal.timeout is not a function` at runtime.
18
+ *
19
+ * @see {@link https://github.com/facebook/react-native/issues/42042}
20
+ */
21
+ export function timeoutSignal(ms: number): AbortSignal {
22
+ if (typeof AbortSignal.timeout === 'function') {
23
+ return AbortSignal.timeout(ms)
24
+ }
25
+
26
+ const controller = new AbortController()
27
+ setTimeout(() => controller.abort(timeoutError(ms)), ms)
28
+ return controller.signal
29
+ }
30
+
31
+ /**
32
+ * Builds the reason used to abort a {@link timeoutSignal} fallback. Mirrors the
33
+ * native `AbortSignal.timeout` behaviour (a `TimeoutError` `DOMException`) when
34
+ * `DOMException` is available, and degrades to a plain `Error` in runtimes that
35
+ * lack it.
36
+ */
37
+ function timeoutError(ms: number): unknown {
38
+ const message = `The operation timed out after ${ms} ms`
39
+ if (typeof DOMException === 'function') {
40
+ return new DOMException(message, 'TimeoutError')
41
+ }
42
+ return new Error(message)
43
+ }
44
+
10
45
  export function combineSignals(
11
46
  signals: readonly (AbortSignal | undefined)[],
12
47
  ): AbortController & Disposable {
@@ -5,4 +5,5 @@
5
5
  "outDir": "./dist",
6
6
  },
7
7
  "include": ["./src"],
8
+ "exclude": ["**/*.test.ts"],
8
9
  }
package/tsconfig.json CHANGED
@@ -1,4 +1,7 @@
1
1
  {
2
2
  "include": [],
3
- "references": [{ "path": "./tsconfig.build.json" }],
3
+ "references": [
4
+ { "path": "./tsconfig.build.json" },
5
+ { "path": "./tsconfig.tests.json" },
6
+ ],
4
7
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig/vitest.json",
3
+ "include": ["./tests", "./src/**/*.test.ts", "./src/core-js.d.ts"],
4
+ "compilerOptions": {
5
+ "rootDir": ".",
6
+ "noUnusedLocals": false,
7
+ },
8
+ }
@@ -0,0 +1,5 @@
1
+ import { defineProject } from 'vitest/config'
2
+
3
+ export default defineProject({
4
+ test: {},
5
+ })