@udibo/oauth2 0.4.1 → 0.5.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 (161) hide show
  1. package/README.md +3 -3
  2. package/SECURITY.md +0 -4
  3. package/esm/adapters/hono/_common.d.ts +10 -13
  4. package/esm/adapters/hono/_common.js +11 -20
  5. package/esm/adapters/hono/authorization-server.d.ts +16 -10
  6. package/esm/adapters/hono/authorization-server.js +13 -8
  7. package/esm/adapters/hono/bff/auth-request-store.d.ts +4 -0
  8. package/esm/adapters/hono/bff/auth-request-store.js +4 -0
  9. package/esm/adapters/hono/bff/bff.d.ts +29 -18
  10. package/esm/adapters/hono/bff/bff.js +15 -11
  11. package/esm/adapters/hono/bff/proxy.js +7 -55
  12. package/esm/adapters/hono/bff/session-store.d.ts +9 -9
  13. package/esm/adapters/hono/bff/session-store.js +3 -2
  14. package/esm/adapters/hono/bff/testing.d.ts +14 -3
  15. package/esm/adapters/hono/bff/testing.js +14 -3
  16. package/esm/adapters/hono/identity.d.ts +5 -3
  17. package/esm/adapters/hono/identity.js +1 -17
  18. package/esm/adapters/hono/resource-server.d.ts +3 -2
  19. package/esm/adapters/hono/resource-server.js +3 -2
  20. package/esm/client/_http.d.ts +4 -7
  21. package/esm/client/_http.js +4 -37
  22. package/esm/client/base.d.ts +3 -6
  23. package/esm/client/bff-client.d.ts +7 -6
  24. package/esm/client/bff-client.js +7 -6
  25. package/esm/client/browser-storage.d.ts +33 -34
  26. package/esm/client/browser-storage.js +47 -32
  27. package/esm/client/check.d.ts +10 -3
  28. package/esm/client/check.js +28 -8
  29. package/esm/client/direct-client.d.ts +49 -29
  30. package/esm/client/direct-client.js +59 -59
  31. package/esm/client/discovery-cache.d.ts +12 -5
  32. package/esm/client/discovery-cache.js +11 -4
  33. package/esm/client/events.d.ts +11 -3
  34. package/esm/client/events.js +1 -1
  35. package/esm/client/mod.d.ts +6 -4
  36. package/esm/client/mod.js +5 -3
  37. package/esm/client/storage.d.ts +53 -23
  38. package/esm/client/storage.js +43 -15
  39. package/esm/errors.d.ts +15 -12
  40. package/esm/errors.js +12 -8
  41. package/esm/identity/delivery.d.ts +7 -7
  42. package/esm/identity/delivery.js +1 -2
  43. package/esm/identity/errors.d.ts +10 -4
  44. package/esm/identity/errors.js +4 -1
  45. package/esm/identity/events.d.ts +21 -5
  46. package/esm/identity/events.js +4 -2
  47. package/esm/identity/external/_shared.d.ts +5 -7
  48. package/esm/identity/external/_shared.js +5 -7
  49. package/esm/identity/external/_token-exchange.d.ts +3 -6
  50. package/esm/identity/external/_token-exchange.js +2 -5
  51. package/esm/identity/external/apple-client-secret.d.ts +13 -8
  52. package/esm/identity/external/apple-client-secret.js +9 -6
  53. package/esm/identity/external/apple.d.ts +17 -10
  54. package/esm/identity/external/apple.js +12 -9
  55. package/esm/identity/external/discord.d.ts +5 -1
  56. package/esm/identity/external/errors.d.ts +6 -2
  57. package/esm/identity/external/errors.js +6 -2
  58. package/esm/identity/external/flow.d.ts +9 -6
  59. package/esm/identity/external/flow.js +6 -5
  60. package/esm/identity/external/github.d.ts +7 -3
  61. package/esm/identity/external/github.js +2 -2
  62. package/esm/identity/external/google.d.ts +5 -1
  63. package/esm/identity/external/oauth2.d.ts +13 -2
  64. package/esm/identity/external/oauth2.js +3 -3
  65. package/esm/identity/external/oidc.d.ts +21 -10
  66. package/esm/identity/external/oidc.js +11 -6
  67. package/esm/identity/external/provider.d.ts +4 -2
  68. package/esm/identity/identifier.d.ts +6 -5
  69. package/esm/identity/identifier.js +6 -5
  70. package/esm/identity/lockout.d.ts +4 -3
  71. package/esm/identity/lockout.js +4 -3
  72. package/esm/identity/mfa/mod.d.ts +2 -1
  73. package/esm/identity/mfa/mod.js +2 -1
  74. package/esm/identity/mfa/recovery-codes.d.ts +2 -1
  75. package/esm/identity/mfa/service.d.ts +29 -11
  76. package/esm/identity/mfa/service.js +35 -11
  77. package/esm/identity/mfa/totp.d.ts +4 -2
  78. package/esm/identity/mfa/totp.js +4 -2
  79. package/esm/identity/migration.d.ts +8 -7
  80. package/esm/identity/migration.js +9 -12
  81. package/esm/identity/mod.d.ts +2 -2
  82. package/esm/identity/mod.js +2 -2
  83. package/esm/identity/otp.d.ts +23 -16
  84. package/esm/identity/otp.js +13 -12
  85. package/esm/identity/password-policy.d.ts +3 -3
  86. package/esm/identity/password-policy.js +3 -3
  87. package/esm/identity/password.d.ts +23 -20
  88. package/esm/identity/password.js +15 -16
  89. package/esm/identity/rate-limit.d.ts +1 -7
  90. package/esm/identity/rate-limit.js +1 -2
  91. package/esm/identity/service.d.ts +76 -44
  92. package/esm/identity/service.js +86 -60
  93. package/esm/identity/session.d.ts +5 -4
  94. package/esm/identity/session.js +5 -4
  95. package/esm/identity/token-flow.d.ts +8 -8
  96. package/esm/identity/token-flow.js +5 -6
  97. package/esm/models/client.d.ts +7 -4
  98. package/esm/models/token.d.ts +2 -3
  99. package/esm/react/callback.d.ts +3 -3
  100. package/esm/react/callback.js +3 -3
  101. package/esm/react/components/class-names.d.ts +3 -6
  102. package/esm/react/components/class-names.js +3 -6
  103. package/esm/react/components/internal.d.ts +7 -17
  104. package/esm/react/components/internal.js +7 -17
  105. package/esm/react/components/mod.d.ts +7 -5
  106. package/esm/react/components/mod.js +7 -5
  107. package/esm/react/components/types.d.ts +8 -6
  108. package/esm/react/context.d.ts +3 -3
  109. package/esm/react/provider.d.ts +3 -2
  110. package/esm/react/provider.js +0 -13
  111. package/esm/react/require-auth.d.ts +0 -3
  112. package/esm/react/require-auth.js +0 -3
  113. package/esm/react/testing.d.ts +3 -2
  114. package/esm/react/testing.js +3 -3
  115. package/esm/react/use-oauth2.d.ts +1 -1
  116. package/esm/server/authorization-server.d.ts +81 -54
  117. package/esm/server/authorization-server.js +76 -100
  118. package/esm/server/grants/authorization-code.d.ts +7 -3
  119. package/esm/server/grants/authorization-code.js +6 -2
  120. package/esm/server/grants/device-authorization.d.ts +1 -1
  121. package/esm/server/grants/grant.d.ts +15 -8
  122. package/esm/server/grants/grant.js +26 -18
  123. package/esm/server/grants/password.d.ts +1 -1
  124. package/esm/server/grants/refresh-token.d.ts +6 -3
  125. package/esm/server/grants/refresh-token.js +4 -3
  126. package/esm/server/introspection-token-reader.d.ts +16 -7
  127. package/esm/server/introspection-token-reader.js +9 -3
  128. package/esm/server/jwks-token-reader.d.ts +3 -3
  129. package/esm/server/jwks-token-reader.js +3 -3
  130. package/esm/server/redirect-uri.d.ts +6 -9
  131. package/esm/server/redirect-uri.js +7 -34
  132. package/esm/server/resource-server.d.ts +5 -3
  133. package/esm/server/resource-server.js +5 -3
  134. package/esm/server/services/client.d.ts +5 -1
  135. package/esm/server/services/device-authorization.d.ts +6 -6
  136. package/esm/server/services/token.d.ts +15 -8
  137. package/esm/server/services/token.js +1 -1
  138. package/esm/server/signing-keys.d.ts +24 -9
  139. package/esm/server/signing-keys.js +20 -9
  140. package/esm/testing/contract/mod.d.ts +3 -3
  141. package/esm/testing/contract/mod.js +3 -3
  142. package/esm/testing/contract/tenant.d.ts +1 -1
  143. package/esm/testing/contract/tenant.js +2 -2
  144. package/esm/testing/mod.d.ts +6 -4
  145. package/esm/testing/mod.js +6 -4
  146. package/esm/testing/services.d.ts +1 -1
  147. package/esm/testing/services.js +1 -1
  148. package/esm/testing/tenant.d.ts +14 -5
  149. package/esm/utils/_buffer.d.ts +3 -2
  150. package/esm/utils/_buffer.js +3 -2
  151. package/esm/utils/_timing-safe.d.ts +8 -6
  152. package/esm/utils/_timing-safe.js +8 -6
  153. package/esm/utils/basic-auth.d.ts +5 -12
  154. package/esm/utils/basic-auth.js +5 -12
  155. package/esm/utils/crypto.d.ts +2 -1
  156. package/esm/utils/crypto.js +2 -1
  157. package/esm/utils/text.d.ts +2 -9
  158. package/esm/utils/text.js +2 -9
  159. package/esm/utils/url.d.ts +2 -2
  160. package/esm/utils/url.js +0 -2
  161. package/package.json +1 -1
