@atcute/oauth-browser-client 2.0.3 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +19 -12
  2. package/dist/agents/exchange.d.ts +3 -2
  3. package/dist/agents/exchange.d.ts.map +1 -1
  4. package/dist/agents/exchange.js +3 -4
  5. package/dist/agents/exchange.js.map +1 -1
  6. package/dist/agents/server-agent.d.ts +6 -6
  7. package/dist/agents/server-agent.d.ts.map +1 -1
  8. package/dist/agents/server-agent.js +5 -9
  9. package/dist/agents/server-agent.js.map +1 -1
  10. package/dist/agents/sessions.d.ts +6 -5
  11. package/dist/agents/sessions.d.ts.map +1 -1
  12. package/dist/agents/sessions.js +16 -1
  13. package/dist/agents/sessions.js.map +1 -1
  14. package/dist/agents/user-agent.d.ts +2 -2
  15. package/dist/agents/user-agent.d.ts.map +1 -1
  16. package/dist/agents/user-agent.js +2 -2
  17. package/dist/agents/user-agent.js.map +1 -1
  18. package/dist/dpop.d.ts +2 -4
  19. package/dist/dpop.d.ts.map +1 -1
  20. package/dist/dpop.js +6 -79
  21. package/dist/dpop.js.map +1 -1
  22. package/dist/environment.d.ts +5 -5
  23. package/dist/environment.d.ts.map +1 -1
  24. package/dist/environment.js.map +1 -1
  25. package/dist/errors.d.ts +3 -3
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/errors.js +3 -3
  28. package/dist/errors.js.map +1 -1
  29. package/dist/index.d.ts +7 -15
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -11
  32. package/dist/index.js.map +1 -1
  33. package/dist/resolvers.d.ts +5 -4
  34. package/dist/resolvers.d.ts.map +1 -1
  35. package/dist/resolvers.js +5 -5
  36. package/dist/resolvers.js.map +1 -1
  37. package/dist/store/db.d.ts +8 -8
  38. package/dist/store/db.d.ts.map +1 -1
  39. package/dist/store/db.js.map +1 -1
  40. package/dist/types/client-assertion.d.ts +2 -3
  41. package/dist/types/client-assertion.d.ts.map +1 -1
  42. package/dist/types/server.d.ts +2 -56
  43. package/dist/types/server.d.ts.map +1 -1
  44. package/dist/types/token.d.ts +9 -21
  45. package/dist/types/token.d.ts.map +1 -1
  46. package/dist/utils/dpop-key.d.ts +10 -0
  47. package/dist/utils/dpop-key.d.ts.map +1 -0
  48. package/dist/utils/dpop-key.js +13 -0
  49. package/dist/utils/dpop-key.js.map +1 -0
  50. package/dist/utils/misc.d.ts.map +1 -1
  51. package/dist/utils/misc.js.map +1 -1
  52. package/dist/utils/response.d.ts.map +1 -1
  53. package/dist/utils/response.js.map +1 -1
  54. package/dist/utils/runtime.d.ts +0 -6
  55. package/dist/utils/runtime.d.ts.map +1 -1
  56. package/dist/utils/runtime.js +0 -16
  57. package/dist/utils/runtime.js.map +1 -1
  58. package/dist/utils/strings.d.ts.map +1 -1
  59. package/dist/utils/strings.js.map +1 -1
  60. package/lib/agents/exchange.ts +15 -16
  61. package/lib/agents/server-agent.ts +21 -24
  62. package/lib/agents/sessions.ts +28 -7
  63. package/lib/agents/user-agent.ts +14 -8
  64. package/lib/dpop.ts +9 -110
  65. package/lib/environment.ts +5 -5
  66. package/lib/errors.ts +15 -14
  67. package/lib/index.ts +16 -16
  68. package/lib/resolvers.ts +17 -15
  69. package/lib/store/db.ts +8 -8
  70. package/lib/types/client-assertion.ts +2 -4
  71. package/lib/types/server.ts +2 -57
  72. package/lib/types/token.ts +10 -24
  73. package/lib/utils/dpop-key.ts +24 -0
  74. package/lib/utils/runtime.ts +0 -22
  75. package/package.json +20 -11
  76. package/dist/types/client.d.ts +0 -38
  77. package/dist/types/client.d.ts.map +0 -1
  78. package/dist/types/client.js +0 -2
  79. package/dist/types/client.js.map +0 -1
  80. package/dist/types/dpop.d.ts +0 -10
  81. package/dist/types/dpop.d.ts.map +0 -1
  82. package/dist/types/dpop.js +0 -2
  83. package/dist/types/dpop.js.map +0 -1
  84. package/dist/types/identity.d.ts +0 -6
  85. package/dist/types/identity.d.ts.map +0 -1
  86. package/dist/types/identity.js +0 -2
  87. package/dist/types/identity.js.map +0 -1
  88. package/dist/types/par.d.ts +0 -5
  89. package/dist/types/par.d.ts.map +0 -1
  90. package/dist/types/par.js +0 -2
  91. package/dist/types/par.js.map +0 -1
  92. package/dist/utils/identity-resolver.d.ts +0 -7
  93. package/dist/utils/identity-resolver.d.ts.map +0 -1
  94. package/dist/utils/identity-resolver.js +0 -8
  95. package/dist/utils/identity-resolver.js.map +0 -1
  96. package/lib/types/client.ts +0 -82
  97. package/lib/types/dpop.ts +0 -9
  98. package/lib/types/identity.ts +0 -12
  99. package/lib/types/par.ts +0 -4
  100. package/lib/utils/identity-resolver.ts +0 -12
