@atproto/oauth-client 0.0.0-spaces-alpha-20260818021221

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.
Files changed (112) hide show
  1. package/CHANGELOG.md +810 -0
  2. package/LICENSE.txt +7 -0
  3. package/README.md +387 -0
  4. package/dist/constants.d.ts +5 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +5 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/errors/auth-method-unsatisfiable-error.d.ts +3 -0
  9. package/dist/errors/auth-method-unsatisfiable-error.d.ts.map +1 -0
  10. package/dist/errors/auth-method-unsatisfiable-error.js +3 -0
  11. package/dist/errors/auth-method-unsatisfiable-error.js.map +1 -0
  12. package/dist/errors/token-invalid-error.d.ts +7 -0
  13. package/dist/errors/token-invalid-error.d.ts.map +1 -0
  14. package/dist/errors/token-invalid-error.js +7 -0
  15. package/dist/errors/token-invalid-error.js.map +1 -0
  16. package/dist/errors/token-refresh-error.d.ts +7 -0
  17. package/dist/errors/token-refresh-error.d.ts.map +1 -0
  18. package/dist/errors/token-refresh-error.js +7 -0
  19. package/dist/errors/token-refresh-error.js.map +1 -0
  20. package/dist/errors/token-revoked-error.d.ts +7 -0
  21. package/dist/errors/token-revoked-error.d.ts.map +1 -0
  22. package/dist/errors/token-revoked-error.js +7 -0
  23. package/dist/errors/token-revoked-error.js.map +1 -0
  24. package/dist/fetch-dpop.d.ts +20 -0
  25. package/dist/fetch-dpop.d.ts.map +1 -0
  26. package/dist/fetch-dpop.js +176 -0
  27. package/dist/fetch-dpop.js.map +1 -0
  28. package/dist/identity-resolver.d.ts +2 -0
  29. package/dist/identity-resolver.d.ts.map +1 -0
  30. package/dist/identity-resolver.js +21 -0
  31. package/dist/identity-resolver.js.map +1 -0
  32. package/dist/index.d.ts +25 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +25 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/lock.d.ts +3 -0
  37. package/dist/lock.d.ts.map +1 -0
  38. package/dist/lock.js +29 -0
  39. package/dist/lock.js.map +1 -0
  40. package/dist/oauth-authorization-server-metadata-resolver.d.ts +25 -0
  41. package/dist/oauth-authorization-server-metadata-resolver.d.ts.map +1 -0
  42. package/dist/oauth-authorization-server-metadata-resolver.js +54 -0
  43. package/dist/oauth-authorization-server-metadata-resolver.js.map +1 -0
  44. package/dist/oauth-callback-error.d.ts +7 -0
  45. package/dist/oauth-callback-error.d.ts.map +1 -0
  46. package/dist/oauth-callback-error.js +14 -0
  47. package/dist/oauth-callback-error.js.map +1 -0
  48. package/dist/oauth-client-auth.d.ts +23 -0
  49. package/dist/oauth-client-auth.d.ts.map +1 -0
  50. package/dist/oauth-client-auth.js +126 -0
  51. package/dist/oauth-client-auth.js.map +1 -0
  52. package/dist/oauth-client.d.ts +338 -0
  53. package/dist/oauth-client.d.ts.map +1 -0
  54. package/dist/oauth-client.js +274 -0
  55. package/dist/oauth-client.js.map +1 -0
  56. package/dist/oauth-protected-resource-metadata-resolver.d.ts +25 -0
  57. package/dist/oauth-protected-resource-metadata-resolver.d.ts.map +1 -0
  58. package/dist/oauth-protected-resource-metadata-resolver.js +54 -0
  59. package/dist/oauth-protected-resource-metadata-resolver.js.map +1 -0
  60. package/dist/oauth-resolver-error.d.ts +7 -0
  61. package/dist/oauth-resolver-error.d.ts.map +1 -0
  62. package/dist/oauth-resolver-error.js +19 -0
  63. package/dist/oauth-resolver-error.js.map +1 -0
  64. package/dist/oauth-resolver.d.ts +73 -0
  65. package/dist/oauth-resolver.d.ts.map +1 -0
  66. package/dist/oauth-resolver.js +99 -0
  67. package/dist/oauth-resolver.js.map +1 -0
  68. package/dist/oauth-response-error.d.ts +11 -0
  69. package/dist/oauth-response-error.d.ts.map +1 -0
  70. package/dist/oauth-response-error.js +22 -0
  71. package/dist/oauth-response-error.js.map +1 -0
  72. package/dist/oauth-server-agent.d.ts +55 -0
  73. package/dist/oauth-server-agent.d.ts.map +1 -0
  74. package/dist/oauth-server-agent.js +189 -0
  75. package/dist/oauth-server-agent.js.map +1 -0
  76. package/dist/oauth-server-factory.d.ts +33 -0
  77. package/dist/oauth-server-factory.d.ts.map +1 -0
  78. package/dist/oauth-server-factory.js +31 -0
  79. package/dist/oauth-server-factory.js.map +1 -0
  80. package/dist/oauth-session.d.ts +34 -0
  81. package/dist/oauth-session.d.ts.map +1 -0
  82. package/dist/oauth-session.js +120 -0
  83. package/dist/oauth-session.js.map +1 -0
  84. package/dist/runtime-implementation.d.ts +17 -0
  85. package/dist/runtime-implementation.d.ts.map +1 -0
  86. package/dist/runtime-implementation.js +2 -0
  87. package/dist/runtime-implementation.js.map +1 -0
  88. package/dist/runtime.d.ts +26 -0
  89. package/dist/runtime.d.ts.map +1 -0
  90. package/dist/runtime.js +100 -0
  91. package/dist/runtime.js.map +1 -0
  92. package/dist/session-getter.d.ts +44 -0
  93. package/dist/session-getter.d.ts.map +1 -0
  94. package/dist/session-getter.js +265 -0
  95. package/dist/session-getter.js.map +1 -0
  96. package/dist/state-store.d.ts +23 -0
  97. package/dist/state-store.d.ts.map +1 -0
  98. package/dist/state-store.js +2 -0
  99. package/dist/state-store.js.map +1 -0
  100. package/dist/types.d.ts +430 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/types.js +9 -0
  103. package/dist/types.js.map +1 -0
  104. package/dist/util.d.ts +20 -0
  105. package/dist/util.d.ts.map +1 -0
  106. package/dist/util.js +68 -0
  107. package/dist/util.js.map +1 -0
  108. package/dist/validate-client-metadata.d.ts +5 -0
  109. package/dist/validate-client-metadata.d.ts.map +1 -0
  110. package/dist/validate-client-metadata.js +76 -0
  111. package/dist/validate-client-metadata.js.map +1 -0
  112. package/package.json +55 -0