@@ -15,9 +15,11 @@
15
15
  * care — the React adapter's `<OAuth2Provider>`, a shared UI helper — should
16
16
  * accept.
17
17
  *
18
- * Storage interfaces (token / refresh-token / auth-request) default to
19
- * in-memory implementations suitable for servers and tests; browser
20
- * applications should substitute the appropriate persistent variants.
18
+ * A {@link DirectClient} keeps its token bundle and refresh token in memory
19
+ * by default, so they are lost on reload; a browser app that must survive one
20
+ * passes {@link IndexedDBRefreshTokenStorage}. The pending auth request
21
+ * defaults to {@link SessionStorageAuthRequestStorage} in a browser document
22
+ * and to memory everywhere else.
21
23
  *
22
24
  * @module
23
25
  */
@@ -27,7 +29,7 @@ export { BffClient, type BffClientOptions, type BffEndpoints, } from "./bff-clie
27
29
  export { type AuthorizationCallbackResult, type AuthorizationRedirect, type AuthorizationServerEndpoints, type DeviceAuthorizationOptions, DirectClient, type DirectClientOptions, type ExchangeOptions, type ExchangeResult, type IntrospectionResult, type LoginOptions, type PollDeviceTokenOptions, } from "./direct-client.js";
28
30
  export { DEFAULT_DISCOVERY_CACHE_MAX_ENTRIES, DEFAULT_DISCOVERY_TTL_MS, type DiscoveryCache, type DiscoveryCacheEntry, MemoryDiscoveryCache, type MemoryDiscoveryCacheOptions, } from "./discovery-cache.js";