@@ -1,18 +1,17 @@
1
- import { nanoid } from 'nanoid';
2
-
1
+ import type { ResolvedActor } from '@atcute/identity-resolver';
3
2
  import type { ActorIdentifier } from '@atcute/lexicons';
3
+ import { generateDpopKey, generatePkce } from '@atcute/oauth-crypto';
4
+ import type { OAuthAuthorizationServerMetadata, OAuthPrompt } from '@atcute/oauth-types';
5
+
6
+ import { nanoid } from 'nanoid';
4
7
 
5
- import { createES256Key } from '../dpop.js';
6
- import { CLIENT_ID, database, REDIRECT_URI } from '../environment.js';
7
- import { AuthorizationError, LoginError } from '../errors.js';
8
- import type { ResolvedIdentity } from '../types/identity.js';
9
- import type { AuthorizationServerMetadata } from '../types/server.js';
10
- import type { Session } from '../types/token.js';
11
- import { generatePKCE } from '../utils/runtime.js';
8
+ import { CLIENT_ID, database, REDIRECT_URI } from '../environment.ts';
9
+ import { AuthorizationError, LoginError } from '../errors.ts';
10
+ import { resolveFromIdentifier, resolveFromService } from '../resolvers.ts';
11
+ import type { Session } from '../types/token.ts';
12
12
 
13
- import { resolveFromIdentifier, resolveFromService } from '../resolvers.js';
14
- import { OAuthServerAgent } from './server-agent.js';
15
- import { storeSession } from './sessions.js';
13
+ import { OAuthServerAgent } from './server-agent.ts';
14
+ import { storeSession } from './sessions.ts';
16
15
 
17
16
  export type AuthorizeTargetOptions =
18
17
  | { type: 'account'; identifier: ActorIdentifier }
@@ -22,7 +21,7 @@ export interface AuthorizeOptions {
22
21
  target: AuthorizeTargetOptions;
23
22
  scope: string;
24
23
  state?: unknown;
25
- prompt?: 'none' | 'login' | 'consent' | 'select_account';
24
+ prompt?: OAuthPrompt | (string & {});
26
25
  display?: 'page' | 'popup' | 'touch' | 'wap';
27
26
  locale?: string;
28
27
  }