@@ -0,0 +1,120 @@
1
+ import { bindFetch } from '@atproto-labs/fetch';
2
+ import { TokenInvalidError } from './errors/token-invalid-error.js';
3
+ import { TokenRevokedError } from './errors/token-revoked-error.js';
4
+ import { dpopFetchWrapper } from './fetch-dpop.js';
5
+ const ReadableStream = globalThis.ReadableStream;
6
+ export class OAuthSession {
7
+ constructor(server, sub, sessionGetter, fetch = globalThis.fetch) {
8
+ this.server = server;
9
+ this.sub = sub;
10
+ this.sessionGetter = sessionGetter;
11
+ this.dpopFetch = dpopFetchWrapper({
12
+ fetch: bindFetch(fetch),
13
+ key: server.dpopKey,
14
+ supportedAlgs: server.serverMetadata.dpop_signing_alg_values_supported,
15
+ sha256: async (v) => server.runtime.sha256(v),
16
+ nonces: server.dpopNonces,
17
+ isAuthServer: false,
18
+ });
19
+ }
20
+ get did() {
21
+ return this.sub;
22
+ }
23
+ get serverMetadata() {
24
+ return this.server.serverMetadata;
25
+ }
26
+ /**
27
+ * @param refresh When `true`, the credentials will be refreshed even if they
28
+ * are not expired. When `false`, the credentials will not be refreshed even
29
+ * if they are expired. When `undefined`, the credentials will be refreshed
30
+ * if, and only if, they are (about to be) expired. Defaults to `undefined`.
31
+ */
32
+ async getTokenSet(refresh) {
33
+ const { tokenSet } = await this.sessionGetter.getSession(this.sub, refresh);
34
+ return tokenSet;
35
+ }
36
+ async getTokenInfo(refresh = 'auto') {
37
+ const tokenSet = await this.getTokenSet(refresh);
38
+ const expiresAt = tokenSet.expires_at == null ? undefined : new Date(tokenSet.expires_at);
39
+ return {
40
+ expiresAt,
41
+ get expired() {
42
+ return expiresAt == null
43
+ ? undefined
44
+ : expiresAt.getTime() < Date.now() - 5e3;
45
+ },
46
+ scope: tokenSet.scope,
47
+ iss: tokenSet.iss,
48
+ aud: tokenSet.aud,
49
+ sub: tokenSet.sub,
50
+ };
51
+ }
52
+ async signOut() {
53
+ try {
54
+ const tokenSet = await this.getTokenSet(false);
55
+ await this.server.revoke(tokenSet.access_token);
56
+ }
57
+ finally {
58
+ await this.sessionGetter.delStored(this.sub, new TokenRevokedError(this.sub));
59
+ }
60
+ }
61
+ async fetchHandler(pathname, init) {
62
+ // This will try and refresh the token if it is known to be expired
63
+ const tokenSet = await this.getTokenSet('auto');
64
+ const initialUrl = new URL(pathname, tokenSet.aud);
65
+ const initialAuth = `${tokenSet.token_type} ${tokenSet.access_token}`;
66
+ const headers = new Headers(init?.headers);
67
+ headers.set('Authorization', initialAuth);
68
+ const initialResponse = await this.dpopFetch(initialUrl, {
69
+ ...init,
70
+ headers,
71
+ });
72
+ // If the token is not expired, we don't need to refresh it
73
+ if (!isInvalidTokenResponse(initialResponse)) {
74
+ return initialResponse;
75
+ }
76
+ let tokenSetFresh;
77
+ try {
78
+ // Force a refresh
79
+ tokenSetFresh = await this.getTokenSet(true);
80
+ }
81
+ catch (err) {
82
+ return initialResponse;
83
+ }
84
+ // The stream was already consumed. We cannot retry the request. A solution
85
+ // would be to tee() the input stream but that would bufferize the entire
86
+ // stream in memory which can lead to memory starvation. Instead, we will
87
+ // return the original response and let the calling code handle retries.
88
+ if (ReadableStream && init?.body instanceof ReadableStream) {
89
+ return initialResponse;
90
+ }
91
+ const finalAuth = `${tokenSetFresh.token_type} ${tokenSetFresh.access_token}`;
92
+ const finalUrl = new URL(pathname, tokenSetFresh.aud);
93
+ headers.set('Authorization', finalAuth);
94
+ const finalResponse = await this.dpopFetch(finalUrl, { ...init, headers });
95
+ // The token was successfully refreshed, but is still not accepted by the
96
+ // resource server. This might be due to the resource server not accepting
97
+ // credentials from the authorization server (e.g. because some migration
98
+ // occurred). Any ways, there is no point in keeping the session.
99
+ if (isInvalidTokenResponse(finalResponse)) {
100
+ // @TODO Is there a "softer" way to handle this, e.g. by marking the
101
+ // session as "expired" in the session store, allowing the user to trigger
102
+ // a new login (using login_hint)?
103
+ await this.sessionGetter.delStored(this.sub, new TokenInvalidError(this.sub));
104
+ }
105
+ return finalResponse;
106
+ }
107
+ }
108
+ /**
109
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3}
110
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no}
111
+ */
112
+ function isInvalidTokenResponse(response) {
113
+ if (response.status !== 401)
114
+ return false;
115
+ const wwwAuth = response.headers.get('WWW-Authenticate');
116
+ return (wwwAuth != null &&
117
+ (wwwAuth.startsWith('Bearer ') || wwwAuth.startsWith('DPoP ')) &&
118
+ wwwAuth.includes('error="invalid_token"'));
119
+ }
120
+ //# sourceMappingURL=oauth-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-session.js","sourceRoot":"","sources":["../src/oauth-session.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AtprotoDid } from '@atproto/did'\nimport type {\n AtprotoOAuthScope,\n OAuthAuthorizationServerMetadata,\n} from '@atproto/oauth-types'\nimport { type Fetch, bindFetch } from '@atproto-labs/fetch'\nimport { TokenInvalidError } from './errors/token-invalid-error.js'\nimport { TokenRevokedError } from './errors/token-revoked-error.js'\nimport { dpopFetchWrapper } from './fetch-dpop.js'\nimport type { OAuthServerAgent, TokenSet } from './oauth-server-agent.js'\nimport type { SessionGetter } from './session-getter.js'\n\nconst ReadableStream = globalThis.ReadableStream as\n typeof globalThis.ReadableStream | undefined\n\nexport type { AtprotoDid, AtprotoOAuthScope }\nexport type TokenInfo = {\n expiresAt?: Date\n expired?: boolean\n scope: AtprotoOAuthScope\n iss: string\n aud: string\n sub: AtprotoDid\n}\n\nexport class OAuthSession {\n protected dpopFetch: Fetch<unknown>\n\n constructor(\n public readonly server: OAuthServerAgent,\n public readonly sub: AtprotoDid,\n private readonly sessionGetter: SessionGetter,\n fetch: Fetch = globalThis.fetch,\n ) {\n this.dpopFetch = dpopFetchWrapper<void>({\n fetch: bindFetch(fetch),\n key: server.dpopKey,\n supportedAlgs: server.serverMetadata.dpop_signing_alg_values_supported,\n sha256: async (v) => server.runtime.sha256(v),\n nonces: server.dpopNonces,\n isAuthServer: false,\n })\n }\n\n get did(): AtprotoDid {\n return this.sub\n }\n\n get serverMetadata(): Readonly<OAuthAuthorizationServerMetadata> {\n return this.server.serverMetadata\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 protected async getTokenSet(refresh: boolean | 'auto'): Promise<TokenSet> {\n const { tokenSet } = await this.sessionGetter.getSession(this.sub, refresh)\n\n return tokenSet\n }\n\n async getTokenInfo(refresh: boolean | 'auto' = 'auto'): Promise<TokenInfo> {\n const tokenSet = await this.getTokenSet(refresh)\n const expiresAt =\n tokenSet.expires_at == null ? undefined : new Date(tokenSet.expires_at)\n\n return {\n expiresAt,\n get expired() {\n return expiresAt == null\n ? undefined\n : expiresAt.getTime() < Date.now() - 5e3\n },\n scope: tokenSet.scope,\n iss: tokenSet.iss,\n aud: tokenSet.aud,\n sub: tokenSet.sub,\n }\n }\n\n async signOut(): Promise<void> {\n try {\n const tokenSet = await this.getTokenSet(false)\n await this.server.revoke(tokenSet.access_token)\n } finally {\n await this.sessionGetter.delStored(\n this.sub,\n new TokenRevokedError(this.sub),\n )\n }\n }\n\n async fetchHandler(pathname: string, init?: RequestInit): Promise<Response> {\n // This will try and refresh the token if it is known to be expired\n const tokenSet = await this.getTokenSet('auto')\n\n const initialUrl = new URL(pathname, tokenSet.aud satisfies string)\n const initialAuth = `${tokenSet.token_type} ${tokenSet.access_token}`\n\n const headers = new Headers(init?.headers)\n headers.set('Authorization', initialAuth)\n\n const initialResponse = await this.dpopFetch(initialUrl, {\n ...init,\n headers,\n })\n\n // If the token is not expired, we don't need to refresh it\n if (!isInvalidTokenResponse(initialResponse)) {\n return initialResponse\n }\n\n let tokenSetFresh: TokenSet\n try {\n // Force a refresh\n tokenSetFresh = await this.getTokenSet(true)\n } catch (err) {\n return initialResponse\n }\n\n // The stream was already consumed. We cannot retry the request. A solution\n // would be to tee() the input stream but that would bufferize the entire\n // stream in memory which can lead to memory starvation. Instead, we will\n // return the original response and let the calling code handle retries.\n if (ReadableStream && init?.body instanceof ReadableStream) {\n return initialResponse\n }\n\n const finalAuth = `${tokenSetFresh.token_type} ${tokenSetFresh.access_token}`\n const finalUrl = new URL(pathname, tokenSetFresh.aud)\n\n headers.set('Authorization', finalAuth)\n\n const finalResponse = await this.dpopFetch(finalUrl, { ...init, headers })\n\n // The token was successfully refreshed, but is still not accepted by the\n // resource server. This might be due to the resource server not accepting\n // credentials from the authorization server (e.g. because some migration\n // occurred). Any ways, there is no point in keeping the session.\n if (isInvalidTokenResponse(finalResponse)) {\n // @TODO Is there a \"softer\" way to handle this, e.g. by marking the\n // session as \"expired\" in the session store, allowing the user to trigger\n // a new login (using login_hint)?\n await this.sessionGetter.delStored(\n this.sub,\n new TokenInvalidError(this.sub),\n )\n }\n\n return finalResponse\n }\n}\n\n/**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3}\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no}\n */\nfunction isInvalidTokenResponse(response: Response) {\n if (response.status !== 401) return false\n const wwwAuth = response.headers.get('WWW-Authenticate')\n return (\n wwwAuth != null &&\n (wwwAuth.startsWith('Bearer ') || wwwAuth.startsWith('DPoP ')) &&\n wwwAuth.includes('error=\"invalid_token\"')\n )\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import type { Key } from '@atproto/jwk';
2
+ import type { Awaitable } from './util.js';
3
+ export type { Key };
4
+ export type RuntimeKeyFactory = (algs: string[]) => Key | PromiseLike<Key>;
5
+ export type RuntimeRandomValues = (length: number) => Awaitable<Uint8Array>;
6
+ export type DigestAlgorithm = {
7
+ name: 'sha256' | 'sha384' | 'sha512';
8
+ };
9
+ export type RuntimeDigest = (data: Uint8Array<ArrayBuffer>, alg: DigestAlgorithm) => Awaitable<Uint8Array>;
10
+ export type RuntimeLock = <T>(name: string, fn: () => Awaitable<T>) => Awaitable<T>;
11
+ export interface RuntimeImplementation {
12
+ createKey: RuntimeKeyFactory;
13
+ getRandomValues: RuntimeRandomValues;
14
+ digest: RuntimeDigest;
15
+ requestLock?: RuntimeLock;
16
+ }
17
+ //# sourceMappingURL=runtime-implementation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-implementation.d.ts","sourceRoot":"","sources":["../src/runtime-implementation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,YAAY,EAAE,GAAG,EAAE,CAAA;AACnB,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;AAE1E,MAAM,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,CAAC,UAAU,CAAC,CAAA;AAE3E,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;CAAE,CAAA;AACtE,MAAM,MAAM,aAAa,GAAG,CAC1B,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,EAC7B,GAAG,EAAE,eAAe,KACjB,SAAS,CAAC,UAAU,CAAC,CAAA;AAE1B,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,EAC1B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,KACnB,SAAS,CAAC,CAAC,CAAC,CAAA;AAEjB,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,iBAAiB,CAAA;IAC5B,eAAe,EAAE,mBAAmB,CAAA;IACpC,MAAM,EAAE,aAAa,CAAA;IACrB,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime-implementation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-implementation.js","sourceRoot":"","sources":["../src/runtime-implementation.ts"],"names":[],"mappings":"","sourcesContent":["import type { Key } from '@atproto/jwk'\nimport type { Awaitable } from './util.js'\n\nexport type { Key }\nexport type RuntimeKeyFactory = (algs: string[]) => Key | PromiseLike<Key>\n\nexport type RuntimeRandomValues = (length: number) => Awaitable<Uint8Array>\n\nexport type DigestAlgorithm = { name: 'sha256' | 'sha384' | 'sha512' }\nexport type RuntimeDigest = (\n data: Uint8Array<ArrayBuffer>,\n alg: DigestAlgorithm,\n) => Awaitable<Uint8Array>\n\nexport type RuntimeLock = <T>(\n name: string,\n fn: () => Awaitable<T>,\n) => Awaitable<T>\n\nexport interface RuntimeImplementation {\n createKey: RuntimeKeyFactory\n getRandomValues: RuntimeRandomValues\n digest: RuntimeDigest\n requestLock?: RuntimeLock\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import type { Jwk, Key } from '@atproto/jwk';
2
+ import type { RuntimeImplementation, RuntimeLock } from './runtime-implementation.js';
3
+ export declare class Runtime {
4
+ protected implementation: RuntimeImplementation;
5
+ readonly hasImplementationLock: boolean;
6
+ readonly usingLock: RuntimeLock;
7
+ constructor(implementation: RuntimeImplementation);
8
+ generateKey(algs: string[]): Promise<Key>;
9
+ sha256(text: string): Promise<string>;
10
+ generateNonce(length?: number): Promise<string>;
11
+ generatePKCE(byteLength?: number): Promise<{
12
+ verifier: string;
13
+ challenge: string;
14
+ method: 'S256';
15
+ }>;
16
+ calculateJwkThumbprint(jwk: Jwk): Promise<string>;
17
+ /**
18
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1}
19
+ * @note It is RECOMMENDED that the output of a suitable random number generator
20
+ * be used to create a 32-octet sequence. The octet sequence is then
21
+ * base64url-encoded to produce a 43-octet URL safe string to use as the code
22
+ * verifier.
23
+ */
24
+ protected generateVerifier(byteLength?: number): Promise<string>;
25
+ }
26
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,KAAK,EACV,qBAAqB,EACrB,WAAW,EACZ,MAAM,6BAA6B,CAAA;AAEpC,qBAAa,OAAO;IAIN,SAAS,CAAC,cAAc,EAAE,qBAAqB;IAH3D,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAA;IACvC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAA;IAE/B,YAAsB,cAAc,EAAE,qBAAqB,EAQ1D;IAEY,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAGrD;IAEY,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIjD;IAEY,aAAa,CAAC,MAAM,SAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAGvD;IAEY,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM;;;gBAKjC,MAAM;OAEjB;IAEY,sBAAsB,CAAC,GAAG,EAAE,GAAG,mBAI3C;IAED;;;;;;OAMG;IACH,UAAgB,gBAAgB,CAAC,UAAU,SAAK,mBAM/C;CACF"}
@@ -0,0 +1,100 @@
1
+ import { base64url } from 'multiformats/bases/base64';
2
+ import { requestLocalLock } from './lock.js';
3
+ export class Runtime {
4
+ constructor(implementation) {
5
+ this.implementation = implementation;
6
+ const { requestLock } = implementation;
7
+ this.hasImplementationLock = requestLock != null;
8
+ this.usingLock =
9
+ requestLock?.bind(implementation) ||
10
+ // Falling back to a local lock
11
+ requestLocalLock;
12
+ }
13
+ async generateKey(algs) {
14
+ const algsSorted = Array.from(algs).sort(compareAlgos);
15
+ return this.implementation.createKey(algsSorted);
16
+ }
17
+ async sha256(text) {
18
+ const bytes = new TextEncoder().encode(text);
19
+ const digest = await this.implementation.digest(bytes, { name: 'sha256' });
20
+ return base64url.baseEncode(digest);
21
+ }
22
+ async generateNonce(length = 16) {
23
+ const bytes = await this.implementation.getRandomValues(length);
24
+ return base64url.baseEncode(bytes);
25
+ }
26
+ async generatePKCE(byteLength) {
27
+ const verifier = await this.generateVerifier(byteLength);
28
+ return {
29
+ verifier,
30
+ challenge: await this.sha256(verifier),
31
+ method: 'S256',
32
+ };
33
+ }
34
+ async calculateJwkThumbprint(jwk) {
35
+ const components = extractJktComponents(jwk);
36
+ const data = JSON.stringify(components);
37
+ return this.sha256(data);
38
+ }
39
+ /**
40
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1}
41
+ * @note It is RECOMMENDED that the output of a suitable random number generator
42
+ * be used to create a 32-octet sequence. The octet sequence is then
43
+ * base64url-encoded to produce a 43-octet URL safe string to use as the code
44
+ * verifier.
45
+ */
46
+ async generateVerifier(byteLength = 32) {
47
+ if (byteLength < 32 || byteLength > 96) {
48
+ throw new TypeError('Invalid code_verifier length');
49
+ }
50
+ const bytes = await this.implementation.getRandomValues(byteLength);
51
+ return base64url.baseEncode(bytes);
52
+ }
53
+ }
54
+ function extractJktComponents(jwk) {
55
+ const get = (field) => {
56
+ const value = jwk[field];
57
+ if (typeof value !== 'string' || !value) {
58
+ throw new TypeError(`"${field}" Parameter missing or invalid`);
59
+ }
60
+ return value;
61
+ };
62
+ switch (jwk.kty) {
63
+ case 'EC':
64
+ return { crv: get('crv'), kty: get('kty'), x: get('x'), y: get('y') };
65
+ case 'OKP':
66
+ return { crv: get('crv'), kty: get('kty'), x: get('x') };
67
+ case 'RSA':
68
+ return { e: get('e'), kty: get('kty'), n: get('n') };
69
+ case 'oct':
70
+ return { k: get('k'), kty: get('kty') };
71
+ default:
72
+ throw new TypeError('"kty" (Key Type) Parameter missing or unsupported');
73
+ }
74
+ }
75
+ /**
76
+ * 256K > ES (256 > 384 > 512) > PS (256 > 384 > 512) > RS (256 > 384 > 512) > other (in original order)
77
+ */
78
+ function compareAlgos(a, b) {
79
+ if (a === 'ES256K')
80
+ return -1;
81
+ if (b === 'ES256K')
82
+ return 1;
83
+ for (const prefix of ['ES', 'PS', 'RS']) {
84
+ if (a.startsWith(prefix)) {
85
+ if (b.startsWith(prefix)) {
86
+ const aLen = parseInt(a.slice(2, 5));
87
+ const bLen = parseInt(b.slice(2, 5));
88
+ // Prefer shorter key lengths
89
+ return aLen - bLen;
90
+ }
91
+ return -1;
92
+ }
93
+ else if (b.startsWith(prefix)) {
94
+ return 1;
95
+ }
96
+ }
97
+ // Don't know how to compare, keep original order
98
+ return 0;
99
+ }
100
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import { base64url } from 'multiformats/bases/base64'\nimport type { Jwk, Key } from '@atproto/jwk'\nimport { requestLocalLock } from './lock.js'\nimport type {\n RuntimeImplementation,\n RuntimeLock,\n} from './runtime-implementation.js'\n\nexport class Runtime {\n readonly hasImplementationLock: boolean\n readonly usingLock: RuntimeLock\n\n constructor(protected implementation: RuntimeImplementation) {\n const { requestLock } = implementation\n\n this.hasImplementationLock = requestLock != null\n this.usingLock =\n requestLock?.bind(implementation) ||\n // Falling back to a local lock\n requestLocalLock\n }\n\n public async generateKey(algs: string[]): Promise<Key> {\n const algsSorted = Array.from(algs).sort(compareAlgos)\n return this.implementation.createKey(algsSorted)\n }\n\n public async sha256(text: string): Promise<string> {\n const bytes = new TextEncoder().encode(text)\n const digest = await this.implementation.digest(bytes, { name: 'sha256' })\n return base64url.baseEncode(digest)\n }\n\n public async generateNonce(length = 16): Promise<string> {\n const bytes = await this.implementation.getRandomValues(length)\n return base64url.baseEncode(bytes)\n }\n\n public async generatePKCE(byteLength?: number) {\n const verifier = await this.generateVerifier(byteLength)\n return {\n verifier,\n challenge: await this.sha256(verifier),\n method: 'S256' as const,\n }\n }\n\n public async calculateJwkThumbprint(jwk: Jwk) {\n const components = extractJktComponents(jwk)\n const data = JSON.stringify(components)\n return this.sha256(data)\n }\n\n /**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1}\n * @note It is RECOMMENDED that the output of a suitable random number generator\n * be used to create a 32-octet sequence. The octet sequence is then\n * base64url-encoded to produce a 43-octet URL safe string to use as the code\n * verifier.\n */\n protected async generateVerifier(byteLength = 32) {\n if (byteLength < 32 || byteLength > 96) {\n throw new TypeError('Invalid code_verifier length')\n }\n const bytes = await this.implementation.getRandomValues(byteLength)\n return base64url.baseEncode(bytes)\n }\n}\n\nfunction extractJktComponents(jwk: Record<string, unknown>) {\n const get = (field: string) => {\n const value = jwk[field]\n if (typeof value !== 'string' || !value) {\n throw new TypeError(`\"${field}\" Parameter missing or invalid`)\n }\n return value\n }\n\n switch (jwk.kty) {\n case 'EC':\n return { crv: get('crv'), kty: get('kty'), x: get('x'), y: get('y') }\n case 'OKP':\n return { crv: get('crv'), kty: get('kty'), x: get('x') }\n case 'RSA':\n return { e: get('e'), kty: get('kty'), n: get('n') }\n case 'oct':\n return { k: get('k'), kty: get('kty') }\n default:\n throw new TypeError('\"kty\" (Key Type) Parameter missing or unsupported')\n }\n}\n\n/**\n * 256K > ES (256 > 384 > 512) > PS (256 > 384 > 512) > RS (256 > 384 > 512) > other (in original order)\n */\nfunction compareAlgos(a: string, b: string): number {\n if (a === 'ES256K') return -1\n if (b === 'ES256K') return 1\n\n for (const prefix of ['ES', 'PS', 'RS']) {\n if (a.startsWith(prefix)) {\n if (b.startsWith(prefix)) {\n const aLen = parseInt(a.slice(2, 5))\n const bLen = parseInt(b.slice(2, 5))\n\n // Prefer shorter key lengths\n return aLen - bLen\n }\n return -1\n } else if (b.startsWith(prefix)) {\n return 1\n }\n }\n\n // Don't know how to compare, keep original order\n return 0\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import type { AtprotoDid } from '@atproto/did';
2
+ import type { Key } from '@atproto/jwk';
3
+ import { CachedGetter, type GetCachedOptions, type SimpleStore } from '@atproto-labs/simple-store';
4
+ import { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js';
5
+ import { TokenInvalidError } from './errors/token-invalid-error.js';
6
+ import { TokenRefreshError } from './errors/token-refresh-error.js';
7
+ import { TokenRevokedError } from './errors/token-revoked-error.js';
8
+ import type { ClientAuthMethod } from './oauth-client-auth.js';
9
+ import type { TokenSet } from './oauth-server-agent.js';
10
+ import type { OAuthServerFactory } from './oauth-server-factory.js';
11
+ import type { Runtime } from './runtime.js';
12
+ export type Session = {
13
+ dpopKey: Key;
14
+ authMethod: ClientAuthMethod;
15
+ tokenSet: TokenSet;
16
+ };
17
+ export type SessionStore = SimpleStore<string, Session>;
18
+ export type SessionGetterOptions = {
19
+ onSessionUpdated?: (sub: AtprotoDid, session: Session) => void;
20
+ onSessionDeleted?: (sub: AtprotoDid, cause: TokenRefreshError | TokenRevokedError | TokenInvalidError | unknown) => void;
21
+ };
22
+ export declare function isExpectedSessionError(err: unknown): err is AuthMethodUnsatisfiableError | TokenInvalidError | TokenRefreshError | TokenRevokedError | TypeError;
23
+ /**
24
+ * There are several advantages to wrapping the sessionStore in a (single)
25
+ * CachedGetter, the main of which is that the cached getter will ensure that at
26
+ * most one fresh call is ever being made. Another advantage, is that it
27
+ * contains the logic for reading from the cache which, if the cache is based on
28
+ * localStorage/indexedDB, will sync across multiple tabs (for a given sub).
29
+ */
30
+ export declare class SessionGetter extends CachedGetter<AtprotoDid, Session> {
31
+ #private;
32
+ constructor(sessionStore: SessionStore, serverFactory: OAuthServerFactory, runtime: Runtime, options?: SessionGetterOptions);
33
+ setStored(sub: AtprotoDid, session: Session): Promise<void>;
34
+ delStored(sub: AtprotoDid, cause?: unknown): Promise<void>;
35
+ get(sub: AtprotoDid, options?: GetCachedOptions): Promise<Session>;
36
+ /**
37
+ * @param refresh When `true`, the credentials will be refreshed even if they
38
+ * are not expired. When `false`, the credentials will not be refreshed even
39
+ * if they are expired. When `undefined`, the credentials will be refreshed
40
+ * if, and only if, they are (about to be) expired. Defaults to `undefined`.
41
+ */
42
+ getSession(sub: AtprotoDid, refresh?: boolean | 'auto'): Promise<Session>;
43
+ }
44
+ //# sourceMappingURL=session-getter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-getter.d.ts","sourceRoot":"","sources":["../src/session-getter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EACL,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,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;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAE9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AACnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAG3C,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,GAAG,CAAA;IACZ,UAAU,EAAE,gBAAgB,CAAA;IAC5B,QAAQ,EAAE,QAAQ,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEvD,MAAM,MAAM,oBAAoB,GAAG;IACjC,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9D,gBAAgB,CAAC,EAAE,CACjB,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,OAAO,KACvE,IAAI,CAAA;CACV,CAAA;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,+GAUlD;AAED;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC;;IAKlE,YACE,YAAY,EAAE,YAAY,EAC1B,aAAa,EAAE,kBAAkB,EACjC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,oBAAyB,EAwInC;IAEc,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,iBAqCzD;IAEc,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAGxE;IAEc,GAAG,CAChB,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,OAAO,CAAC,CAuBlB;IAED;;;;;OAKG;IACG,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,GAAE,OAAO,GAAG,MAAe,oBAKnE;CACF"}
@@ -0,0 +1,265 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { CachedGetter, } from '@atproto-labs/simple-store';
54
+ import { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js';
55
+ import { TokenInvalidError } from './errors/token-invalid-error.js';
56
+ import { TokenRefreshError } from './errors/token-refresh-error.js';
57
+ import { TokenRevokedError } from './errors/token-revoked-error.js';
58
+ import { OAuthResponseError } from './oauth-response-error.js';
59
+ import { combineSignals, timeoutSignal } from './util.js';
60
+ export function isExpectedSessionError(err) {
61
+ return (err instanceof TokenRefreshError ||
62
+ err instanceof TokenRevokedError ||
63
+ err instanceof TokenInvalidError ||
64
+ err instanceof AuthMethodUnsatisfiableError ||
65
+ // The stored session is invalid (e.g. missing properties) and cannot
66
+ // be used properly
67
+ err instanceof TypeError);
68
+ }
69
+ /**
70
+ * There are several advantages to wrapping the sessionStore in a (single)
71
+ * CachedGetter, the main of which is that the cached getter will ensure that at
72
+ * most one fresh call is ever being made. Another advantage, is that it
73
+ * contains the logic for reading from the cache which, if the cache is based on
74
+ * localStorage/indexedDB, will sync across multiple tabs (for a given sub).
75
+ */
76
+ export class SessionGetter extends CachedGetter {
77
+ #serverFactory;
78
+ #runtime;
79
+ #options;
80
+ constructor(sessionStore, serverFactory, runtime, options = {}) {
81
+ super(async (sub, { signal }, storedSession) => {
82
+ // There needs to be a previous session to be able to refresh. If
83
+ // storedSession is undefined, it means that the store does not contain
84
+ // a session for the given sub.
85
+ if (storedSession === undefined) {
86
+ // Because the session is not in the store, this.delStored() method
87
+ // will not be called by the CachedGetter class (because there is
88
+ // nothing to delete). This would typically happen if there is no
89
+ // synchronization mechanism between instances of this class. Let's
90
+ // make sure an event is dispatched here if this occurs.
91
+ const msg = 'The session was deleted by another process';
92
+ const cause = new TokenRefreshError(sub, msg);
93
+ await options.onSessionDeleted?.call(null, sub, cause);
94
+ throw cause;
95
+ }
96
+ // @NOTE Throwing a TokenRefreshError (or any other error class defined
97
+ // in the deleteOnError options) will result in this.delStored() being
98
+ // called.
99
+ const { dpopKey, authMethod, tokenSet } = storedSession;
100
+ if (sub !== tokenSet.sub) {
101
+ // Fool-proofing (e.g. against invalid session storage)
102
+ throw new TokenRefreshError(sub, 'Stored session sub mismatch');
103
+ }
104
+ if (!tokenSet.refresh_token) {
105
+ throw new TokenRefreshError(sub, 'No refresh token available');
106
+ }
107
+ const server = await serverFactory.fromIssuer(tokenSet.iss, authMethod, dpopKey, { signal });
108
+ // Because refresh tokens can only be used once, we must not use the
109
+ // "signal" to abort the refresh, or throw any abort error beyond this
110
+ // point. Any thrown error beyond this point will prevent the
111
+ // TokenGetter from obtaining, and storing, the new token set,
112
+ // effectively rendering the currently saved session unusable.
113
+ signal?.throwIfAborted();
114
+ try {
115
+ const newTokenSet = await server.refresh(tokenSet);
116
+ if (sub !== newTokenSet.sub) {
117
+ // The server returned another sub. Was the tokenSet manipulated?
118
+ throw new TokenRefreshError(sub, 'Token set sub mismatch');
119
+ }
120
+ return {
121
+ dpopKey,
122
+ tokenSet: newTokenSet,
123
+ authMethod: server.authMethod,
124
+ };
125
+ }
126
+ catch (cause) {
127
+ // Since refresh tokens can only be used once, we might run into
128
+ // concurrency issues if multiple instances (e.g. browser tabs) are
129
+ // trying to refresh the same token simultaneously. The chances of
130
+ // this happening when multiple instances are started simultaneously
131
+ // is reduced by randomizing the expiry time (see isStale() below).
132
+ // The best solution is to use a mutex/lock to ensure that only one
133
+ // instance is refreshing the token at a time (runtime.usingLock) but
134
+ // that is not always possible. Let's try to recover from concurrency
135
+ // issues, or force the session to be deleted by throwing a
136
+ // TokenRefreshError.
137
+ if (cause instanceof OAuthResponseError &&
138
+ cause.status === 400 &&
139
+ cause.error === 'invalid_grant') {
140
+ // In case there is no lock implementation in the runtime, we will
141
+ // wait for a short time to give the other concurrent instances a
142
+ // chance to finish their refreshing of the token. If a concurrent
143
+ // refresh did occur, we will pretend that this one succeeded.
144
+ if (!runtime.hasImplementationLock) {
145
+ await new Promise((r) => setTimeout(r, 1000));
146
+ const stored = await this.getStored(sub);
147
+ if (stored === undefined) {
148
+ // A concurrent refresh occurred and caused the session to be
149
+ // deleted (for a reason we can't know at this point).
150
+ // Using a distinct error message mainly for debugging
151
+ // purposes. Also, throwing a TokenRefreshError to trigger
152
+ // deletion through the deleteOnError callback.
153
+ const msg = 'The session was deleted by another process';
154
+ throw new TokenRefreshError(sub, msg, { cause });
155
+ }
156
+ else if (stored.tokenSet.access_token !== tokenSet.access_token ||
157
+ stored.tokenSet.refresh_token !== tokenSet.refresh_token) {
158
+ // A concurrent refresh occurred. Pretend this one succeeded.
159
+ return stored;
160
+ }
161
+ else {
162
+ // There were no concurrent refresh. The token is (likely)
163
+ // simply no longer valid.
164
+ }
165
+ }
166
+ // Make sure the session gets deleted from the store
167
+ const msg = cause.errorDescription ?? 'The session was revoked';
168
+ throw new TokenRefreshError(sub, msg, { cause });
169
+ }
170
+ throw cause;
171
+ }
172
+ }, sessionStore, {
173
+ isStale: (sub, { tokenSet }) => {
174
+ return (tokenSet.expires_at != null &&
175
+ new Date(tokenSet.expires_at).getTime() <
176
+ Date.now() +
177
+ // Add some lee way to ensure the token is not expired when it
178
+ // reaches the server.
179
+ 10e3 +
180
+ // Add some randomness to reduce the chances of multiple
181
+ // instances trying to refresh the token at the same.
182
+ 30e3 * Math.random());
183
+ },
184
+ deleteOnError: isExpectedSessionError,
185
+ });
186
+ this.#serverFactory = serverFactory;
187
+ this.#runtime = runtime;
188
+ this.#options = options;
189
+ }
190
+ async setStored(sub, session) {
191
+ // Prevent tampering with the stored value
192
+ if (sub !== session.tokenSet.sub) {
193
+ throw new Error('Token set does not match the expected sub');
194
+ }
195
+ try {
196
+ await super.setStored(sub, session);
197
+ }
198
+ catch (err) {
199
+ // If we fail to persist the session, we assume that we won't be able to
200
+ // fetch it again. We don't want to leave the session in a "limbo" state,
201
+ // so we proactively revoke the session and delete it from the store.
202
+ const errors = [err];
203
+ try {
204
+ const { tokenSet, dpopKey, authMethod } = session;
205
+ const server = await this.#serverFactory.fromIssuer(tokenSet.iss, authMethod, dpopKey);
206
+ await server.revoke(tokenSet.refresh_token ?? tokenSet.access_token);
207
+ }
208
+ catch (err) {
209
+ errors.push(err);
210
+ }
211
+ try {
212
+ await this.delStored(sub, err);
213
+ }
214
+ catch (err) {
215
+ errors.push(err);
216
+ }
217
+ throw new AggregateError(errors, `Failed to store session for ${sub}`);
218
+ }
219
+ await this.#options.onSessionUpdated?.call(null, sub, session);
220
+ }
221
+ async delStored(sub, cause) {
222
+ await super.delStored(sub, cause);
223
+ await this.#options.onSessionDeleted?.call(null, sub, cause);
224
+ }
225
+ async get(sub, options) {
226
+ const session = await this.#runtime.usingLock(`@atproto-oauth-client-${sub}`, async () => {
227
+ const env_1 = { stack: [], error: void 0, hasError: false };
228
+ try {
229
+ // Make sure, even if there is no signal in the options, that the
230
+ // request will be cancelled after at most 30 seconds.
231
+ const signal = timeoutSignal(30e3);
232
+ const abortController = __addDisposableResource(env_1, combineSignals([options?.signal, signal]), false);
233
+ return await super.get(sub, {
234
+ ...options,
235
+ signal: abortController.signal,
236
+ });
237
+ }
238
+ catch (e_1) {
239
+ env_1.error = e_1;
240
+ env_1.hasError = true;
241
+ }
242
+ finally {
243
+ __disposeResources(env_1);
244
+ }
245
+ });
246
+ if (sub !== session.tokenSet.sub) {
247
+ // Fool-proofing (e.g. against invalid session storage)
248
+ throw new Error('Token set does not match the expected sub');
249
+ }
250
+ return session;
251
+ }
252
+ /**
253
+ * @param refresh When `true`, the credentials will be refreshed even if they
254
+ * are not expired. When `false`, the credentials will not be refreshed even
255
+ * if they are expired. When `undefined`, the credentials will be refreshed
256
+ * if, and only if, they are (about to be) expired. Defaults to `undefined`.
257
+ */
258
+ async getSession(sub, refresh = 'auto') {
259
+ return this.get(sub, {
260
+ noCache: refresh === true,
261
+ allowStale: refresh === false,
262
+ });
263
+ }
264
+ }
265
+ //# sourceMappingURL=session-getter.js.map