29
31
  export { type AuthenticatedEvent, type ErrorEvent, type LoggedOutEvent, type OAuth2ClientEvent, type OAuth2ClientEventListener, type TokenRefreshedEvent, } from "./events.js";
30
- export { type AuthRequestRecord, type AuthRequestStorage, MemoryAuthRequestStorage, MemoryRefreshTokenStorage, MemoryTokenStorage, type RefreshTokenStorage, type TokenBundle, type TokenStorage, } from "./storage.js";
32
+ export { type AuthRequestRecord, type AuthRequestStorage, MemoryAuthRequestStorage, type MemoryAuthRequestStorageOptions, MemoryRefreshTokenStorage, MemoryTokenStorage, type RefreshTokenStorage, type TokenBundle, type TokenStorage, } from "./storage.js";
31
33
  export { checkPermissions, type CheckPermissionsOptions, type CheckPermissionsResult, type CheckResource, } from "./check.js";
32
34
  export { Authorization, authorizationFromClaims, } from "../models/authorization.js";
33
35
  export type { OrganizationContext } from "../models/authorization.js";
package/esm/client/mod.js CHANGED
@@ -15,9 +15,11 @@
15
15
  * care — the React adapter's `<OAuth2Provider>`, a shared UI helper — should
16
16
  * accept.
17
17
  *
18
- * Storage interfaces (token / refresh-token / auth-request) default to
19
- * in-memory implementations suitable for servers and tests; browser
20
- * applications should substitute the appropriate persistent variants.
18
+ * A {@link DirectClient} keeps its token bundle and refresh token in memory
19
+ * by default, so they are lost on reload; a browser app that must survive one
20
+ * passes {@link IndexedDBRefreshTokenStorage}. The pending auth request
21
+ * defaults to {@link SessionStorageAuthRequestStorage} in a browser document
22
+ * and to memory everywhere else.
21
23
  *
22
24
  * @module
23
25
  */
@@ -3,19 +3,18 @@
3
3
  * tokens, so none of this applies to it.
4
4
  *
5
5
  * The client keeps three pieces of mutable state across calls:
6
- * - the current token bundle (access + refresh + expiry + id token),
7
- * - the refresh token specifically (may be split into a harder-to-exfiltrate
8
- * store than the access token — e.g. IndexedDB or a Web Worker),
6
+ * - the current token bundle (access token, expiry, scope, id token),
7
+ * - the refresh token, in its own store so it can persist somewhere the
8
+ * access token does not,
9
9
  * - a short-lived auth-request map (`state` → `{ codeVerifier, returnTo, … }`)
10
10
  * used to correlate an authorize redirect with its callback.
11
11
  *
12
- * The defaults here are all in-memory and live only for the process / tab
13
- * lifetime. Browser applications that need persistence across reloads should
14
- * plug in the appropriate implementation — most commonly:
15
- * - `MemoryTokenStorage` for access tokens (always),
16
- * - an `IndexedDB`-backed `RefreshTokenStorage` for refresh tokens (so they
17
- * are not readable via `document.cookie` or `localStorage`),
18
- * - `sessionStorage`-backed `AuthRequestStorage` for the redirect state map.
12
+ * The implementations here are in-memory and live only for the process / tab
13
+ * lifetime. `DirectClient` uses them by default, except that in a browser
14
+ * document its auth-request storage defaults to `sessionStorage`. A browser
15
+ * app that must stay signed in across reloads passes
16
+ * `IndexedDBRefreshTokenStorage` for the refresh token and keeps the access
17
+ * token in memory.
19
18
  *
20
19
  * @module
21
20
  */
@@ -42,10 +41,9 @@ export interface TokenStorage {
42
41
  clear(): Promise<void> | void;
43
42
  }
44
43
  /**
45
- * Persists the refresh token independently from the access token. Kept
46
- * separate so applications can harden refresh-token storage (IndexedDB,
47
- * Web Worker, or a server-side session cookie) without changing where
48
- * short-lived access tokens live.
44
+ * Persists the refresh token independently from the access token, so it can
45
+ * outlive a reload (IndexedDB, a Web Worker, a server-side session) while the
46
+ * short-lived access token stays wherever {@link TokenStorage} keeps it.
49
47
  */