@@ -35,7 +34,7 @@ export interface AuthorizeOptions {
35
34
  export const createAuthorizationUrl = async (options: AuthorizeOptions): Promise<URL> => {
36
35
  const { target, scope, state = null, ...reqs } = options;
37
36
 
38
- let resolved: { identity?: ResolvedIdentity; metadata: AuthorizationServerMetadata };
37
+ let resolved: { identity?: ResolvedActor; metadata: OAuthAuthorizationServerMetadata };
39
38
  switch (target.type) {
40
39
  case 'account': {
41
40
  resolved = await resolveFromIdentifier(target.identifier);
@@ -55,8 +54,8 @@ export const createAuthorizationUrl = async (options: AuthorizeOptions): Promise
55
54
 
56
55
  const sid = nanoid(24);
57
56
 
58
- const pkce = await generatePKCE();
59
- const dpopKey = await createES256Key();
57
+ const pkce = await generatePkce();
58
+ const dpopKey = await generateDpopKey(['ES256']);
60
59
 
61
60
  const params = {
62
61
  display: reqs.display,
@@ -1,22 +1,22 @@
1
1
  import type { Did } from '@atcute/lexicons';
2
-
3
- import { createDPoPFetch, createDPoPSignage } from '../dpop.js';
4
- import { CLIENT_ID, fetchClientAssertion, REDIRECT_URI } from '../environment.js';
5
- import { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.js';
6
- import { resolveFromIdentifier } from '../resolvers.js';
7
- import type { DPoPKey } from '../types/dpop.js';
8
- import type { OAuthParResponse } from '../types/par.js';
9
- import type { PersistedAuthorizationServerMetadata } from '../types/server.js';
10
- import type { ExchangeInfo, OAuthTokenResponse, TokenInfo } from '../types/token.js';
11
- import { pick } from '../utils/misc.js';
12
- import { extractContentType } from '../utils/response.js';
2
+ import { createDpopProofSigner, type DpopPrivateJwk } from '@atcute/oauth-crypto';
3
+ import type { AtprotoOAuthTokenResponse, OAuthParResponse } from '@atcute/oauth-types';
4
+
5
+ import { createDPoPFetch } from '../dpop.ts';
6
+ import { CLIENT_ID, fetchClientAssertion, REDIRECT_URI } from '../environment.ts';
7
+ import { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.ts';
8
+ import { resolveFromIdentifier } from '../resolvers.ts';
9
+ import type { PersistedAuthorizationServerMetadata } from '../types/server.ts';
10
+ import type { ExchangeInfo, TokenInfo } from '../types/token.ts';
11
+ import { pick } from '../utils/misc.ts';
12
+ import { extractContentType } from '../utils/response.ts';
13
13
 
14
14
  export class OAuthServerAgent {
15
15
  #fetch: typeof fetch;
16
16
  #metadata: PersistedAuthorizationServerMetadata;
17
- #dpopKey: DPoPKey;
17
+ #dpopKey: DpopPrivateJwk;
18
18
 
19
- constructor(metadata: PersistedAuthorizationServerMetadata, dpopKey: DPoPKey) {
19
+ constructor(metadata: PersistedAuthorizationServerMetadata, dpopKey: DpopPrivateJwk) {
20
20
  this.#metadata = metadata;
21
21
  this.#dpopKey = dpopKey;
22
22
  this.#fetch = createDPoPFetch(dpopKey, true);
@@ -26,7 +26,7 @@ export class OAuthServerAgent {
26
26
  endpoint: 'pushed_authorization_request',
27
27
  payload: Record<string, unknown>,
28
28
  ): Promise<OAuthParResponse>;
29
- async request(endpoint: 'token', payload: Record<string, unknown>): Promise<OAuthTokenResponse>;
29
+ async request(endpoint: 'token', payload: Record<string, unknown>): Promise<AtprotoOAuthTokenResponse>;
30
30
  async request(endpoint: 'revocation', payload: Record<string, unknown>): Promise<any>;
31
31
  async request(endpoint: 'introspection', payload: Record<string, unknown>): Promise<any>;
32
32
  async request(endpoint: string, payload: Record<string, unknown>): Promise<any> {
@@ -39,17 +39,12 @@ export class OAuthServerAgent {
39
39
  (endpoint === 'token' || endpoint === 'pushed_authorization_request') &&
40
40
  fetchClientAssertion !== undefined
41
41
  ) {
42
- const jkt = this.#dpopKey.jkt;
43
- if (jkt === undefined) {
44
- throw new Error(`DPoP key missing jkt field`);
45
- }
42
+ const sign = createDpopProofSigner(this.#dpopKey);
46
43
 
47
44
  const assertion = await fetchClientAssertion({
48
- jkt: jkt,
49
45
  aud: this.#metadata.issuer,
50
- createDpopProof: async (url) => {
51
- const sign = createDPoPSignage(this.#dpopKey);
52
- return await sign('POST', url, undefined, undefined);
46
+ createDpopProof: async (url, nonce) => {
47
+ return await sign('POST', url, nonce, undefined);
53
48
  },
54
49
  });
55
50
 
@@ -120,7 +115,7 @@ export class OAuthServerAgent {
120
115
  }
121
116
  }
122
117
 
123
- #processTokenResponse(res: OAuthTokenResponse): TokenInfo {
118
+ #processTokenResponse(res: AtprotoOAuthTokenResponse): TokenInfo {
124
119
  if (!res.sub) {
125
120
  throw new TypeError(`missing sub field in token response`);
126
121
  }
@@ -140,7 +135,9 @@ export class OAuthServerAgent {
140
135
  };
141
136
  }
142
137
 
143
- async #processExchangeResponse(res: OAuthTokenResponse): Promise<{ info: ExchangeInfo; token: TokenInfo }> {
138
+ async #processExchangeResponse(
139
+ res: AtprotoOAuthTokenResponse,
140
+ ): Promise<{ info: ExchangeInfo; token: TokenInfo }> {
144
141
  const sub = res.sub;
145
142
  if (!sub) {
146
143
  throw new TypeError(`missing sub field in token response`);
@@ -1,11 +1,12 @@
1
1
  import type { Did } from '@atcute/lexicons';
2
2
 
3
- import { database } from '../environment.js';
4
- import { OAuthResponseError, TokenRefreshError } from '../errors.js';
5
- import type { Session } from '../types/token.js';
6
- import { locks } from '../utils/runtime.js';
3
+ import { database } from '../environment.ts';
4
+ import { OAuthResponseError, TokenRefreshError } from '../errors.ts';
5
+ import type { RawSession, Session } from '../types/token.ts';
6
+ import { isLegacyDpopKey, migrateLegacyDpopKey } from '../utils/dpop-key.ts';
7
+ import { locks } from '../utils/runtime.ts';
7
8
 
8
- import { OAuthServerAgent } from './server-agent.js';
9
+ import { OAuthServerAgent } from './server-agent.ts';
9
10
 
10
11
  export interface SessionGetOptions {
11
12
  signal?: AbortSignal;
@@ -13,7 +14,7 @@ export interface SessionGetOptions {
13
14
  allowStale?: boolean;
14
15
  }
15
16
 
16
- type PendingItem<V> = Promise<{ value: V; isFresh: boolean }>;
17
+ type PendingItem<V> = { value: V; isFresh: boolean };
17
18
  const pending = new Map<Did, Promise<PendingItem<Session>>>();
18
19
 
19
20
  export const getSession = async (sub: Did, options?: SessionGetOptions): Promise<Session> => {
@@ -49,7 +50,7 @@ export const getSession = async (sub: Did, options?: SessionGetOptions): Promise
49
50
  }
50
51
 
51
52
  const run = async (): Promise<PendingItem<Session>> => {
52
- const storedSession = database.sessions.get(sub);
53
+ const storedSession = await migrateSessionIfNeeded(sub, database.sessions.get(sub));
53
54
 
54
55
  if (storedSession && allowStored(storedSession)) {
55
56
  // Use the stored value as return value for the current execution
@@ -140,3 +141,23 @@ const isTokenUsable = ({ token }: Session): boolean => {
140
141
  const expires = token.expires_at;
141
142
  return expires == null || Date.now() + 60_000 <= expires;
142
143
  };
144
+
145
+ const migrateSessionIfNeeded = async (
146
+ sub: Did,
147
+ session: RawSession | undefined,
148
+ ): Promise<Session | undefined> => {
149
+ if (!session || !isLegacyDpopKey(session.dpopKey)) {
150
+ return session as Session | undefined;
151
+ }
152
+
153
+ const dpopKey = await migrateLegacyDpopKey(session.dpopKey);
154
+ const migrated = { ...session, dpopKey };
155
+
156
+ try {
157
+ database.sessions.set(sub, migrated);
158
+ } catch {
159
+ // ignore persistence errors
160
+ }
161
+
162
+ return migrated;
163
+ };
@@ -1,17 +1,20 @@
1
1
  import type { FetchHandlerObject } from '@atcute/client';
2
2
  import type { Did } from '@atcute/lexicons';
3
3
 
4
- import { createDPoPFetch } from '../dpop.js';
5
- import type { Session } from '../types/token.js';
4
+ import { createDPoPFetch } from '../dpop.ts';
5
+ import type { Session } from '../types/token.ts';
6
6
 
7
- import { OAuthServerAgent } from './server-agent.js';
8
- import { type SessionGetOptions, deleteStoredSession, getSession } from './sessions.js';
7
+ import { OAuthServerAgent } from './server-agent.ts';
8
+ import { type SessionGetOptions, deleteStoredSession, getSession } from './sessions.ts';
9
9
 
10
10
  export class OAuthUserAgent implements FetchHandlerObject {
11
11
  #fetch: typeof fetch;
12
12
  #getSessionPromise: Promise<Session> | undefined;
13
13
 
14
- constructor(public session: Session) {
14
+ session: Session;
15
+
16
+ constructor(session: Session) {
17
+ this.session = session;
15
18
  this.#fetch = createDPoPFetch(session.dpopKey, false);
16
19
  }
17
20
 
@@ -23,9 +26,12 @@ export class OAuthUserAgent implements FetchHandlerObject {
23
26
  const promise = getSession(this.session.info.sub, options);
24
27
 
25
28
  promise
26
- .then((session) => {
27
- this.session = session;
28
- })
29
+ .then(
30
+ (session) => {
31
+ this.session = session;
32
+ },
33
+ () => {},
34
+ )
29
35
  .finally(() => {
30
36
  this.#getSessionPromise = undefined;
31
37
  });
package/lib/dpop.ts CHANGED
@@ -1,82 +1,20 @@
1
- import { fromBase64Url, toBase64Url } from '@atcute/multibase';
2
- import { encodeUtf8 } from '@atcute/uint8array';
1
+ import { createDpopProofSigner, sha256Base64Url, type DpopPrivateJwk } from '@atcute/oauth-crypto';
3
2
 
4
- import { nanoid } from 'nanoid';
3
+ import { database } from './environment.ts';
4
+ import { extractContentType } from './utils/response.ts';
5
5
 
6
- import { database } from './environment.js';
7
- import type { DPoPKey } from './types/dpop.js';
8
- import { extractContentType } from './utils/response.js';
9
- import { stringToSha256 } from './utils/runtime.js';
10
-
11
- const ES256_ALG = { name: 'ECDSA', namedCurve: 'P-256' } as const;
12
-
13
- export const createES256Key = async (): Promise<DPoPKey> => {
14
- const pair = await crypto.subtle.generateKey(ES256_ALG, true, ['sign', 'verify']);
15
-
16
- const key = await crypto.subtle.exportKey('pkcs8', pair.privateKey);
17
- const { ext: _ext, key_ops: _key_opts, ...jwk } = await crypto.subtle.exportKey('jwk', pair.publicKey);
18
-
19
- const canonicalJwk = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
20
- const jkt = await stringToSha256(canonicalJwk);
21
-
22
- return {
23
- typ: 'ES256',
24
- key: toBase64Url(new Uint8Array(key)),
25
- jwt: toBase64Url(encodeUtf8(JSON.stringify({ typ: 'dpop+jwt', alg: 'ES256', jwk: jwk }))),
26
- jkt: jkt,
27
- };
28
- };
29
-
30
- export const createDPoPSignage = (dpopKey: DPoPKey) => {
31
- const headerString = dpopKey.jwt;
32
- const keyPromise = crypto.subtle.importKey(
33
- 'pkcs8',
34
- fromBase64Url(dpopKey.key) as Uint8Array<ArrayBuffer>,
35
- ES256_ALG,
36
- true,
37
- ['sign'],
38
- );
39
-
40
- const constructPayload = (htm: string, htu: string, nonce: string | undefined, ath: string | undefined) => {
41
- const payload = {
42
- ath: ath,
43
- htm: htm,
44
- htu: htu,
45
- iat: Math.floor(Date.now() / 1_000),
46
- jti: nanoid(24),
47
- nonce: nonce,
48
- };
49
-
50
- return toBase64Url(encodeUtf8(JSON.stringify(payload)));
51
- };
52
-
53
- return async (method: string, htu: string, nonce: string | undefined, ath: string | undefined) => {
54
- const payloadString = constructPayload(method, htu, nonce, ath);
55
-
56
- const signed = await crypto.subtle.sign(
57
- { name: 'ECDSA', hash: { name: 'SHA-256' } },
58
- await keyPromise,
59
- encodeUtf8(headerString + '.' + payloadString) as Uint8Array<ArrayBuffer>,
60
- );
61
-
62
- const signatureString = toBase64Url(new Uint8Array(signed));
63
-
64
- return headerString + '.' + payloadString + '.' + signatureString;
65
- };
66
- };
67
-
68
- export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeof fetch => {
6
+ export const createDPoPFetch = (dpopKey: DpopPrivateJwk, isAuthServer?: boolean): typeof fetch => {
69
7
  const nonces = database.dpopNonces;
70
8
  const pending = database.inflightDpop;
71
9
 
72
- const sign = createDPoPSignage(dpopKey);
10
+ const sign = createDpopProofSigner(dpopKey);
73
11
 
74
12
  return async (input, init) => {
75
13
  const request = new Request(input, init);
76
14
 
77
15
  const authorizationHeader = request.headers.get('authorization');
78
16
  const ath = authorizationHeader?.startsWith('DPoP ')
79
- ? await stringToSha256(authorizationHeader.slice(5))
17
+ ? await sha256Base64Url(authorizationHeader.slice(5))
80
18
  : undefined;
81
19
 
82
20
  const { method, url } = request;
@@ -84,44 +22,24 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
84
22
 
85
23
  const htu = origin + pathname;
86
24
 
87
- // See if we have a pending promise for this origin, we'll await before
88
- // proceeding with this request, next comment describes what the promise
89
- // is meant to be.
90
25
  let deferred = pending.get(origin);
91
26
  if (deferred) {
92
27
  await deferred.promise;
93
28
  deferred = undefined;
94
29
  }
95
30
 
96
- // Get our persisted nonce value for this origin
97
31
  let initNonce: string | undefined;
98
32
  let expiredOrMissing = false;
99
33
  try {
100
34
  const [nonce, lapsed] = nonces.getWithLapsed(origin);
101
35
 
102
36
  initNonce = nonce;
103
-
104
- // The problem with DPoP nonces is that we don't have insight as to when
105
- // they'll expire, either we have a nonce value or we don't.
106
- //
107
- // Which is very unfortunate, if the client makes multiple requests at the
108
- // same time, there's a chance that all of them will fail due to the nonce
109
- // value having expired.
110
- //
111
- // To make this less painful, if it's been over 3 minutes since we last
112
- // had a nonce value, or we never had one to begin with, we'll let this
113
- // request through and defer everyone else until we get a possibly fresh
114
- // nonce value.
115
- //
116
- // 3 minutes being the DPoP nonce expiration time set by the reference PDS
117
- // implementation.
118
37
  expiredOrMissing = lapsed > 3 * 60 * 1_000;
119
38
  } catch {
120
- // Ignore read errors, we'll just act like we're missing a nonce.
39
+ // ignore read errors
121
40
  }
122
41
 
123
42
  if (expiredOrMissing) {
124
- // Defer everyone else until this request finishes.
125
43
  pending.set(origin, (deferred = Promise.withResolvers()));
126
44
  }
127
45
 
@@ -134,44 +52,30 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
134
52
 
135
53
  nextNonce = initResponse.headers.get('dpop-nonce');
136
54
  if (nextNonce === null || nextNonce === initNonce) {
137
- // No nonce was returned or it is the same as the one we sent. No need to
138
- // update the nonce store, or retry the request.
139
-
140
55
  return initResponse;
141
56
  }
142
57
 
143
- // Store the fresh nonce for future requests
144
58
  try {
145
59
  nonces.set(origin, nextNonce);
146
60
  } catch {
147
- // Ignore write errors
61
+ // ignore write errors
148
62
  }
149
63
 
150
64
  const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);
151
65
  if (!shouldRetry) {
152
- // Not a "use_dpop_nonce" error, so there is no need to retry
153
-
154
66
  return initResponse;
155
67
  }
156
68
 
157
69
  if (input === request || init?.body instanceof ReadableStream) {
158
- // If the input stream was already consumed, we cannot retry the request. A
159
- // solution would be to clone() the request but that would bufferize the
160
- // entire stream in memory which can lead to memory starvation. Instead, we
161
- // will return the original response and let the calling code handle retries.
162
-
163
70
  return initResponse;
164
71
  }
165
72
  } finally {
166
- // Now everyone can have their turn.
167
73
  if (deferred) {
168
74
  pending.delete(origin);
169
75
  deferred.resolve();
170
76
  }
171
77
  }
172
78
 
173
- // We got here because we were asked to retry the request (due to missing
174
- // nonce value in the first request), let's do just that.
175
79
  {
176
80
  const nextProof = await sign(method, htu, nextNonce, ath);
177
81
  const nextRequest = new Request(input, init);
@@ -179,13 +83,12 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
179
83
 
180
84
  const retryResponse = await fetch(nextRequest);
181
85
 
182
- // Check if the server returned another new nonce in the retry response
183
86
  const retryNonce = retryResponse.headers.get('dpop-nonce');
184
87
  if (retryNonce !== null && retryNonce !== nextNonce) {
185
88
  try {
186
89
  nonces.set(origin, retryNonce);
187
90
  } catch {
188
- // Ignore write errors
91
+ // ignore write errors
189
92
  }
190
93
  }
191
94
 
@@ -195,8 +98,6 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
195
98
  };
196
99
 
197
100
  const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean): Promise<boolean> => {
198
- // https://datatracker.ietf.org/doc/html/rfc6750#section-3
199
- // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no
200
101
  if (isAuthServer === undefined || isAuthServer === false) {
201
102
  if (response.status === 401) {
202
103
  const wwwAuth = response.headers.get('www-authenticate');
@@ -206,14 +107,12 @@ const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean):
206
107
  }
207
108
  }
208
109
 
209
- // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid
210
110
  if (isAuthServer === undefined || isAuthServer === true) {
211
111
  if (response.status === 400 && extractContentType(response.headers) === 'application/json') {
212
112
  try {
213
113
  const json = await response.clone().json();
214
114
  return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce';
215
115
  } catch {
216
- // Response too big (to be "use_dpop_nonce" error) or invalid JSON
217
116
  return false;
218
117
  }
219
118
  }
@@ -1,7 +1,7 @@
1
- import type { IdentityResolver } from './types/identity.js';
1
+ import type { ActorResolver } from '@atcute/identity-resolver';
2
2
 
3
- import { createOAuthDatabase, type OAuthDatabase } from './store/db.js';
4
- import type { ClientAssertionFetcher } from './types/client-assertion.js';
3
+ import { createOAuthDatabase, type OAuthDatabase } from './store/db.ts';
4
+ import type { ClientAssertionFetcher } from './types/client-assertion.ts';
5
5
 
6
6
  export let CLIENT_ID: string;
7
7
  export let REDIRECT_URI: string;
@@ -10,7 +10,7 @@ export let fetchClientAssertion: ClientAssertionFetcher | undefined;
10
10
 
11
11
  export let database: OAuthDatabase;
12
12
 
13
- export let identityResolver: IdentityResolver;
13
+ export let identityResolver: ActorResolver;
14
14
 
15
15
  export interface ConfigureOAuthOptions {
16
16
  /**
@@ -22,7 +22,7 @@ export interface ConfigureOAuthOptions {
22
22
  };
23
23
 
24
24
  /** resolves actor identifiers into identity metadata */
25
- identityResolver: IdentityResolver;
25
+ identityResolver: ActorResolver;
26
26
 
27
27
  /**
28
28
  * optional function to fetch DPoP-bound client assertions from your backend.
package/lib/errors.ts CHANGED
@@ -15,25 +15,23 @@ export class ResolverError extends Error {
15
15
  export class TokenRefreshError extends Error {
16
16
  override name = 'TokenRefreshError';
17
17
 
18
- constructor(
19
- public readonly sub: Did,
20
- message: string,
21
- options?: ErrorOptions,
22
- ) {
18
+ readonly sub: Did;
19
+
20
+ constructor(sub: Did, message: string, options?: ErrorOptions) {
23
21
  super(message, options);
22
+ this.sub = sub;
24
23
  }
25
24
  }
26
25
 
27
26
  export class OAuthResponseError extends Error {
28
27
  override name = 'OAuthResponseError';
29
28
 
29
+ readonly response: Response;
30
+ readonly data: any;
30
31
  readonly error: string | undefined;
31
32
  readonly description: string | undefined;
32
33
 
33
- constructor(
34
- public readonly response: Response,
35
- public readonly data: any,
36
- ) {
34
+ constructor(response: Response, data: any) {
37
35
  const error = ifString(ifObject(data)?.['error']);
38
36
  const errorDescription = ifString(ifObject(data)?.['error_description']);
39
37
 
@@ -43,6 +41,8 @@ export class OAuthResponseError extends Error {
43
41
 
44
42
  super(message);
45
43
 
44
+ this.response = response;
45
+ this.data = data;
46
46
  this.error = error;
47
47
  this.description = errorDescription;
48
48
  }
@@ -59,12 +59,13 @@ export class OAuthResponseError extends Error {
59
59
  export class FetchResponseError extends Error {
60
60
  override name = 'FetchResponseError';
61
61
 
62
- constructor(
63
- public readonly response: Response,
64
- public status: number,
65
- message: string,
66
- ) {
62
+ readonly response: Response;
63
+ status: number;
64
+
65
+ constructor(response: Response, status: number, message: string) {
67
66
  super(message);
67
+ this.response = response;
68
+ this.status = status;
68
69
  }
69
70
  }
70
71
 
package/lib/index.ts CHANGED
@@ -1,19 +1,19 @@
1
- export { configureOAuth, type ConfigureOAuthOptions } from './environment.js';
1
+ export { configureOAuth, type ConfigureOAuthOptions } from './environment.ts';
2
2
 
3
- export * from './errors.js';
3
+ export * from './errors.ts';
4
4
 
5
- export * from './agents/exchange.js';
6
- export * from './agents/server-agent.js';
7
- export * from './agents/sessions.js';
8
- export * from './agents/user-agent.js';
5
+ export * from './agents/exchange.ts';
6
+ export {
7
+ getSession,
8
+ deleteStoredSession,
9
+ listStoredSessions,
10
+ type SessionGetOptions,
11
+ } from './agents/sessions.ts';
12
+ export * from './agents/user-agent.ts';
9
13
 
10
- export * from './types/client-assertion.js';
11
- export * from './types/client.js';
12
- export * from './types/dpop.js';
13
- export * from './types/identity.js';
14
- export * from './types/par.js';
15
- export * from './types/server.js';
16
- export * from './types/store.js';
17
- export * from './types/token.js';
18
-
19
- export * from './utils/identity-resolver.js';
14
+ export type {
15
+ ClientAssertionCredentials,
16
+ ClientAssertionFetcher,
17
+ FetchClientAssertionParams,
18
+ } from './types/client-assertion.ts';
19
+ export type { TokenInfo, ExchangeInfo, Session } from './types/token.ts';