50
48
  export interface RefreshTokenStorage {
51
49
  /** Returns the stored refresh token, or `null` when none has been persisted. */
@@ -74,15 +72,29 @@ export interface AuthRequestRecord {
74
72
  */
75
73
  export interface AuthRequestStorage {
76
74
  /**
77
- * Returns the record for {@linkcode state}, or `null` if absent or expired.
78
- * Implementations may prune the entry when returning `null` for an expired one.
75
+ * Returns the record for {@linkcode state}, or `null` if absent. An
76
+ * implementation may also return `null` for, and prune, a record it deems
77
+ * expired; `DirectClient` separately rejects one older than its
78
+ * `authRequestTtlMs`.
79
79
  */
80
80
  get(state: string): Promise<AuthRequestRecord | null> | AuthRequestRecord | null;
81
81
  /** Stores {@linkcode value} under {@linkcode state} before the authorize redirect. */
82
82
  set(state: string, value: AuthRequestRecord): Promise<void> | void;
83
- /** Removes one entry (called after successful callback consumption). */
83
+ /**
84
+ * Returns the record for {@linkcode state} and removes it in one atomic
85
+ * step, or `null` if absent. `DirectClient` claims the record this way
86
+ * before it calls the token endpoint, so of two callbacks racing on one
87
+ * `state` only one reaches it. Optional for compatibility: without it the
88
+ * client falls back to `get` then `delete`, which still consumes the record
89
+ * before the token call but lets concurrent callers that both read it
90
+ * before either deletes it redeem it twice. Implement it with the backing
91
+ * store's own atomic primitive (`GETDEL`, `DELETE … RETURNING`) whenever
92
+ * the store is shared between processes or requests.
93
+ */
94
+ take?(state: string): Promise<AuthRequestRecord | null> | AuthRequestRecord | null;
95
+ /** Removes one entry — a claimed or stale one. */
84
96
  delete(state: string): Promise<void> | void;
85
- /** Removes every entry. Called on logout. */
97
+ /** Removes every entry. Called when the client clears its session. */
86
98
  clear(): Promise<void> | void;
87
99
  }
88
100
  /** Default in-memory {@link TokenStorage}. Data is lost when the process exits. */
@@ -108,17 +120,35 @@ export declare class MemoryRefreshTokenStorage implements RefreshTokenStorage {
108
120
  /** Discards the in-memory refresh token. */
109
121
  clear(): void;
110
122
  }
123
+ /** Options for {@link MemoryAuthRequestStorage}. */
124
+ export interface MemoryAuthRequestStorageOptions {
125
+ /**
126
+ * Age past which a record is pruned the next time any record is written,
127
+ * in ms. Defaults to 10 minutes; `Infinity` never prunes.
128
+ */
129
+ ttlMs?: number;
130
+ }
111
131
  /**
112
- * Default in-memory {@link AuthRequestStorage}. Data is lost when the
113
- * process exits — fine for server-side / CLI usage. Browser apps should
114
- * plug in a `sessionStorage`-backed implementation so the record survives
115
- * the authorize-redirect round-trip.
132
+ * In-memory {@link AuthRequestStorage}, `DirectClient`'s default outside a
133
+ * browser document. Data is lost when the process exits. Every write prunes
134
+ * records older than {@link MemoryAuthRequestStorageOptions.ttlMs}, so logins
135
+ * that never return to the callback do not accumulate in a long-running
136
+ * server.
116
137
  */
117
138
  export declare class MemoryAuthRequestStorage implements AuthRequestStorage {
118
139
  #private;
140
+ /** Configures the pruning TTL, 10 minutes by default. */
141
+ constructor(options?: MemoryAuthRequestStorageOptions);
119
142
  /** Returns the record for {@linkcode state}, or `null` if none is stored. */
120
143
  get(state: string): AuthRequestRecord | null;
121
- /** Stores {@linkcode value} under {@linkcode state}. */
144
+ /** Returns and removes the record for {@linkcode state} in one step. */
145
+ take(state: string): AuthRequestRecord | null;
146
+ /**
147
+ * Stores {@linkcode value} under {@linkcode state}, first pruning records
148
+ * past the TTL. Pruning walks records oldest-write-first and stops at the
149
+ * first one inside the TTL, so it assumes each `createdAt` is the time of
150
+ * its write, as `DirectClient.login` sets it.
151
+ */
122
152
  set(state: string, value: AuthRequestRecord): void;
123
153
  /** Removes the record for {@linkcode state}. */
124
154
  delete(state: string): void;
@@ -3,19 +3,18 @@
3
3
  * tokens, so none of this applies to it.
4
4
  *
5
5
  * The client keeps three pieces of mutable state across calls:
6
- * - the current token bundle (access + refresh + expiry + id token),
7
- * - the refresh token specifically (may be split into a harder-to-exfiltrate
8
- * store than the access token — e.g. IndexedDB or a Web Worker),
6
+ * - the current token bundle (access token, expiry, scope, id token),
7
+ * - the refresh token, in its own store so it can persist somewhere the
8
+ * access token does not,
9
9
  * - a short-lived auth-request map (`state` → `{ codeVerifier, returnTo, … }`)
10
10
  * used to correlate an authorize redirect with its callback.
11
11
  *
12
- * The defaults here are all in-memory and live only for the process / tab
13
- * lifetime. Browser applications that need persistence across reloads should
14
- * plug in the appropriate implementation — most commonly:
15
- * - `MemoryTokenStorage` for access tokens (always),
16
- * - an `IndexedDB`-backed `RefreshTokenStorage` for refresh tokens (so they
17
- * are not readable via `document.cookie` or `localStorage`),
18
- * - `sessionStorage`-backed `AuthRequestStorage` for the redirect state map.
12
+ * The implementations here are in-memory and live only for the process / tab
13
+ * lifetime. `DirectClient` uses them by default, except that in a browser
14
+ * document its auth-request storage defaults to `sessionStorage`. A browser
15
+ * app that must stay signed in across reloads passes
16
+ * `IndexedDBRefreshTokenStorage` for the refresh token and keeps the access
17
+ * token in memory.
19
18
  *
20
19
  * @module
21
20
  */
@@ -55,19 +54,38 @@ export class MemoryRefreshTokenStorage {
55
54
  }
56
55
  }
57
56
  /**
58
- * Default in-memory {@link AuthRequestStorage}. Data is lost when the
59
- * process exits — fine for server-side / CLI usage. Browser apps should
60
- * plug in a `sessionStorage`-backed implementation so the record survives
61
- * the authorize-redirect round-trip.
57
+ * In-memory {@link AuthRequestStorage}, `DirectClient`'s default outside a
58
+ * browser document. Data is lost when the process exits. Every write prunes
59
+ * records older than {@link MemoryAuthRequestStorageOptions.ttlMs}, so logins
60
+ * that never return to the callback do not accumulate in a long-running
61
+ * server.
62
62
  */
63
63
  export class MemoryAuthRequestStorage {
64
64
  #records = new Map();
65
+ #ttlMs;
66
+ /** Configures the pruning TTL, 10 minutes by default. */
67
+ constructor(options = {}) {
68
+ this.#ttlMs = options.ttlMs ?? 10 * 60 * 1000;
69
+ }
65
70
  /** Returns the record for {@linkcode state}, or `null` if none is stored. */
66
71
  get(state) {
67
72
  return this.#records.get(state) ?? null;
68
73
  }
69
- /** Stores {@linkcode value} under {@linkcode state}. */
74
+ /** Returns and removes the record for {@linkcode state} in one step. */
75
+ take(state) {
76
+ const record = this.#records.get(state) ?? null;
77
+ this.#records.delete(state);
78
+ return record;
79
+ }
80
+ /**
81
+ * Stores {@linkcode value} under {@linkcode state}, first pruning records
82
+ * past the TTL. Pruning walks records oldest-write-first and stops at the
83
+ * first one inside the TTL, so it assumes each `createdAt` is the time of
84
+ * its write, as `DirectClient.login` sets it.
85
+ */
70
86
  set(state, value) {
87
+ this.#pruneExpired();
88
+ this.#records.delete(state);
71
89
  this.#records.set(state, value);
72
90
  }
73
91
  /** Removes the record for {@linkcode state}. */
@@ -78,4 +96,14 @@ export class MemoryAuthRequestStorage {
78
96
  clear() {
79
97
  this.#records.clear();
80
98
  }
99
+ #pruneExpired() {
100
+ if (this.#ttlMs === Infinity)
101
+ return;
102
+ const cutoff = Date.now() - this.#ttlMs;
103
+ for (const [state, record] of this.#records) {
104
+ if (record.createdAt >= cutoff)
105
+ return;
106
+ this.#records.delete(state);
107
+ }
108
+ }
81
109
  }
package/esm/errors.d.ts CHANGED
@@ -28,10 +28,9 @@ export interface OAuth2ProblemDetailsExtensions {
28
28
  error_uri?: string;
29
29
  }
30
30
  /**
31
- * Constructor type for OAuth2 error classes produced by
32
- * `createHttpErrorClass`. Declared explicitly so the exported error classes
33
- * have a JSR-fast public type (the inferred type spans multiple constructor
34
- * overloads and trips the `no-slow-types` rule).
31
+ * Constructor type shared by every OAuth2 error class in this module: each
32
+ * takes an optional status, message, and `HttpErrorOptions` (whose
33
+ * `extensions` carry the OAuth2 fields) and builds an {@linkcode OAuth2Error}.
35
34
  */
36
35
  export type OAuth2ErrorClass<Extensions extends OAuth2ProblemDetailsExtensions = OAuth2ProblemDetailsExtensions> = {
37
36
  new <E extends Extensions = Extensions>(status?: number, message?: string, options?: HttpErrorOptions<E>): HttpError<E>;
@@ -53,19 +52,23 @@ export type OAuth2Error = HttpError<OAuth2ProblemDetailsExtensions>;
53
52
  * (e.g. {@linkcode InvalidRequestError}) when the condition is known.
54
53
  */
55
54
  export declare const OAuth2Error: OAuth2ErrorClass;
56
- /** Type guard for OAuth2 errors. */
55
+ /**
56
+ * Type guard for OAuth2 errors: true for any `HttpError` whose
57
+ * `extensions.error` is set, whichever class constructed it.
58
+ */
57
59
  export declare function isOAuth2Error(value: unknown): value is OAuth2Error;
58
60
  /**
59
61
  * Converts an arbitrary error into an {@link OAuth2Error}.
60
62
  *
61
- * If the input is already an OAuth2Error it is returned as-is; otherwise it is
62
- * wrapped in a {@link ServerError} with the original error attached as `cause`.
63
+ * An input that is already an OAuth2Error is returned as the same object;
64
+ * anything else is wrapped in a {@link ServerError} with the original error
65
+ * attached as `cause`.
63
66
  *
64
- * The returned error also has its `type` and `extensions.error_uri` synced so
65
- * that if one is set the other is too. This lets both the standard OAuth2
66
- * error format (which emits `error_uri`) and the RFC 9457 Problem Details
67
- * format (which emits `type`) carry the same URI without the handler having to
68
- * set both fields.
67
+ * Either way, `type` and `extensions.error_uri` are synced so that if one is
68
+ * set the other is too — **mutating an OAuth2Error input in place**. This lets
69
+ * both the standard OAuth2 error format (which emits `error_uri`) and the
70
+ * RFC 9457 Problem Details format (which emits `type`) carry the same URI
71
+ * without the handler having to set both fields.
69
72
  */
70
73
  export declare function toOAuth2Error(error: unknown): OAuth2Error;
71
74
  /**
package/esm/errors.js CHANGED
@@ -24,21 +24,25 @@ export const OAuth2Error = createHttpErrorClass({
24
24
  error: "server_error",
25
25
  },
26
26
  });
27
- /** Type guard for OAuth2 errors. */
27
+ /**
28
+ * Type guard for OAuth2 errors: true for any `HttpError` whose
29
+ * `extensions.error` is set, whichever class constructed it.
30
+ */
28
31
  export function isOAuth2Error(value) {
29
32
  return value instanceof HttpError && value.extensions.error !== undefined;
30
33
  }
31
34
  /**
32
35
  * Converts an arbitrary error into an {@link OAuth2Error}.
33
36
  *
34
- * If the input is already an OAuth2Error it is returned as-is; otherwise it is
35
- * wrapped in a {@link ServerError} with the original error attached as `cause`.
37
+ * An input that is already an OAuth2Error is returned as the same object;
38
+ * anything else is wrapped in a {@link ServerError} with the original error
39
+ * attached as `cause`.
36
40
  *
37
- * The returned error also has its `type` and `extensions.error_uri` synced so
38
- * that if one is set the other is too. This lets both the standard OAuth2
39
- * error format (which emits `error_uri`) and the RFC 9457 Problem Details
40
- * format (which emits `type`) carry the same URI without the handler having to
41
- * set both fields.
41
+ * Either way, `type` and `extensions.error_uri` are synced so that if one is
42
+ * set the other is too — **mutating an OAuth2Error input in place**. This lets
43
+ * both the standard OAuth2 error format (which emits `error_uri`) and the
44
+ * RFC 9457 Problem Details format (which emits `type`) carry the same URI
45
+ * without the handler having to set both fields.
42
46
  */
43
47
  export function toOAuth2Error(error) {
44
48
  const oauth2Error = isOAuth2Error(error)
@@ -5,8 +5,7 @@
5
5
  * The library owns the **token lifecycle** ({@link TokenFlowService}); your app
6
6
  * owns **transport** — SMTP, SES, Resend, Twilio, etc. So delivery is a set of
7
7
  * app-supplied async hooks, and the package ships URL builders so apps stop
8
- * hand-assembling `${protocol}://${host}/verify-email?token=…` strings. This is
9
- * the Better Auth model (`sendVerificationEmail` / `sendResetPassword`).
8
+ * hand-assembling `${protocol}://${host}/verify-email?token=…` strings.
10
9
  *
11
10
  * @module
12
11
  */
@@ -15,9 +14,10 @@ export interface DeliveryMessage {
15
14
  /** Recipient — an email address or phone number. */
16
15
  to: string;
17
16
  /**
18
- * The ready-to-use action link (already carrying the token), when the caller
19
- * built one via {@link buildVerificationUrl} / {@link buildResetUrl}. Absent
20
- * if you'd rather build the message body from `token` yourself.
17
+ * The ready-to-use action link (already carrying the token).
18
+ * `IdentityService` sets it only when configured with a `baseUrl`, building
19
+ * it with the matching `build*Url` helper at its default path; otherwise it
20
+ * is absent and you build the link from `token` yourself.
21
21
  */
22
22
  url?: string;
23
23
  /** The raw token, for apps that compose their own URL or message. */
@@ -47,8 +47,8 @@ export interface CodeDeliveryMessage {
47
47
  /** A delivery transport callback for one-time codes. */
48
48
  export type CodeDeliveryHook = (message: CodeDeliveryMessage) => Promise<void> | void;
49
49
  /**
50
- * App-supplied delivery transports. Omit a hook to disable that message type.
51
- * The package calls the relevant hook after minting the token; you send it.
50
+ * App-supplied delivery transports. Omit a hook and that message is never sent
51
+ * (the flow still mints and stores the token or code).
52
52
  *
53
53
  * Trigger point `delivery` (see `docs/trigger-points.md`). The package calls a
54
54
  * hook after minting the credential; a **throw** (e.g. a mailer outage) is
@@ -5,8 +5,7 @@
5
5
  * The library owns the **token lifecycle** ({@link TokenFlowService}); your app
6
6
  * owns **transport** — SMTP, SES, Resend, Twilio, etc. So delivery is a set of
7
7
  * app-supplied async hooks, and the package ships URL builders so apps stop
8
- * hand-assembling `${protocol}://${host}/verify-email?token=…` strings. This is
9
- * the Better Auth model (`sendVerificationEmail` / `sendResetPassword`).
8
+ * hand-assembling `${protocol}://${host}/verify-email?token=…` strings.
10
9
  *
11
10
  * @module
12
11
  */
@@ -6,9 +6,12 @@
6
6
  * @module
7
7
  */
8
8
  /**
9
- * Stable, machine-readable identity error codes. Most are raised by your store
10
- * or the service; `forbidden_origin` is raised by the Hono route factory's
11
- * same-origin guard before a handler runs.
9
+ * Stable, machine-readable identity error codes. The package raises
10
+ * `rate_limited`, `weak_password`, and the `mfa_*` codes, and the Hono route
11
+ * factory answers `forbidden_origin` (its same-origin guard, before a handler
12
+ * runs) and `invalid_credentials` / `invalid_token` (a failed sign-in or token
13
+ * check); the rest are for your store and routes to throw (e.g. `identifier_taken` on
14
+ * a duplicate sign-up, `captcha_failed` after `verifyCaptcha`).
12
15
  */
13
16
  export type IdentityErrorCode = "invalid_credentials" | "invalid_token" | "rate_limited" | "weak_password" | "identifier_taken" | "email_not_verified" | "mfa_not_enrolled" | "mfa_already_enrolled" | "captcha_failed" | "forbidden_origin";
14
17
  /** Options for {@link IdentityError}. */
@@ -31,7 +34,10 @@ export declare class IdentityError extends Error {
31
34
  /** Builds an identity error; `message` defaults to the `code` when omitted. */
32
35
  constructor(code: IdentityErrorCode, message?: string, options?: IdentityErrorOptions);
33
36
  }
34
- /** Type guard for {@link IdentityError}. */
37
+ /**
38
+ * Type guard for {@link IdentityError}. An `instanceof` check, so an error from
39
+ * another realm or a second copy of the package fails it; compare `name` there.
40
+ */
35
41
  export declare function isIdentityError(value: unknown): value is IdentityError;
36
42
  /** The HTTP statuses an identity error maps to (a `ContentfulStatusCode` union). */
37
43
  export type IdentityErrorStatus = 400 | 401 | 403 | 409 | 422 | 429;
@@ -24,7 +24,10 @@ export class IdentityError extends Error {
24
24
  this.retryAfterMs = options?.retryAfterMs;
25
25
  }
26
26
  }
27
- /** Type guard for {@link IdentityError}. */
27
+ /**
28
+ * Type guard for {@link IdentityError}. An `instanceof` check, so an error from
29
+ * another realm or a second copy of the package fails it; compare `name` there.
30
+ */
28
31
  export function isIdentityError(value) {
29
32
  return value instanceof IdentityError;
30
33
  }
@@ -13,8 +13,10 @@
13
13
  * Some outcomes are *only* visible here, because the flow that produced them
14
14
  * resolves uniformly by design: `delivery.failed` is the sole in-band signal
15
15
  * that a message never went out (and, when `invalidated` is `false`, that a
16
- * live credential the user never received is still outstanding). Treat this
17
- * hook as a monitored surface, not just an audit log.
16
+ * live credential the user never received is still outstanding), and
17
+ * `credential_mint.failed` the sole signal that a token or code store outage
18
+ * kept one from being minted at all. Treat this hook as a monitored surface,
19
+ * not just an audit log.
18
20
  *
19
21
  * Events describe internal outcomes (including whether an identifier resolved
20
22
  * to an account), so they are for server-side capture only — never surface
@@ -26,8 +28,9 @@ import type { DeliveryHooks } from "./delivery.js";
26
28
  /** Why a sign-in attempt failed. Internal detail — never shown to the user. */
27
29
  export type SignInFailureReason = "unknown_identifier" | "no_password" | "wrong_password";
28
30
  /**
29
- * Why a sign-in code verification failed. `unknown_email` is internal-only
30
- * detail — the caller-facing result reports it as `invalid`.
31
+ * Why a sign-in code verification failed. Internal detail — never shown to the
32
+ * user: `IdentityService.verifySignInCode` reports every one of these as
33
+ * `invalid`.
31
34
  */
32
35
  export type SignInCodeFailureReason = "invalid" | "expired" | "locked" | "unknown_email";
33
36
  /**
@@ -122,9 +125,22 @@ export type IdentityEvent = {
122
125
  * `"session_revocation_failed"` — the new password was set but the
123
126
  * configured {@link RevocableSessionService} threw before the user's other
124
127
  * sessions could be revoked, so the reset is reported failed (and rethrown)
125
- * rather than trusted; retry it so the still-live sessions are cleared.
128
+ * rather than trusted. The reset token is already spent, so clear the
129
+ * still-live sessions by calling `revokeAllByUser` again from this event,
130
+ * or have the user complete a fresh reset link.
126
131
  */
127
132
  reason: "invalid_token" | "session_revocation_failed";
133
+ } | {
134
+ type: "credential_mint.failed";
135
+ /**
136
+ * Which enumeration-safe request flow could not mint its credential
137
+ * because the token or code store threw. The call still resolved `void`,
138
+ * exactly as it does for an unknown email, so this event is the only
139
+ * signal that the user was never sent anything.
140
+ */
141
+ flow: "password_reset" | "signin_link" | "signin_code";
142
+ /** The store's failure message. */
143
+ error: string;
128
144
  } | {
129
145
  type: "email_verification.requested";
130
146
  userId: string;
@@ -13,8 +13,10 @@
13
13
  * Some outcomes are *only* visible here, because the flow that produced them
14
14
  * resolves uniformly by design: `delivery.failed` is the sole in-band signal
15
15
  * that a message never went out (and, when `invalidated` is `false`, that a
16
- * live credential the user never received is still outstanding). Treat this
17
- * hook as a monitored surface, not just an audit log.
16
+ * live credential the user never received is still outstanding), and
17
+ * `credential_mint.failed` the sole signal that a token or code store outage
18
+ * kept one from being minted at all. Treat this hook as a monitored surface,
19
+ * not just an audit log.
18
20
  *
19
21
  * Events describe internal outcomes (including whether an identifier resolved
20
22
  * to an account), so they are for server-side capture only — never surface
@@ -3,10 +3,7 @@
3
3
  *
4
4
  * Not part of the public API — reach for a connector from
5
5
  * `@udibo/oauth2/identity/external` instead, which re-exports the one type an
6
- * adopter configures ({@link AzpPolicy}). These live here so the same failure
7
- * reads the same way whichever connector hit it: one error renderer, one
8
- * error-body reader, one provider-text sanitizer, and one id_token claim gate
9
- * rather than a copy per provider. The token exchange itself is
6
+ * adopter configures ({@link AzpPolicy}). The token exchange is
10
7
  * `_token-exchange.ts`.
11
8
  *
12
9
  * @module
@@ -75,9 +72,10 @@ export interface IdTokenClaimsInput {
75
72
  leewaySeconds?: number;
76
73
  }
77
74
  /**
78
- * Enforces the id_token claim rules of OpenID Connect Core §3.1.3.7 that do not
79
- * depend on the signature: `iss`, `aud`, `azp`, `exp`, and `nonce`, in that
80
- * order. Callers verify the signature (or establish the token's provenance)
75
+ * Enforces these OpenID Connect Core §3.1.3.7 claim checks, in order: `iss`,
76
+ * `aud`, `azp`, `exp` (a missing `exp` fails), and `nonce` (only when
77
+ * `expectedNonce` is given). `iat`, `acr`, and `auth_time` are not checked.
78
+ * Callers verify the signature (or establish the token's provenance)
81
79
  * themselves before calling this.
82
80
  *
83
81
  * Throws {@link ExternalAuthError} with code `nonce_mismatch` when the `nonce`
@@ -3,10 +3,7 @@
3
3
  *
4
4
  * Not part of the public API — reach for a connector from
5
5
  * `@udibo/oauth2/identity/external` instead, which re-exports the one type an
6
- * adopter configures ({@link AzpPolicy}). These live here so the same failure
7
- * reads the same way whichever connector hit it: one error renderer, one
8
- * error-body reader, one provider-text sanitizer, and one id_token claim gate
9
- * rather than a copy per provider. The token exchange itself is
6
+ * adopter configures ({@link AzpPolicy}). The token exchange is
10
7
  * `_token-exchange.ts`.
11
8
  *
12
9
  * @module
@@ -61,9 +58,10 @@ export function stripTrailingSlash(value) {
61
58
  return value.endsWith("/") ? value.slice(0, -1) : value;
62
59
  }
63
60
  /**
64
- * Enforces the id_token claim rules of OpenID Connect Core §3.1.3.7 that do not
65
- * depend on the signature: `iss`, `aud`, `azp`, `exp`, and `nonce`, in that
66
- * order. Callers verify the signature (or establish the token's provenance)
61
+ * Enforces these OpenID Connect Core §3.1.3.7 claim checks, in order: `iss`,
62
+ * `aud`, `azp`, `exp` (a missing `exp` fails), and `nonce` (only when
63
+ * `expectedNonce` is given). `iat`, `acr`, and `auth_time` are not checked.
64
+ * Callers verify the signature (or establish the token's provenance)
67
65
  * themselves before calling this.
68
66
  *
69
67
  * Throws {@link ExternalAuthError} with code `nonce_mismatch` when the `nonce`
@@ -3,11 +3,8 @@
3
3
  * through — {@link githubProvider}, {@link oauth2Provider} (and its
4
4
  * {@link discordProvider} preset), and {@link appleProvider}.
5
5
  * {@link oidcProvider}, and {@link googleProvider} on top of it, exchange via
6
- * `OAuth2Client` instead and do not reach this module.
7
- *
8
- * Not part of the public API. Split out of `_shared.ts` so the ladder — the one
9
- * place a provider's bytes become an error message or a token — sits next to
10
- * the tests that pin it.
6
+ * `DirectClient` instead and do not reach this module. Not part of the public
7
+ * API.
11
8
  *
12
9
  * @module
13
10
  */
@@ -35,7 +32,7 @@ export interface TokenExchangeInput {
35
32
  * `RequestInit`** — `redirect` and `signal` in particular. They are the only
36
33
  * channel the redirect refusal and the deadline reach the transport, so a
37
34
  * wrapper that rebuilds `init` from scratch silently reopens the credential
38
- * replay hole. `createGuardedFetch` forwards both.
35
+ * replay hole.
39
36
  */
40
37
  fetch: typeof fetch;
41
38
  /** Guidance appended when a failure carries no OAuth2 error code. */
@@ -3,11 +3,8 @@
3
3
  * through — {@link githubProvider}, {@link oauth2Provider} (and its
4
4
  * {@link discordProvider} preset), and {@link appleProvider}.
5
5
  * {@link oidcProvider}, and {@link googleProvider} on top of it, exchange via
6
- * `OAuth2Client` instead and do not reach this module.
7
- *
8
- * Not part of the public API. Split out of `_shared.ts` so the ladder — the one
9
- * place a provider's bytes become an error message or a token — sits next to
10
- * the tests that pin it.
6
+ * `DirectClient` instead and do not reach this module. Not part of the public
7
+ * API.
11
8
  *
12
9
  * @module
13
10
  */
@@ -38,8 +38,8 @@ export interface AppleClientSecretOptions {
38
38
  privateKey: string;
39
39
  /**
40
40
  * Lifetime of the generated secret in seconds. Defaults to
41
- * {@link DEFAULT_APPLE_CLIENT_SECRET_TTL_SECONDS}; must not exceed
42
- * {@link APPLE_CLIENT_SECRET_MAX_TTL_SECONDS}.
41
+ * {@link DEFAULT_APPLE_CLIENT_SECRET_TTL_SECONDS}; must be a finite number
42
+ * that does not exceed {@link APPLE_CLIENT_SECRET_MAX_TTL_SECONDS}.
43
43
  */
44
44
  expiresInSeconds?: number;
45
45
  /** Provider id used in error messages. Defaults to `"apple"`. */
@@ -54,7 +54,8 @@ export interface AppleClientSecretOptions {
54
54
  *
55
55
  * @throws {ExternalAuthError} `configuration` naming the offending input when
56
56
  * the team id / key id / client id is empty, the `.p8` is not a valid P-256
57
- * key, or the requested TTL exceeds Apple's cap.
57
+ * key, or the requested TTL is not a finite number between 1 and Apple's
58
+ * cap (`NaN` and `Infinity` are rejected, never signed into `exp`).
58
59
  *
59
60
  * @example
60
61
  * ```ts
@@ -67,15 +68,19 @@ export interface AppleClientSecretOptions {
67
68
  * ```
68
69
  */
69
70
  export declare function generateAppleClientSecret(options: AppleClientSecretOptions): Promise<string>;
70
- /** A cached client-secret source: returns a valid secret, re-signing lazily. */
71
+ /**
72
+ * Returns the client-secret JWT to present on a token exchange; called once per
73
+ * exchange. {@link createAppleClientSecretFactory} builds the caching one.
74
+ */
71
75
  export type AppleClientSecretFactory = () => Promise<string>;
72
76
  /**
73
77
  * Build a self-renewing Apple client-secret source. It signs a secret with
74
78
  * {@link generateAppleClientSecret} on first call, caches it, and re-signs
75
- * automatically once the cached secret is within `renewBeforeSeconds` of expiry
76
- * — so a long-lived {@link appleProvider} never presents an expired assertion.
77
- * The key material is validated on the first call, surfacing a misconfiguration
78
- * as a `configuration` {@link ExternalAuthError}.
79
+ * automatically once the cached secret is within `renewBeforeSeconds` (default
80
+ * one hour) of expiry — so a long-lived {@link appleProvider} never presents
81
+ * an expired assertion. The key material is validated on the first call, not
82
+ * at construction, surfacing a misconfiguration as a `configuration`
83
+ * {@link ExternalAuthError}; a failed call caches nothing.
79
84
  */
80
85
  export declare function createAppleClientSecretFactory(options: AppleClientSecretOptions & {
81
86
  renewBeforeSeconds?: number;