@korajs/auth 1.0.0-beta.12 → 1.0.0-beta.14

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 (101) hide show
  1. package/README.md +52 -47
  2. package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
  3. package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
  4. package/dist/index.cjs +645 -150
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -9
  7. package/dist/index.d.ts +27 -9
  8. package/dist/index.js +644 -150
  9. package/dist/index.js.map +1 -1
  10. package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
  11. package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
  12. package/dist/react.d.cts +2 -2
  13. package/dist/react.d.ts +2 -2
  14. package/dist/server.cjs +2880 -1667
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +810 -169
  17. package/dist/server.d.ts +810 -169
  18. package/dist/server.js +2848 -1646
  19. package/dist/server.js.map +1 -1
  20. package/dist/svelte.cjs +2 -2
  21. package/dist/svelte.cjs.map +1 -1
  22. package/dist/svelte.d.cts +2 -2
  23. package/dist/svelte.d.ts +2 -2
  24. package/dist/svelte.js +2 -2
  25. package/dist/svelte.js.map +1 -1
  26. package/dist/vue.d.cts +1 -1
  27. package/dist/vue.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/src/admin/admin-api.ts +327 -0
  30. package/src/admin/audit-log.ts +324 -0
  31. package/src/admin/webhooks.ts +576 -0
  32. package/src/bindings/create-auth-session.ts +184 -0
  33. package/src/bindings/create-org-session.ts +130 -0
  34. package/src/client/auth-client.ts +1592 -0
  35. package/src/client/auth-sync.ts +213 -0
  36. package/src/client/device-session.ts +104 -0
  37. package/src/client/org-client.ts +399 -0
  38. package/src/client/quickstart.ts +108 -0
  39. package/src/client/storage.ts +94 -0
  40. package/src/device/device-identity.ts +330 -0
  41. package/src/device/device-store.ts +379 -0
  42. package/src/encryption/auto-lock.ts +170 -0
  43. package/src/encryption/database-encryption.ts +265 -0
  44. package/src/encryption/key-derivation.ts +149 -0
  45. package/src/encryption/operation-encryptor.ts +361 -0
  46. package/src/index.ts +132 -0
  47. package/src/mfa/totp.ts +826 -0
  48. package/src/org/org-routes.ts +758 -0
  49. package/src/org/org-store.ts +490 -0
  50. package/src/org/org-types.ts +230 -0
  51. package/src/passkey/passkey-client.ts +597 -0
  52. package/src/passkey/passkey-server.ts +779 -0
  53. package/src/postgres/ensure-schema.ts +65 -0
  54. package/src/provider/adapter.ts +246 -0
  55. package/src/provider/built-in/auth-routes.ts +1313 -0
  56. package/src/provider/built-in/email-verification.ts +303 -0
  57. package/src/provider/built-in/password-hash.ts +118 -0
  58. package/src/provider/built-in/password-reset.ts +416 -0
  59. package/src/provider/built-in/postgres-user-store.ts +365 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +335 -0
  62. package/src/provider/built-in/sync-scopes.ts +85 -0
  63. package/src/provider/built-in/user-store.ts +465 -0
  64. package/src/provider/external/clerk-adapter.ts +157 -0
  65. package/src/provider/external/external-jwt-provider.ts +491 -0
  66. package/src/provider/external/supabase-adapter.ts +163 -0
  67. package/src/provider/oauth/linked-identity-store.ts +108 -0
  68. package/src/provider/oauth/oauth-flow.ts +550 -0
  69. package/src/provider/oauth/oauth-types.ts +184 -0
  70. package/src/provider/oauth/postgres-oauth-store.ts +296 -0
  71. package/src/provider/oauth/sqlite-oauth-store.ts +285 -0
  72. package/src/rbac/rbac-engine.ts +323 -0
  73. package/src/rbac/rbac-types.ts +210 -0
  74. package/src/rbac/scope-resolver.ts +140 -0
  75. package/src/react/AuthProvider.tsx +97 -0
  76. package/src/react/OrgProvider.tsx +41 -0
  77. package/src/react/auth-context.ts +26 -0
  78. package/src/react/hooks.ts +110 -0
  79. package/src/react/org-hooks.ts +214 -0
  80. package/src/react.ts +26 -0
  81. package/src/server.ts +338 -0
  82. package/src/session/session.ts +401 -0
  83. package/src/svelte/auth-context.ts +50 -0
  84. package/src/svelte/org-context.ts +32 -0
  85. package/src/svelte/org-hooks.ts +201 -0
  86. package/src/svelte/use-auth.ts +115 -0
  87. package/src/svelte.ts +25 -0
  88. package/src/tokens/encrypted-token-store.ts +360 -0
  89. package/src/tokens/jwt.ts +236 -0
  90. package/src/tokens/postgres-token-revocation-store.ts +140 -0
  91. package/src/tokens/sqlite-token-revocation-store.ts +121 -0
  92. package/src/tokens/token-manager.ts +821 -0
  93. package/src/tokens/token-store.ts +192 -0
  94. package/src/types.ts +394 -0
  95. package/src/vue/auth-context.ts +10 -0
  96. package/src/vue/auth-provider-types.ts +5 -0
  97. package/src/vue/auth-provider.ts +76 -0
  98. package/src/vue/org-hooks.ts +193 -0
  99. package/src/vue/org-provider.ts +49 -0
  100. package/src/vue/use-auth.ts +139 -0
  101. package/src/vue.ts +10 -0
package/dist/server.d.ts CHANGED
@@ -1,65 +1,105 @@
1
- import { A as AuthTokens, T as TokenPayload } from './operation-encryptor-DRmKNWpF.js';
2
- export { O as OperationEncryptionError, e as OperationEncryptor, f as OperationEncryptorConfig, h as computePublicKeyThumbprint, l as isEncryptedField, v as verifyChallenge } from './operation-encryptor-DRmKNWpF.js';
3
- import { KoraError } from '@korajs/core';
1
+ import { A as AuthTokens, T as TokenPayload } from './operation-encryptor-DDdlb9bm.js';
2
+ export { O as OperationEncryptionError, e as OperationEncryptor, f as OperationEncryptorConfig, h as computePublicKeyThumbprint, l as isEncryptedField, v as verifyChallenge } from './operation-encryptor-DDdlb9bm.js';
3
+ import { ScopeMap, KoraError } from '@korajs/core';
4
4
 
5
+ /**
6
+ * Default window during which a just-rotated refresh token may be presented
7
+ * once more and receive the SAME successor pair (NEW-AUTH-3). Covers a rotation
8
+ * response lost on a flaky network without opening a long replay window.
9
+ */
10
+ declare const DEFAULT_REFRESH_REUSE_GRACE_MS = 30000;
11
+ /**
12
+ * Payload of an `mfa_pending` token: proof that the first factor succeeded,
13
+ * accepted ONLY by the MFA verification step and rejected everywhere else
14
+ * (`validateToken` does not recognise its type).
15
+ */
16
+ interface MfaPendingPayload {
17
+ jti: string;
18
+ sub: string;
19
+ dev: string;
20
+ type: 'mfa_pending';
21
+ iat: number;
22
+ exp: number;
23
+ /** Methods already satisfied (for example `['pwd']`). */
24
+ amr: string[];
25
+ }
26
+ /** Result of an atomic {@link TokenRevocationStore.consume}. */
27
+ interface ConsumeResult {
28
+ /** True only for the single call that consumed the id first. */
29
+ firstUse: boolean;
30
+ /** When the id was first consumed (milliseconds since epoch). */
31
+ consumedAt: number;
32
+ }
5
33
  /**
6
34
  * Interface for server-side token revocation storage.
7
35
  *
8
36
  * Implementing this interface allows the TokenManager to:
9
- * - Revoke individual tokens by their `jti`
10
- * - Detect refresh token reuse (potential theft indicator)
11
- * - Invalidate all tokens for a specific device on revocation
37
+ * - Revoke individual tokens (and token families) by id
38
+ * - Rotate refresh tokens atomically (each refresh token mints at most one successor)
39
+ * - Invalidate every credential a device or a user obtained before a point in time
12
40
  *
13
- * The in-memory implementation ({@link InMemoryTokenRevocationStore}) is suitable
14
- * for development. Production deployments should use a persistent store (Redis, database).
41
+ * Shipped implementations: {@link InMemoryTokenRevocationStore} (development),
42
+ * `SqliteTokenRevocationStore` and `PostgresTokenRevocationStore`. The SQLite and
43
+ * Postgres user stores expose one sharing their database through
44
+ * `getTokenRevocationStore()`, which `createKoraAuthServer` uses by default.
15
45
  */
16
46
  interface TokenRevocationStore {
17
47
  /**
18
- * Check whether a token has been revoked.
19
- * @param jti - The JWT ID to check
20
- * @returns true if the token has been revoked
48
+ * Check whether a token (or token family, keyed `family:<id>`) has been revoked.
49
+ * @param jti - The JWT ID (or family key) to check
21
50
  */
22
51
  isRevoked(jti: string): Promise<boolean>;
23
52
  /**
24
- * Revoke a specific token by its JWT ID.
25
- * @param jti - The JWT ID to revoke
26
- * @param expiresAt - The token's original expiration time (seconds since epoch).
27
- * The store may use this to auto-purge expired revocations.
53
+ * Revoke a specific token (or token family) by id.
54
+ * @param jti - The JWT ID (or family key) to revoke
55
+ * @param expiresAt - Expiry in seconds since epoch; the store may purge after it
28
56
  */
29
57
  revoke(jti: string, expiresAt: number): Promise<void>;
30
58
  /**
31
- * Revoke all tokens associated with a specific device.
32
- * Called when a device is revoked to invalidate all its tokens.
33
- * @param deviceId - The device ID whose tokens should be revoked
59
+ * Atomically mark an id as consumed (test-and-set). Exactly one concurrent
60
+ * caller observes `firstUse: true`, on every instance sharing the store.
61
+ * @param jti - The id to consume
62
+ * @param expiresAt - Expiry in seconds since epoch; the store may purge after it
34
63
  */
35
- revokeAllForDevice(deviceId: string): Promise<void>;
64
+ consume(jti: string, expiresAt: number): Promise<ConsumeResult>;
65
+ /** Whether an id has been consumed (read-only; never consumes). */
66
+ isConsumed(jti: string): Promise<boolean>;
36
67
  /**
37
- * Check whether all tokens for a device have been revoked.
38
- * @param deviceId - The device ID to check
39
- * @returns true if the device's token family has been revoked
68
+ * Reject every token for a device issued at or before `before` (ms). Later
69
+ * tokens (a fresh sign-in on the same device) stay valid. Monotonic: a
70
+ * smaller `before` never lowers an existing cut-off.
40
71
  */
41
- isDeviceRevoked(deviceId: string): Promise<boolean>;
72
+ revokeAllForDevice(deviceId: string, before?: number): Promise<void>;
73
+ /** Cut-off (ms) recorded by {@link revokeAllForDevice}, or null. */
74
+ getDeviceRevokedBefore(deviceId: string): Promise<number | null>;
75
+ /**
76
+ * Reject every token for a user issued at or before `before` (ms). Used by
77
+ * password reset and change, admin session revocation and user deletion.
78
+ */
79
+ revokeAllForUser(userId: string, before?: number): Promise<void>;
80
+ /** Cut-off (ms) recorded by {@link revokeAllForUser}, or null. */
81
+ getUserRevokedBefore(userId: string): Promise<number | null>;
42
82
  }
43
83
  /**
44
84
  * In-memory token revocation store.
45
85
  *
46
- * Suitable for development and testing. Revoked tokens are stored in a Set
47
- * and automatically cleaned up when they would have expired naturally.
48
- *
49
- * **Not suitable for production**: revocations are lost on server restart
50
- * and not shared across server instances. Use a Redis or database-backed
51
- * store in production.
86
+ * Suitable for development and testing. Revocations are lost on restart and not
87
+ * shared across instances, so `createKoraAuthServer` refuses it in production
88
+ * unless `allowInMemory: true` is set.
52
89
  */
53
90
  declare class InMemoryTokenRevocationStore implements TokenRevocationStore {
54
91
  private readonly revokedTokens;
55
- private readonly revokedDevices;
92
+ private readonly consumed;
93
+ private readonly deviceCutoffs;
94
+ private readonly userCutoffs;
56
95
  isRevoked(jti: string): Promise<boolean>;
57
96
  revoke(jti: string, expiresAt: number): Promise<void>;
58
- revokeAllForDevice(deviceId: string): Promise<void>;
59
- /**
60
- * Check if a device has been revoked.
61
- */
62
- isDeviceRevoked(deviceId: string): Promise<boolean>;
97
+ consume(jti: string, expiresAt: number): Promise<ConsumeResult>;
98
+ isConsumed(jti: string): Promise<boolean>;
99
+ revokeAllForDevice(deviceId: string, before?: number): Promise<void>;
100
+ getDeviceRevokedBefore(deviceId: string): Promise<number | null>;
101
+ revokeAllForUser(userId: string, before?: number): Promise<void>;
102
+ getUserRevokedBefore(userId: string): Promise<number | null>;
63
103
  /**
64
104
  * Remove expired revocations to prevent unbounded memory growth.
65
105
  * Call periodically (e.g., every hour) in long-running servers.
@@ -78,8 +118,6 @@ interface TokenManagerConfig {
78
118
  *
79
119
  * For key rotation, provide an array of secrets. The first secret is used for
80
120
  * signing new tokens; all secrets are tried during verification (newest first).
81
- * This allows graceful rotation: add the new secret at index 0, then remove
82
- * the old secret after all tokens signed with it have expired.
83
121
  */
84
122
  secret: string | string[];
85
123
  /** Access token lifetime in milliseconds (default: 15 minutes) */
@@ -89,22 +127,48 @@ interface TokenManagerConfig {
89
127
  /** Device credential lifetime in milliseconds (default: 90 days) */
90
128
  deviceCredentialLifetime?: number;
91
129
  /**
92
- * Optional token revocation store. When provided, enables:
93
- * - Individual token revocation via `revokeToken()`
94
- * - Refresh token reuse detection (consumed tokens are tracked)
95
- * - Device-level token invalidation via `revokeDeviceTokens()`
96
- *
130
+ * Optional token revocation store. When provided, enables revocation,
131
+ * atomic refresh rotation with reuse detection, and device/user cut-offs.
97
132
  * Without a revocation store, tokens are valid until they expire.
98
133
  */
99
134
  revocationStore?: TokenRevocationStore;
135
+ /**
136
+ * Window (ms) during which a just-rotated refresh token is accepted ONCE more
137
+ * and returns the same successor pair. Set 0 to disable. Default 30 seconds.
138
+ */
139
+ refreshReuseGraceMs?: number;
140
+ }
141
+ /** Options for minting a token set. */
142
+ interface IssueTokenOptions {
143
+ /** Refresh-token family to continue. A new family is started when omitted. */
144
+ family?: string;
145
+ /** Authentication methods (RFC 8176) to record, for example `['pwd', 'otp']`. */
146
+ amr?: string[];
100
147
  }
148
+ /** Why a refresh was refused. */
149
+ type RefreshFailureReason = 'invalid' | 'revoked' | 'reused' | 'in_progress' | 'device_revoked' | 'user_revoked';
150
+ /** Outcome of {@link TokenManager.rotateRefreshToken}. */
151
+ type RefreshResult = {
152
+ ok: true;
153
+ tokens: {
154
+ accessToken: string;
155
+ refreshToken: string;
156
+ };
157
+ /** The verified payload of the refresh token that was presented. */
158
+ payload: TokenPayload;
159
+ /** True when this was the one grace replay of a just-rotated token. */
160
+ replayed: boolean;
161
+ } | {
162
+ ok: false;
163
+ reason: RefreshFailureReason;
164
+ };
101
165
  /**
102
166
  * Server-side token manager responsible for issuing, refreshing, and validating
103
167
  * Kora authentication tokens.
104
168
  *
105
169
  * Uses HMAC-SHA256 signed JWTs with unique `jti` identifiers for every token.
106
- * Supports key rotation (multiple secrets), token revocation, and refresh token
107
- * reuse detection.
170
+ * Supports key rotation (multiple secrets), token revocation, token families and
171
+ * atomic, rotation-safe refresh.
108
172
  *
109
173
  * @example
110
174
  * ```typescript
@@ -113,14 +177,9 @@ interface TokenManagerConfig {
113
177
  * revocationStore: new InMemoryTokenRevocationStore(),
114
178
  * })
115
179
  *
116
- * // Issue all tokens at once
117
180
  * const tokens = tokenManager.issueTokens('user-123', 'device-456')
118
- *
119
- * // Validate an access token
120
- * const payload = tokenManager.validateToken(tokens.accessToken)
121
- *
122
- * // Refresh when the access token expires
123
- * const newTokens = await tokenManager.refreshAccessToken(tokens.refreshToken)
181
+ * const payload = await tokenManager.validateTokenWithRevocation(tokens.accessToken)
182
+ * const next = await tokenManager.rotateRefreshToken(tokens.refreshToken)
124
183
  * ```
125
184
  */
126
185
  declare class TokenManager {
@@ -130,46 +189,38 @@ declare class TokenManager {
130
189
  private readonly refreshTokenLifetime;
131
190
  private readonly deviceCredentialLifetime;
132
191
  private readonly revocationStore;
192
+ private readonly refreshReuseGraceMs;
193
+ /** Refresh jtis whose rotation is executing on this instance right now. */
194
+ private readonly rotating;
133
195
  constructor(config: TokenManagerConfig);
134
196
  /**
135
197
  * Generate a cryptographically random secret suitable for HMAC-SHA256 signing.
136
198
  *
137
- * Returns a 64-character hex string (32 bytes / 256 bits of entropy).
138
- * Store this securely (environment variable, secrets manager) — never in source code.
139
- *
140
199
  * @returns A random 256-bit hex-encoded secret
141
200
  */
142
201
  static generateSecret(): string;
202
+ /** The revocation store this manager enforces, if any. */
203
+ getRevocationStore(): TokenRevocationStore | undefined;
143
204
  /**
144
205
  * Issue a signed JWT access token.
145
206
  *
146
- * Access tokens are short-lived (default 15 minutes) and used to authorize
147
- * API requests. When expired, use {@link refreshAccessToken} with a valid
148
- * refresh token to obtain a new one.
149
- *
150
207
  * @param userId - The subject (user ID) to encode in the token
151
208
  * @param deviceId - The device ID of the requesting device
209
+ * @param options - Family and authentication methods to record
152
210
  * @returns A signed JWT string with type 'access'
153
211
  */
154
- issueAccessToken(userId: string, deviceId: string): string;
212
+ issueAccessToken(userId: string, deviceId: string, options?: IssueTokenOptions): string;
155
213
  /**
156
214
  * Issue a signed JWT refresh token.
157
215
  *
158
- * Refresh tokens are longer-lived (default 90 days) and used exclusively
159
- * to obtain new access tokens via {@link refreshAccessToken}. They should
160
- * be stored securely and never sent to resource APIs.
161
- *
162
216
  * @param userId - The subject (user ID) to encode in the token
163
217
  * @param deviceId - The device ID of the requesting device
218
+ * @param options - Family and authentication methods to record
164
219
  * @returns A signed JWT string with type 'refresh'
165
220
  */
166
- issueRefreshToken(userId: string, deviceId: string): string;
221
+ issueRefreshToken(userId: string, deviceId: string, options?: IssueTokenOptions): string;
167
222
  /**
168
- * Issue a signed device credential token.
169
- *
170
- * Device credentials are long-lived tokens bound to a device's public key.
171
- * They include a `mustCheckinBy` deadline; if the device does not check in
172
- * before this deadline, the credential should be treated as revoked.
223
+ * Issue a signed device credential token bound to a device's public key.
173
224
  *
174
225
  * @param userId - The subject (user ID) to encode in the token
175
226
  * @param deviceId - The device ID of the requesting device
@@ -178,77 +229,192 @@ declare class TokenManager {
178
229
  */
179
230
  issueDeviceCredential(userId: string, deviceId: string, publicKeyThumbprint: string): string;
180
231
  /**
181
- * Issue a complete set of authentication tokens.
182
- *
183
- * Always issues an access token and refresh token. If a `publicKeyThumbprint`
184
- * is provided, also issues a device credential.
232
+ * Issue a complete set of authentication tokens for a new session. The access
233
+ * and refresh token share a fresh family id.
185
234
  *
186
235
  * @param userId - The subject (user ID) to encode in the tokens
187
236
  * @param deviceId - The device ID of the requesting device
188
- * @param publicKeyThumbprint - Optional SHA-256 thumbprint of the device's public key.
189
- * When provided, a device credential is included in the returned tokens.
237
+ * @param publicKeyThumbprint - Optional device key thumbprint; adds a device credential
238
+ * @param options - Authentication methods to record
190
239
  * @returns An {@link AuthTokens} object containing the issued tokens
191
240
  */
192
- issueTokens(userId: string, deviceId: string, publicKeyThumbprint?: string): AuthTokens;
241
+ issueTokens(userId: string, deviceId: string, publicKeyThumbprint?: string, options?: Omit<IssueTokenOptions, 'family'>): AuthTokens;
242
+ /**
243
+ * Issue a short-lived `mfa_pending` token after a successful first factor
244
+ * for a user enrolled in MFA (AUTH-10). It grants nothing by itself.
245
+ *
246
+ * @param userId - The user who passed the first factor
247
+ * @param deviceId - The device the session will be bound to
248
+ * @param amr - Methods already satisfied (for example `['pwd']`)
249
+ * @returns A signed JWT of type `mfa_pending`
250
+ */
251
+ issueMfaPendingToken(userId: string, deviceId: string, amr: string[]): string;
252
+ /**
253
+ * Verify an `mfa_pending` token without consuming it, so a mistyped code can
254
+ * be retried with the same token (TOTP checks have their own backoff).
255
+ *
256
+ * @param token - The `mfa_pending` JWT
257
+ * @returns Its payload, or null when invalid, expired or already redeemed
258
+ */
259
+ verifyMfaPendingToken(token: string): Promise<MfaPendingPayload | null>;
193
260
  /**
194
- * Validate and decode a token.
261
+ * Redeem an `mfa_pending` token exactly once (atomic across instances).
195
262
  *
196
- * Verifies the HMAC-SHA256 signature (trying all configured secrets for key rotation),
197
- * checks that the token has not expired, and validates all required claims.
198
- * Returns null (rather than throwing) for invalid or expired tokens, so callers
199
- * can handle authentication failure without try/catch.
263
+ * @param payload - A payload returned by {@link verifyMfaPendingToken}
264
+ * @returns True only for the single successful redemption
265
+ */
266
+ redeemMfaPendingToken(payload: MfaPendingPayload): Promise<boolean>;
267
+ /**
268
+ * Validate and decode a token's signature, expiry and claims.
269
+ *
270
+ * This does NOT consult revocation. Every request-authorization path must use
271
+ * {@link validateTokenWithRevocation} (or `BuiltInAuthRoutes.authenticateAccess`).
200
272
  *
201
273
  * @param token - The JWT string to validate
202
- * @returns The decoded {@link TokenPayload} if valid, or null if the token is
203
- * invalid, expired, or missing required claims
274
+ * @returns The decoded {@link TokenPayload}, or null if invalid or expired
204
275
  */
205
276
  validateToken(token: string): TokenPayload | null;
206
277
  /**
207
- * Validate a token and check it against the revocation store.
208
- *
209
- * Like {@link validateToken}, but also checks whether the token's `jti` has been
210
- * revoked or belongs to a revoked device. Requires a revocation store to be configured.
278
+ * Validate a token and check every revocation primitive: the token's own
279
+ * `jti`, its family, the device cut-off and the per-user cut-off.
211
280
  *
212
281
  * @param token - The JWT string to validate
213
282
  * @returns The decoded {@link TokenPayload} if valid and not revoked, or null otherwise
214
283
  */
215
284
  validateTokenWithRevocation(token: string): Promise<TokenPayload | null>;
285
+ /**
286
+ * Why an otherwise valid token is no longer accepted, or null when it is.
287
+ */
288
+ revocationReason(payload: TokenPayload): Promise<'revoked' | 'device_revoked' | 'user_revoked' | null>;
216
289
  /**
217
290
  * Revoke a specific token by its JWT ID.
218
291
  *
219
- * Requires a revocation store to be configured. After revocation, the token
220
- * will be rejected by {@link validateTokenWithRevocation}.
221
- *
222
292
  * @param jti - The JWT ID of the token to revoke
223
293
  * @param expiresAt - The token's expiration time (seconds since epoch)
224
294
  */
225
295
  revokeToken(jti: string, expiresAt: number): Promise<void>;
226
296
  /**
227
- * Revoke all tokens for a specific device.
297
+ * Revoke a whole refresh-token family (one sign-in and all its rotations,
298
+ * including the access tokens minted along the way).
228
299
  *
229
- * Called when a device is revoked to ensure all its existing tokens
230
- * (access, refresh, and device credentials) are invalidated.
300
+ * @param family - The family id (`fam` claim)
301
+ * @param expiresAt - Latest expiry of any token in the family (seconds since epoch)
302
+ */
303
+ revokeFamily(family: string, expiresAt: number): Promise<void>;
304
+ /**
305
+ * Revoke every token issued to a device up to now. A later sign-in on the
306
+ * same device issues tokens that are accepted again.
231
307
  *
232
308
  * @param deviceId - The device ID whose tokens should be revoked
233
309
  */
234
310
  revokeDeviceTokens(deviceId: string): Promise<void>;
235
311
  /**
236
- * Refresh an access token using a valid refresh token.
312
+ * Revoke every token issued to a user up to now (password reset or change,
313
+ * admin session revocation, account deletion).
237
314
  *
238
- * Implements **refresh token rotation with reuse detection**: a new refresh token
239
- * is issued alongside the new access token. The old refresh token's `jti` is
240
- * recorded in the revocation store (if configured). If a previously consumed
241
- * refresh token is presented again, it indicates potential token theft.
315
+ * @param userId - The user whose credentials should be revoked
316
+ */
317
+ revokeAllForUser(userId: string): Promise<void>;
318
+ /**
319
+ * Rotate a refresh token: atomically consume it and mint its successor pair.
242
320
  *
243
- * Returns null if the provided token is invalid, expired, or not a refresh token.
321
+ * - Each refresh `jti` mints at most one successor family member (AUTH-6).
322
+ * - A concurrent duplicate on this instance gets `in_progress` (retry later).
323
+ * - Within the grace window, presenting a just-rotated token ONCE more returns
324
+ * the SAME successor pair, so a response lost on the wire does not sign the
325
+ * user out (NEW-AUTH-3).
326
+ * - Any other reuse revokes the token FAMILY, never the device (NEW-AUTH-1).
244
327
  *
245
328
  * @param refreshToken - The refresh token JWT string
246
- * @returns A new access/refresh token pair, or null if the refresh token is invalid
329
+ * @returns The successor pair, or the reason the refresh was refused
330
+ */
331
+ rotateRefreshToken(refreshToken: string): Promise<RefreshResult>;
332
+ /**
333
+ * Refresh an access token using a valid refresh token.
334
+ *
335
+ * Convenience wrapper over {@link rotateRefreshToken} that collapses every
336
+ * failure to null. HTTP handlers should use `rotateRefreshToken` so they can
337
+ * tell a client to retry (`in_progress`) instead of signing it out.
338
+ *
339
+ * @param refreshToken - The refresh token JWT string
340
+ * @returns A new access/refresh token pair, or null if the refresh was refused
247
341
  */
248
342
  refreshAccessToken(refreshToken: string): Promise<{
249
343
  accessToken: string;
250
344
  refreshToken: string;
251
345
  } | null>;
346
+ private rotate;
347
+ /**
348
+ * Deterministically mint the successor pair of a refresh token. The jtis and
349
+ * issue time derive from the consumed token and its consumption time, so a
350
+ * grace replay re-signs byte-identical tokens without storing them.
351
+ */
352
+ private successorTokens;
353
+ private deriveJti;
354
+ private basePayload;
355
+ private sign;
356
+ private verifySignature;
357
+ }
358
+
359
+ type MaybePromise<T> = T | Promise<T>;
360
+ /**
361
+ * Verified identity of a sync session, derived on the server from a validated
362
+ * access token. Never contains client-supplied values.
363
+ */
364
+ interface VerifiedSyncClaims {
365
+ /** Verified user id (`sub`). */
366
+ userId: string;
367
+ /** Verified device id (`dev`). */
368
+ deviceId: string;
369
+ /** The user's email from the user store. */
370
+ email: string;
371
+ /** The user's display name from the user store. */
372
+ name: string;
373
+ }
374
+ /**
375
+ * Server-side scope derivation for the sync auth provider (AUTH-1).
376
+ *
377
+ * The result is the session's complete grant: the client handshake can only
378
+ * narrow it, and a schema-scoped collection whose binding is unresolved is
379
+ * denied (`SCOPE_REQUIRED`), never widened.
380
+ */
381
+ interface SyncScopeOptions {
382
+ /**
383
+ * Extra verified scope values merged over the default `{ userId }`, for
384
+ * example `{ orgId: await orgOf(userId) }`. Every schema-scoped collection is
385
+ * bound from these values. Return `undefined`/`null` for a key to deny the
386
+ * collections that need it.
387
+ */
388
+ scopeValues?: (claims: VerifiedSyncClaims) => MaybePromise<Record<string, unknown>>;
389
+ /**
390
+ * Full explicit grant. When provided it replaces the default derivation:
391
+ * collections it omits are not visible. Use `claimScopes(values, explicit)`
392
+ * from `@korajs/core` to combine claim binding with explicit predicates.
393
+ */
394
+ resolveScopes?: (claims: VerifiedSyncClaims) => MaybePromise<ScopeMap>;
395
+ }
396
+ /** Auth context returned to `@korajs/server` by the sync auth provider. */
397
+ interface SyncAuthContext {
398
+ userId: string;
399
+ scopes?: Record<string, Record<string, unknown>>;
400
+ metadata?: Record<string, unknown>;
401
+ /** Access-token expiry (ms since epoch); a session must not outlive it (AUTH-11). */
402
+ expiresAt?: number;
403
+ }
404
+ /** Structural `AuthProvider` returned by `toSyncAuthProvider()`. */
405
+ interface SyncAuthProvider {
406
+ authenticate(token: string): Promise<SyncAuthContext | null>;
407
+ /**
408
+ * Revocation feed (device revoke, sign-out, password reset or change, admin
409
+ * revoke). `KoraSyncServer` subscribes automatically and terminates the
410
+ * matching live sessions (AUTH-11).
411
+ *
412
+ * @returns An unsubscribe function
413
+ */
414
+ onRevoke?(listener: (event: {
415
+ userId: string;
416
+ deviceId?: string;
417
+ }) => void | Promise<void>): () => void;
252
418
  }
253
419
 
254
420
  /**
@@ -302,6 +468,13 @@ interface AuthDevice {
302
468
  declare class DuplicateEmailError extends KoraError {
303
469
  constructor();
304
470
  }
471
+ /**
472
+ * Thrown when a device id is already registered to a different user. A token's
473
+ * `dev` claim must always name a device owned by its `sub` (AUTH-5).
474
+ */
475
+ declare class DeviceOwnershipError extends KoraError {
476
+ constructor(deviceId: string);
477
+ }
305
478
  /**
306
479
  * Generic interface for user and device persistence.
307
480
  *
@@ -334,7 +507,11 @@ interface UserStore {
334
507
  findByEmail(email: string): Promise<StoredUser | null>;
335
508
  /** Find a user by ID. */
336
509
  findById(id: string): Promise<StoredUser | null>;
337
- /** Register a device for a user. Idempotent if device already exists and is not revoked. */
510
+ /**
511
+ * Register a device for a user. Idempotent for the same owner (a revoked
512
+ * device is re-activated). Must throw {@link DeviceOwnershipError} when the id
513
+ * already belongs to a different user.
514
+ */
338
515
  registerDevice(params: {
339
516
  id: string;
340
517
  userId: string;
@@ -359,6 +536,12 @@ interface UserStore {
359
536
  delete(userId: string): Promise<void>;
360
537
  /** Update the last-seen timestamp for a device. No-op if device does not exist. */
361
538
  touchDevice(deviceId: string): Promise<void>;
539
+ /**
540
+ * Optional token revocation store that lives with the user data (same
541
+ * database). `createKoraAuthServer` uses it by default, so revocations
542
+ * persist and are shared exactly as far as users are.
543
+ */
544
+ getTokenRevocationStore?(): TokenRevocationStore;
362
545
  }
363
546
  /**
364
547
  * In-memory user and device store for the built-in auth provider.
@@ -387,6 +570,10 @@ declare class InMemoryUserStore implements UserStore {
387
570
  private readonly devicesById;
388
571
  /** Device IDs indexed by user ID for fast listing */
389
572
  private readonly devicesByUserId;
573
+ /** Revocations live with the users, so every server sharing this store shares them. */
574
+ private readonly revocationStore;
575
+ /** The token revocation store that shares this store's lifetime. */
576
+ getTokenRevocationStore(): TokenRevocationStore;
390
577
  /**
391
578
  * Create a new user account.
392
579
  *
@@ -421,9 +608,10 @@ declare class InMemoryUserStore implements UserStore {
421
608
  /**
422
609
  * Register a device for a user.
423
610
  *
424
- * If a device with the same ID already exists and is not revoked, it is
425
- * returned as-is (idempotent registration). If it was previously revoked,
426
- * it is re-activated with updated details.
611
+ * If a device with the same ID already exists for the same user and is not
612
+ * revoked, it is returned as-is (idempotent registration). If it was
613
+ * previously revoked, it is re-activated with updated details. A device id
614
+ * owned by another user is refused.
427
615
  *
428
616
  * @param params - Device registration parameters
429
617
  * @param params.id - Unique device identifier
@@ -431,6 +619,7 @@ declare class InMemoryUserStore implements UserStore {
431
619
  * @param params.publicKey - Base64url-encoded device public key or thumbprint
432
620
  * @param params.name - Human-readable device name
433
621
  * @returns The registered device record
622
+ * @throws {DeviceOwnershipError} If the id is registered to another user
434
623
  */
435
624
  registerDevice(params: {
436
625
  id: string;
@@ -598,6 +787,61 @@ interface AuthRoutesConfig {
598
787
  * (10 attempts per minute).
599
788
  */
600
789
  rateLimiter?: RateLimiter;
790
+ /**
791
+ * Called after credentials are revoked (sign-out, device revocation, user-wide
792
+ * revocation). `createKoraAuthServer().bindSyncServer()` uses it to end live
793
+ * sync sessions (AUTH-11).
794
+ */
795
+ onRevoke?: (event: AuthRevocationEvent) => void | Promise<void>;
796
+ /**
797
+ * Second-factor verifier (a `TotpManager` fits). When configured, users with
798
+ * MFA enabled get `{ mfaRequired, mfaToken }` from sign-in instead of tokens,
799
+ * and only `POST /auth/mfa/verify` issues their session (AUTH-10).
800
+ */
801
+ mfa?: MfaVerifier;
802
+ }
803
+ /** Second-factor checks used at sign-in. `TotpManager` implements this. */
804
+ interface MfaVerifier {
805
+ isEnabled(userId: string): Promise<boolean>;
806
+ verify(userId: string, code: string): Promise<boolean>;
807
+ verifyRecoveryCode?(userId: string, recoveryCode: string): Promise<boolean>;
808
+ }
809
+ /** Sign-in result for a user who still has to pass the second factor. */
810
+ interface MfaChallenge {
811
+ mfaRequired: true;
812
+ /** Short-lived token accepted only by `POST /auth/mfa/verify`. */
813
+ mfaToken: string;
814
+ }
815
+ /** Successful primary authentication: a session, or an MFA challenge. */
816
+ type SignInResult = {
817
+ user: AuthUser;
818
+ tokens: AuthTokens;
819
+ } | MfaChallenge;
820
+ /**
821
+ * Describes credentials that were just revoked, so live sessions holding them
822
+ * (for example open sync connections) can be terminated.
823
+ */
824
+ type AuthRevocationEvent = {
825
+ kind: 'device';
826
+ userId: string;
827
+ deviceId: string;
828
+ } | {
829
+ kind: 'user';
830
+ userId: string;
831
+ } | {
832
+ kind: 'session';
833
+ userId: string;
834
+ deviceId: string;
835
+ family: string | null;
836
+ };
837
+ /** Result of {@link BuiltInAuthRoutes.authenticateAccess}. */
838
+ interface AuthenticatedAccess {
839
+ /** Verified access-token payload. */
840
+ payload: TokenPayload;
841
+ /** The token's user, as stored. */
842
+ user: StoredUser;
843
+ /** The token's device record (null when the token was minted without one). */
844
+ device: AuthDevice | null;
601
845
  }
602
846
  /**
603
847
  * Response envelope returned by all auth route handlers.
@@ -608,12 +852,19 @@ interface AuthRoutesConfig {
608
852
  interface AuthRouteResponse<T> {
609
853
  /** HTTP status code */
610
854
  status: number;
611
- /** Either the success payload or an error message */
855
+ /**
856
+ * Either the success payload or an error message. `code` is a stable,
857
+ * machine-readable Kora error code; clients use it to tell a definitive
858
+ * rejection from a proxy or captive-portal response.
859
+ */
612
860
  body: {
613
861
  data: T;
614
862
  } | {
615
863
  error: string;
864
+ code?: string;
616
865
  };
866
+ /** Optional response headers (for example `Retry-After`). */
867
+ headers?: Record<string, string>;
617
868
  }
618
869
  /**
619
870
  * HTTP route handlers for the built-in Kora auth provider.
@@ -654,7 +905,70 @@ declare class BuiltInAuthRoutes {
654
905
  private readonly tokenManager;
655
906
  private readonly challengeStore;
656
907
  private readonly rateLimiter;
908
+ private readonly revokeListeners;
909
+ /** Lazily computed hash used to equalize sign-in timing for unknown emails. */
910
+ private dummyCredential;
911
+ private readonly mfa;
657
912
  constructor(config: AuthRoutesConfig);
913
+ /**
914
+ * Finish a successful primary authentication (password, OAuth): issue a
915
+ * session, or an MFA challenge when the user is enrolled in MFA. No
916
+ * full-privilege token is ever issued to an MFA user without a fresh second
917
+ * factor.
918
+ *
919
+ * @param user - The authenticated user
920
+ * @param deviceId - The (already registered) device
921
+ * @param amr - Methods satisfied so far (for example `['pwd']`)
922
+ */
923
+ completePrimaryAuthentication(user: AuthUser, deviceId: string, amr: string[]): Promise<AuthRouteResponse<SignInResult>>;
924
+ /**
925
+ * Handle the second factor (POST /auth/mfa/verify).
926
+ *
927
+ * Exchanges an `mfa_pending` token plus a TOTP code (or recovery code) for a
928
+ * session whose tokens carry `amr` including `otp` (or `rcv`). The pending
929
+ * token is redeemed once; a wrong code can be retried until it expires.
930
+ */
931
+ handleMfaVerify(body: {
932
+ mfaToken?: unknown;
933
+ code?: unknown;
934
+ recoveryCode?: unknown;
935
+ }): Promise<AuthRouteResponse<{
936
+ user: AuthUser;
937
+ tokens: AuthTokens;
938
+ }>>;
939
+ /**
940
+ * Subscribe to credential revocations (sign-out, device revocation,
941
+ * user-wide revocation).
942
+ *
943
+ * @param listener - Called after each revocation is persisted
944
+ * @returns An unsubscribe function
945
+ */
946
+ onRevoke(listener: (event: AuthRevocationEvent) => void | Promise<void>): () => void;
947
+ /**
948
+ * Authenticate an access token for any request-authorization path.
949
+ *
950
+ * The single check used by every HTTP route and by the sync provider: it
951
+ * verifies signature and expiry, the token's own revocation, its family, the
952
+ * device cut-off, the per-user cut-off, that the user still exists, and that
953
+ * the device record is neither revoked nor owned by someone else.
954
+ *
955
+ * @param token - Raw access token (without "Bearer ")
956
+ * @returns The verified access, or null when the token must be rejected
957
+ */
958
+ authenticateAccess(token: string): Promise<AuthenticatedAccess | null>;
959
+ /**
960
+ * Revoke every credential a user holds (password reset or change, admin
961
+ * session revocation, account deletion) and notify revocation listeners.
962
+ *
963
+ * @param userId - The user whose sessions end now
964
+ */
965
+ revokeAllForUser(userId: string): Promise<void>;
966
+ private emitRevoke;
967
+ /**
968
+ * Register the device for a successful primary authentication, refusing ids
969
+ * owned by another user.
970
+ */
971
+ private registerSignInDevice;
658
972
  /**
659
973
  * Handle user sign-up (POST /auth/signup).
660
974
  *
@@ -699,10 +1013,7 @@ declare class BuiltInAuthRoutes {
699
1013
  password: string;
700
1014
  deviceId?: string;
701
1015
  devicePublicKey?: string;
702
- }, clientIp?: string): Promise<AuthRouteResponse<{
703
- user: AuthUser;
704
- tokens: AuthTokens;
705
- }>>;
1016
+ }, clientIp?: string): Promise<AuthRouteResponse<SignInResult>>;
706
1017
  /**
707
1018
  * Handle token refresh (POST /auth/refresh).
708
1019
  *
@@ -832,17 +1143,24 @@ declare class BuiltInAuthRoutes {
832
1143
  * @returns A 64-character hex string (32 random bytes)
833
1144
  */
834
1145
  static generateChallenge(): string;
1146
+ private getDummyCredential;
835
1147
  /**
836
1148
  * Creates a sync server auth provider compatible with `@korajs/server`.
837
1149
  *
838
1150
  * The returned object implements the `AuthProvider` interface from
839
1151
  * `@korajs/server`, validating access tokens and returning an auth
840
- * context containing the user ID and device metadata. This bridges
841
- * the built-in auth system with the sync server's authentication layer.
1152
+ * context containing the user ID, device metadata and a SERVER-DERIVED
1153
+ * scope grant. The client handshake can only narrow that grant (AUTH-1).
1154
+ *
1155
+ * By default the grant binds every schema-scoped collection from
1156
+ * `{ userId: <verified sub> }`. Collections scoped by any other key (for
1157
+ * example `orgId`) are denied until `scopeValues` or `resolveScopes`
1158
+ * supplies it; they are never widened to "every tenant".
842
1159
  *
843
1160
  * Also checks device revocation status during authentication, ensuring
844
1161
  * that revoked devices are rejected even if their tokens haven't expired.
845
1162
  *
1163
+ * @param options - Optional server-side scope derivation
846
1164
  * @returns An object with an `authenticate` method suitable for KoraSyncServer's `auth` config
847
1165
  *
848
1166
  * @example
@@ -850,17 +1168,13 @@ declare class BuiltInAuthRoutes {
850
1168
  * const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
851
1169
  * const syncServer = new KoraSyncServer({
852
1170
  * store,
853
- * auth: routes.toSyncAuthProvider(),
1171
+ * auth: routes.toSyncAuthProvider({
1172
+ * scopeValues: async ({ userId }) => ({ orgId: await orgOf(userId) }),
1173
+ * }),
854
1174
  * })
855
1175
  * ```
856
1176
  */
857
- toSyncAuthProvider(): {
858
- authenticate(token: string): Promise<{
859
- userId: string;
860
- scopes?: Record<string, Record<string, unknown>>;
861
- metadata?: Record<string, unknown>;
862
- } | null>;
863
- };
1177
+ toSyncAuthProvider(options?: SyncScopeOptions): SyncAuthProvider;
864
1178
  }
865
1179
 
866
1180
  /**
@@ -1021,6 +1335,20 @@ declare class InMemoryLinkedIdentityStore implements LinkedIdentityStore {
1021
1335
  delete(userId: string, provider: string): Promise<void>;
1022
1336
  }
1023
1337
 
1338
+ /** What an OAuth flow is for. A state minted for one purpose is useless for the other. */
1339
+ type OAuthFlowPurpose = 'signin' | 'link';
1340
+ /** Binding requested when an authorization URL is created (AUTH-3). */
1341
+ interface OAuthFlowBinding {
1342
+ /** The flow purpose. Defaults to `'signin'`. */
1343
+ purpose?: OAuthFlowPurpose;
1344
+ /** For `'link'`: the authenticated Kora user starting the flow. */
1345
+ userId?: string;
1346
+ /**
1347
+ * A random secret held only by the initiating client (an HttpOnly cookie on
1348
+ * the web, memory for native/PKCE). Only its SHA-256 is stored server-side.
1349
+ */
1350
+ binding?: string;
1351
+ }
1024
1352
  /**
1025
1353
  * In-memory OAuth state store for development.
1026
1354
  * Use Redis or a database in production for multi-server deployments.
@@ -1080,7 +1408,7 @@ declare class OAuthManager {
1080
1408
  * Generate an authorization URL for the user to visit.
1081
1409
  * Returns the URL and the state parameter for CSRF validation.
1082
1410
  */
1083
- getAuthorizationUrl(providerId: string, metadata?: Record<string, unknown>): Promise<{
1411
+ getAuthorizationUrl(providerId: string, metadata?: Record<string, unknown>, flow?: OAuthFlowBinding): Promise<{
1084
1412
  url: string;
1085
1413
  state: string;
1086
1414
  }>;
@@ -1089,12 +1417,17 @@ declare class OAuthManager {
1089
1417
  * Validates the state parameter, exchanges the code for tokens,
1090
1418
  * and fetches user info.
1091
1419
  *
1420
+ * The state is single-use and is redeemable only for the purpose it was
1421
+ * minted for, by the same user (for linking) and the same client binding.
1422
+ * A mismatch is rejected before the code is exchanged.
1423
+ *
1092
1424
  * @param providerId - The OAuth provider
1093
1425
  * @param code - The authorization code from the callback
1094
1426
  * @param state - The state parameter from the callback
1427
+ * @param expected - The purpose, user and client binding of this callback
1095
1428
  * @returns Tokens and user info from the provider
1096
1429
  */
1097
- handleCallback(providerId: string, code: string, state: string): Promise<{
1430
+ handleCallback(providerId: string, code: string, state: string, expected?: OAuthFlowBinding): Promise<{
1098
1431
  tokens: OAuthTokens;
1099
1432
  userInfo: OAuthUserInfo;
1100
1433
  stateMetadata?: Record<string, unknown>;
@@ -1156,9 +1489,21 @@ interface OAuthServerConfig extends Omit<OAuthManagerConfig, 'providers'> {
1156
1489
  */
1157
1490
  allowUnlinkLastIdentity?: boolean;
1158
1491
  }
1159
- interface CreateKoraAuthServerOptions {
1492
+ interface CreateKoraAuthServerOptions extends SyncScopeOptions {
1160
1493
  /** Existing user store. Defaults to InMemoryUserStore for development. */
1161
1494
  userStore?: UserStore;
1495
+ /**
1496
+ * Token revocation store. Defaults to the user store's own revocation store
1497
+ * (`userStore.getTokenRevocationStore()`), so revocations persist and are shared
1498
+ * exactly as far as the users are.
1499
+ */
1500
+ revocationStore?: TokenRevocationStore;
1501
+ /**
1502
+ * Allow in-memory user or revocation stores when `NODE_ENV=production`.
1503
+ * In-memory stores lose every account and every revocation on restart and are
1504
+ * not shared between instances, so production refuses them by default.
1505
+ */
1506
+ allowInMemory?: boolean;
1162
1507
  /** Existing token manager. Overrides `jwtSecret` and `tokenManager` options. */
1163
1508
  tokenManager?: TokenManager;
1164
1509
  /** JWT secret. Required in production when `tokenManager` is not provided. */
@@ -1171,6 +1516,22 @@ interface CreateKoraAuthServerOptions {
1171
1516
  oauth?: OAuthServerConfig;
1172
1517
  challengeStore?: ChallengeStore;
1173
1518
  rateLimiter?: RateLimiter;
1519
+ /**
1520
+ * Second-factor verifier (for example a `TotpManager`). Users with MFA enabled
1521
+ * then sign in in two steps: `{ mfaRequired, mfaToken }`, then
1522
+ * `POST /auth/mfa/verify` with `{ mfaToken, code }`.
1523
+ */
1524
+ mfa?: MfaVerifier;
1525
+ }
1526
+ /**
1527
+ * The part of `KoraSyncServer` the auth server needs to end live sessions.
1528
+ * Structural, so `@korajs/auth` does not depend on `@korajs/server`.
1529
+ */
1530
+ interface SyncSessionTerminator {
1531
+ terminateSessions(filter: {
1532
+ userId?: string;
1533
+ deviceId?: string;
1534
+ }): unknown;
1174
1535
  }
1175
1536
  interface KoraAuthServer {
1176
1537
  routes: BuiltInAuthRoutes;
@@ -1178,8 +1539,31 @@ interface KoraAuthServer {
1178
1539
  tokenManager: TokenManager;
1179
1540
  oauth?: OAuthManager;
1180
1541
  linkedIdentityStore?: LinkedIdentityStore;
1181
- auth: ReturnType<BuiltInAuthRoutes['toSyncAuthProvider']>;
1542
+ /** Sync auth provider with server-derived scopes (pass to `KoraSyncServer`). */
1543
+ auth: SyncAuthProvider;
1182
1544
  handleRequest(request: KoraAuthHttpRequest): Promise<AuthRouteResponse<unknown>>;
1545
+ /**
1546
+ * Revoke every credential a user holds (call after a password change made
1547
+ * outside these routes, an admin action or an account deletion).
1548
+ */
1549
+ revokeAllForUser(userId: string): Promise<void>;
1550
+ /** Subscribe to credential revocations. Returns an unsubscribe function. */
1551
+ onRevoke(listener: (event: AuthRevocationEvent) => void | Promise<void>): () => void;
1552
+ /**
1553
+ * End live sync sessions when their credentials are revoked (AUTH-11).
1554
+ * Requires a sync server exposing `terminateSessions({ userId, deviceId })`.
1555
+ * A `KoraSyncServer` constructed with this server's `auth` provider already
1556
+ * follows its revocations; call this for a server built with a wrapping or
1557
+ * custom provider. Binding both is harmless. Returns an unsubscribe function.
1558
+ */
1559
+ bindSyncServer(server: SyncSessionTerminator): () => void;
1560
+ }
1561
+ /**
1562
+ * Thrown by `createKoraAuthServer` in production when a store would silently
1563
+ * lose accounts or revocations on restart.
1564
+ */
1565
+ declare class InMemoryAuthStoreError extends KoraError {
1566
+ constructor(which: 'userStore' | 'revocationStore');
1183
1567
  }
1184
1568
  /**
1185
1569
  * Create the built-in Kora auth server with production-shaped defaults.
@@ -1189,6 +1573,85 @@ interface KoraAuthServer {
1189
1573
  */
1190
1574
  declare function createKoraAuthServer(options?: CreateKoraAuthServerOptions): KoraAuthServer;
1191
1575
 
1576
+ /**
1577
+ * Minimal better-sqlite3 subset to avoid a hard dependency on the package.
1578
+ */
1579
+ interface SqliteRevocationDatabase {
1580
+ exec(source: string): void;
1581
+ prepare(source: string): {
1582
+ run(...params: unknown[]): {
1583
+ changes: number;
1584
+ };
1585
+ get(...params: unknown[]): unknown;
1586
+ };
1587
+ }
1588
+ /**
1589
+ * SQLite-backed {@link TokenRevocationStore} (better-sqlite3).
1590
+ *
1591
+ * Revocations survive restarts and are shared by every process using the same
1592
+ * database file. `consume` is a single `INSERT ... ON CONFLICT DO NOTHING`, so it
1593
+ * is atomic: better-sqlite3 executes statements synchronously under SQLite's
1594
+ * write lock.
1595
+ *
1596
+ * @example
1597
+ * ```typescript
1598
+ * const userStore = await createSqliteUserStore({ filename: './auth.db' })
1599
+ * const revocationStore = userStore.getTokenRevocationStore()
1600
+ * ```
1601
+ */
1602
+ declare class SqliteTokenRevocationStore implements TokenRevocationStore {
1603
+ private readonly db;
1604
+ constructor(db: SqliteRevocationDatabase);
1605
+ isRevoked(jti: string): Promise<boolean>;
1606
+ revoke(jti: string, expiresAt: number): Promise<void>;
1607
+ consume(jti: string, expiresAt: number): Promise<ConsumeResult>;
1608
+ isConsumed(jti: string): Promise<boolean>;
1609
+ revokeAllForDevice(deviceId: string, before?: number): Promise<void>;
1610
+ getDeviceRevokedBefore(deviceId: string): Promise<number | null>;
1611
+ revokeAllForUser(userId: string, before?: number): Promise<void>;
1612
+ getUserRevokedBefore(userId: string): Promise<number | null>;
1613
+ /** Delete revocations and consumptions whose tokens have expired. */
1614
+ cleanup(): Promise<void>;
1615
+ private setCutoff;
1616
+ private getCutoff;
1617
+ }
1618
+
1619
+ /**
1620
+ * Minimal postgres-js tagged-template client.
1621
+ */
1622
+ type PostgresRevocationClient = (template: TemplateStringsArray, ...args: unknown[]) => Promise<Record<string, unknown>[]>;
1623
+ /**
1624
+ * PostgreSQL-backed {@link TokenRevocationStore} (postgres-js).
1625
+ *
1626
+ * Shared by every server instance using the database. `consume` is one
1627
+ * `INSERT ... ON CONFLICT DO NOTHING RETURNING`, so exactly one concurrent
1628
+ * caller, on any instance, consumes a refresh token (AUTH-6).
1629
+ *
1630
+ * @example
1631
+ * ```typescript
1632
+ * const userStore = await createPostgresUserStore({ connectionString })
1633
+ * const revocationStore = userStore.getTokenRevocationStore()
1634
+ * ```
1635
+ */
1636
+ declare class PostgresTokenRevocationStore implements TokenRevocationStore {
1637
+ private readonly sql;
1638
+ private readonly ready;
1639
+ constructor(sql: PostgresRevocationClient);
1640
+ private ensureTables;
1641
+ isRevoked(jti: string): Promise<boolean>;
1642
+ revoke(jti: string, expiresAt: number): Promise<void>;
1643
+ consume(jti: string, expiresAt: number): Promise<ConsumeResult>;
1644
+ isConsumed(jti: string): Promise<boolean>;
1645
+ revokeAllForDevice(deviceId: string, before?: number): Promise<void>;
1646
+ getDeviceRevokedBefore(deviceId: string): Promise<number | null>;
1647
+ revokeAllForUser(userId: string, before?: number): Promise<void>;
1648
+ getUserRevokedBefore(userId: string): Promise<number | null>;
1649
+ /** Delete revocations and consumptions whose tokens have expired. */
1650
+ cleanup(): Promise<void>;
1651
+ private setCutoff;
1652
+ private getCutoff;
1653
+ }
1654
+
1192
1655
  /**
1193
1656
  * Creates a signed JWT (HS256) from a payload and secret.
1194
1657
  *
@@ -1348,8 +1811,12 @@ interface PasswordResetStore {
1348
1811
  store(token: PasswordResetToken): Promise<void>;
1349
1812
  /** Look up a token. Returns null if not found. */
1350
1813
  get(token: string): Promise<PasswordResetToken | null>;
1351
- /** Mark a token as consumed. */
1352
- consume(token: string): Promise<void>;
1814
+ /**
1815
+ * Mark a token as consumed. Should be atomic and return `true` only for the
1816
+ * call that consumed a not-yet-consumed token; `false` makes the reset fail
1817
+ * as already used. Returning nothing is accepted for older stores.
1818
+ */
1819
+ consume(token: string): Promise<boolean | undefined>;
1353
1820
  /** Count active (non-consumed, non-expired) tokens for an email. */
1354
1821
  countActiveForEmail(email: string): Promise<number>;
1355
1822
  /** Remove expired tokens. */
@@ -1368,11 +1835,23 @@ interface PasswordResetConfig {
1368
1835
  /** Max reset requests per email in the TTL window. Defaults to 3. */
1369
1836
  maxRequestsPerEmail?: number;
1370
1837
  /**
1371
- * Callback invoked when a reset is requested.
1372
- * The developer must implement email sending.
1373
- * If not provided, the token is returned in the route response (development mode).
1838
+ * Callback invoked when a reset is requested. The developer must implement
1839
+ * email sending: the token must only ever reach the account's mailbox.
1374
1840
  */
1375
1841
  onResetRequested?: (email: string, token: string, expiresAt: number) => void | Promise<void>;
1842
+ /**
1843
+ * Development only: also return the reset token in the `requestReset`
1844
+ * response when no `onResetRequested` callback is configured. Ignored when
1845
+ * `NODE_ENV=production`; the token is never disclosed to the requester there.
1846
+ * @default false
1847
+ */
1848
+ exposeTokenForDevelopment?: boolean;
1849
+ /**
1850
+ * Called after a password is reset or changed, once every earlier credential
1851
+ * of the user has been revoked. Use it to end live sessions (for example
1852
+ * `authServer.revokeAllForUser` when the user store has no revocation store).
1853
+ */
1854
+ onPasswordChanged?: (userId: string) => void | Promise<void>;
1376
1855
  }
1377
1856
  declare class PasswordResetError extends KoraError {
1378
1857
  constructor(message: string, code: string, context?: Record<string, unknown>);
@@ -1390,7 +1869,7 @@ declare class InMemoryPasswordResetStore implements PasswordResetStore {
1390
1869
  private tokens;
1391
1870
  store(token: PasswordResetToken): Promise<void>;
1392
1871
  get(token: string): Promise<PasswordResetToken | null>;
1393
- consume(token: string): Promise<void>;
1872
+ consume(token: string): Promise<boolean>;
1394
1873
  countActiveForEmail(email: string): Promise<number>;
1395
1874
  cleanExpired(): Promise<number>;
1396
1875
  }
@@ -1419,13 +1898,16 @@ declare class PasswordResetManager {
1419
1898
  private readonly tokenTtlMs;
1420
1899
  private readonly maxRequestsPerEmail;
1421
1900
  private readonly onResetRequested?;
1901
+ private readonly exposeTokenForDevelopment;
1902
+ private readonly onPasswordChanged?;
1903
+ private warnedNoDelivery;
1422
1904
  constructor(config: PasswordResetConfig);
1423
1905
  /**
1424
1906
  * Request a password reset for an email.
1425
- * Always returns success to prevent email enumeration.
1907
+ * Always returns the same success response to prevent email enumeration.
1426
1908
  *
1427
- * If a callback is configured, invokes it with the token.
1428
- * In development mode (no callback), returns the token in the response.
1909
+ * The token is delivered only through `onResetRequested`. It is returned in
1910
+ * the response only with `exposeTokenForDevelopment: true` outside production.
1429
1911
  */
1430
1912
  requestReset(email: string): Promise<{
1431
1913
  status: number;
@@ -1464,6 +1946,11 @@ declare class PasswordResetManager {
1464
1946
  error: string;
1465
1947
  };
1466
1948
  }>;
1949
+ /**
1950
+ * A password change kills every earlier credential (AUTH-7b): every access
1951
+ * and refresh token issued before now is rejected on every path.
1952
+ */
1953
+ private revokeEarlierCredentials;
1467
1954
  }
1468
1955
 
1469
1956
  /**
@@ -1641,7 +2128,10 @@ interface SqliteDatabase$1 {
1641
2128
  */
1642
2129
  declare class SqliteUserStore implements UserStore {
1643
2130
  private readonly db;
2131
+ private revocationStore;
1644
2132
  constructor(db: SqliteDatabase$1);
2133
+ /** Token revocations stored in the same SQLite database as the users. */
2134
+ getTokenRevocationStore(): TokenRevocationStore;
1645
2135
  private ensureTables;
1646
2136
  createUser(params: {
1647
2137
  email: string;
@@ -1687,6 +2177,19 @@ declare function createSqliteUserStore(options: {
1687
2177
  interface PostgresClient$1 {
1688
2178
  begin<T>(fn: (sql: PostgresClient$1) => Promise<T>): Promise<T>;
1689
2179
  (template: TemplateStringsArray, ...args: unknown[]): Promise<Record<string, unknown>[]>;
2180
+ /** postgres-js: close every connection of the pool (optional in this subset). */
2181
+ end?(options?: {
2182
+ timeout?: number;
2183
+ }): Promise<void>;
2184
+ }
2185
+ /** Options for {@link PostgresUserStore}. */
2186
+ interface PostgresUserStoreOptions {
2187
+ /**
2188
+ * Whether {@link PostgresUserStore.close} ends the client's connections. True for
2189
+ * a store made by `createPostgresUserStore` (it opened the client); false by
2190
+ * default for a client you pass in, which you end yourself.
2191
+ */
2192
+ ownsClient?: boolean;
1690
2193
  }
1691
2194
  /**
1692
2195
  * PostgreSQL-backed user and device store using postgres-js.
@@ -1706,12 +2209,32 @@ interface PostgresClient$1 {
1706
2209
  * connectionString: 'postgres://user:pass@localhost:5432/mydb',
1707
2210
  * })
1708
2211
  * const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
2212
+ * // On shutdown (or at the end of a script):
2213
+ * await userStore.close()
1709
2214
  * ```
1710
2215
  */
1711
2216
  declare class PostgresUserStore implements UserStore {
1712
2217
  private readonly sql;
1713
2218
  private readonly ready;
1714
- constructor(sql: PostgresClient$1);
2219
+ private revocationStore;
2220
+ private readonly ownsClient;
2221
+ private closed;
2222
+ constructor(sql: PostgresClient$1, options?: PostgresUserStoreOptions);
2223
+ /**
2224
+ * Release the store's database connections, so a script using it can exit. Waits
2225
+ * for table creation to settle first. A client passed to the constructor is left
2226
+ * open unless `ownsClient` was set (end it yourself). Idempotent.
2227
+ *
2228
+ * @example
2229
+ * ```typescript
2230
+ * const users = await createPostgresUserStore({ connectionString })
2231
+ * const device = await users.findDevice(nodeId)
2232
+ * await users.close()
2233
+ * ```
2234
+ */
2235
+ close(): Promise<void>;
2236
+ /** Token revocations stored in the same Postgres database as the users. */
2237
+ getTokenRevocationStore(): TokenRevocationStore;
1715
2238
  private ensureTables;
1716
2239
  createUser(params: {
1717
2240
  email: string;
@@ -2044,6 +2567,22 @@ interface ExternalJwtProviderConfig {
2044
2567
  * ```
2045
2568
  */
2046
2569
  mapClaims?: (claims: Record<string, unknown>) => ExternalUserInfo;
2570
+ /**
2571
+ * Accepted audience(s) (`aud`). When set, tokens for any other audience are
2572
+ * rejected, even if they share the signing secret. Defaults to
2573
+ * `'authenticated'` when `providerName` is `'supabase'`.
2574
+ */
2575
+ audience?: string | string[];
2576
+ /** Accepted issuer(s) (`iss`). When set, tokens from any other issuer are rejected. */
2577
+ issuer?: string | string[];
2578
+ /**
2579
+ * Verified scope values for the sync grant, derived from the validated claims
2580
+ * (AUTH-1). Merged over `{ userId }`; every schema-scoped collection is bound
2581
+ * from them, and a collection whose binding is missing is denied.
2582
+ */
2583
+ scopeValues?: (claims: Record<string, unknown>) => Record<string, unknown>;
2584
+ /** Full explicit sync grant from the validated claims (replaces the default). */
2585
+ resolveScopes?: (claims: Record<string, unknown>) => ScopeMap | Promise<ScopeMap>;
2047
2586
  }
2048
2587
  /**
2049
2588
  * Authentication provider adapter for external JWT issuers.
@@ -2088,6 +2627,10 @@ declare class ExternalJwtProvider implements AuthProviderAdapter {
2088
2627
  private readonly jwtSecret;
2089
2628
  private readonly customValidateToken;
2090
2629
  private readonly mapClaims;
2630
+ private readonly audiences;
2631
+ private readonly issuers;
2632
+ private readonly scopeValues;
2633
+ private readonly resolveScopes;
2091
2634
  constructor(config: ExternalJwtProviderConfig);
2092
2635
  /**
2093
2636
  * Not supported for external providers.
@@ -2200,6 +2743,8 @@ declare class ExternalJwtProvider implements AuthProviderAdapter {
2200
2743
  * @returns The decoded claims object, or null if the token is invalid
2201
2744
  */
2202
2745
  private extractClaims;
2746
+ /** Enforce configured `aud` / `iss` so tokens for another service are refused. */
2747
+ private audienceAndIssuerMatch;
2203
2748
  }
2204
2749
 
2205
2750
  /**
@@ -2506,6 +3051,12 @@ declare function verifyRegistrationResponse(params: {
2506
3051
  expectedChallenge: string;
2507
3052
  expectedOrigin: string;
2508
3053
  expectedRpId: string;
3054
+ /**
3055
+ * Require the User Verified (UV) flag. Kora's options request
3056
+ * `userVerification: 'required'`, so the response must honour it.
3057
+ * @default true
3058
+ */
3059
+ requireUserVerification?: boolean;
2509
3060
  }): Promise<RegistrationVerificationResult>;
2510
3061
  /** Options returned by generateAuthenticationOptions for the client. */
2511
3062
  interface AuthenticationOptions {
@@ -2605,6 +3156,12 @@ declare function verifyAuthenticationResponse(params: {
2605
3156
  expectedRpId: string;
2606
3157
  publicKey: string;
2607
3158
  previousSignCount: number;
3159
+ /**
3160
+ * Require the User Verified (UV) flag. Kora's options request
3161
+ * `userVerification: 'required'`, so the assertion must honour it.
3162
+ * @default true
3163
+ */
3164
+ requireUserVerification?: boolean;
2608
3165
  }): Promise<AuthenticationVerificationResult>;
2609
3166
 
2610
3167
  /**
@@ -2808,7 +3365,11 @@ interface OrgStore {
2808
3365
  /** Consume an invitation (mark as accepted). Returns the invitation details. */
2809
3366
  consumeInvitation(token: string): Promise<OrgInvitation>;
2810
3367
  /** Revoke a pending invitation. */
2811
- revokeInvitation(invitationId: string): Promise<void>;
3368
+ /**
3369
+ * Revoke a pending invitation of `orgId`. Must throw InvitationNotFoundError
3370
+ * when the invitation belongs to another org (AUTH-4).
3371
+ */
3372
+ revokeInvitation(orgId: string, invitationId: string): Promise<void>;
2812
3373
  /** List pending invitations for an organization. */
2813
3374
  listPendingInvitations(orgId: string): Promise<OrgInvitation[]>;
2814
3375
  /** List pending invitations for a specific email address. */
@@ -2847,7 +3408,7 @@ declare class InMemoryOrgStore implements OrgStore {
2847
3408
  createInvitation(orgId: string, invitedBy: string, params: CreateInvitationParams): Promise<OrgInvitation>;
2848
3409
  getInvitationByToken(token: string): Promise<OrgInvitation | null>;
2849
3410
  consumeInvitation(token: string): Promise<OrgInvitation>;
2850
- revokeInvitation(invitationId: string): Promise<void>;
3411
+ revokeInvitation(orgId: string, invitationId: string): Promise<void>;
2851
3412
  listPendingInvitations(orgId: string): Promise<OrgInvitation[]>;
2852
3413
  listInvitationsForEmail(email: string): Promise<OrgInvitation[]>;
2853
3414
  cleanExpiredInvitations(): Promise<number>;
@@ -2873,7 +3434,26 @@ interface OrgRouteResponse<T> {
2873
3434
  interface OrgRoutesConfig {
2874
3435
  /** The organization store backing all org operations */
2875
3436
  orgStore: OrgStore;
3437
+ /**
3438
+ * Resolves a user's email and its verification status (a `UserStore`
3439
+ * satisfies this). Invitations are listed and accepted only for the caller's
3440
+ * own VERIFIED email; without a lookup, pass the verified identity to
3441
+ * `acceptInvitation` / `listMyInvitations` explicitly.
3442
+ */
3443
+ userLookup?: {
3444
+ findById(userId: string): Promise<{
3445
+ email: string;
3446
+ emailVerified: boolean;
3447
+ } | null>;
3448
+ };
3449
+ }
3450
+ /** A server-verified email identity used to match invitations. */
3451
+ interface VerifiedEmailIdentity {
3452
+ email: string;
3453
+ emailVerified: boolean;
2876
3454
  }
3455
+ /** Invitation as shown to its invitee: the secret token is never included. */
3456
+ type InviteeInvitation = Omit<OrgInvitation, 'token'>;
2877
3457
  /**
2878
3458
  * Server-side route handlers for organization management.
2879
3459
  *
@@ -2893,6 +3473,7 @@ interface OrgRoutesConfig {
2893
3473
  */
2894
3474
  declare class OrgRoutes {
2895
3475
  private readonly store;
3476
+ private readonly userLookup;
2896
3477
  constructor(config: OrgRoutesConfig);
2897
3478
  /**
2898
3479
  * Create a new organization. The authenticated user becomes the owner.
@@ -2965,11 +3546,17 @@ declare class OrgRoutes {
2965
3546
  role?: unknown;
2966
3547
  }): Promise<OrgRouteResponse<OrgInvitation>>;
2967
3548
  /**
2968
- * Accept an invitation by its token. The authenticated user joins the org.
3549
+ * Accept an invitation by its token. The authenticated user joins the org
3550
+ * only if the invitation was addressed to the user's own verified email.
3551
+ *
3552
+ * @param userId - The authenticated user
3553
+ * @param params - The invitation token
3554
+ * @param identity - The user's verified email identity; resolved through
3555
+ * `userLookup` when omitted
2969
3556
  */
2970
3557
  acceptInvitation(userId: string, params: {
2971
3558
  token?: unknown;
2972
- }): Promise<OrgRouteResponse<Membership>>;
3559
+ }, identity?: VerifiedEmailIdentity): Promise<OrgRouteResponse<Membership>>;
2973
3560
  /**
2974
3561
  * Revoke a pending invitation. Requires admin or higher.
2975
3562
  */
@@ -2981,9 +3568,15 @@ declare class OrgRoutes {
2981
3568
  */
2982
3569
  listPendingInvitations(userId: string, orgId: string): Promise<OrgRouteResponse<OrgInvitation[]>>;
2983
3570
  /**
2984
- * List pending invitations for the authenticated user's email.
3571
+ * List pending invitations addressed to the authenticated user's own
3572
+ * verified email. The email is resolved server-side (never taken from the
3573
+ * request) and invitation tokens are never returned.
3574
+ *
3575
+ * @param userId - The authenticated user
3576
+ * @param identity - The user's verified identity; resolved through `userLookup` when omitted
2985
3577
  */
2986
- listMyInvitations(email: string): Promise<OrgRouteResponse<OrgInvitation[]>>;
3578
+ listMyInvitations(userId: string, identity?: VerifiedEmailIdentity): Promise<OrgRouteResponse<InviteeInvitation[]>>;
3579
+ private resolveIdentity;
2987
3580
  /**
2988
3581
  * Check if the caller has the required role in the org.
2989
3582
  * Returns an error response if not authorized, or null if authorized.
@@ -3609,6 +4202,10 @@ interface TotpSecret {
3609
4202
  * until the first code is consumed.
3610
4203
  */
3611
4204
  lastUsedTimeStep?: number;
4205
+ /** Consecutive failed code checks (TOTP or recovery). Reset on success. */
4206
+ failedAttempts?: number;
4207
+ /** While set and in the future, every code check fails without being evaluated. */
4208
+ lockedUntil?: number;
3612
4209
  }
3613
4210
  /**
3614
4211
  * Setup result returned when enabling TOTP MFA.
@@ -3635,6 +4232,14 @@ interface TotpStore {
3635
4232
  declare class TotpError extends KoraError {
3636
4233
  constructor(message: string, code: string, context?: Record<string, unknown>);
3637
4234
  }
4235
+ /**
4236
+ * Thrown by `disable` / `regenerateRecoveryCodes` while code checks are locked
4237
+ * after repeated failures (AUTH-10). `verify` returns false instead.
4238
+ */
4239
+ declare class TotpLockedError extends TotpError {
4240
+ readonly lockedUntil: number;
4241
+ constructor(lockedUntil: number);
4242
+ }
3638
4243
  declare class TotpInvalidCodeError extends TotpError {
3639
4244
  constructor();
3640
4245
  }
@@ -3659,31 +4264,6 @@ declare class InMemoryTotpStore implements TotpStore {
3659
4264
  getByUserId(userId: string): Promise<TotpSecret | null>;
3660
4265
  delete(userId: string): Promise<void>;
3661
4266
  }
3662
- /**
3663
- * Manages TOTP-based Multi-Factor Authentication.
3664
- *
3665
- * Implements RFC 6238 (TOTP) and RFC 4226 (HOTP) with Web Crypto API.
3666
- * Compatible with Google Authenticator, Authy, 1Password, and other
3667
- * TOTP-compatible authenticator apps.
3668
- *
3669
- * @example
3670
- * ```typescript
3671
- * const totp = new TotpManager({
3672
- * issuer: 'MyApp',
3673
- * store: new InMemoryTotpStore(),
3674
- * })
3675
- *
3676
- * // Step 1: Enable MFA (returns QR code URI and recovery codes)
3677
- * const setup = await totp.enable('user-123', 'alice@example.com')
3678
- * // Show setup.uri as QR code, show setup.recoveryCodes once
3679
- *
3680
- * // Step 2: Verify setup with a code from authenticator app
3681
- * await totp.verifySetup('user-123', '123456')
3682
- *
3683
- * // Step 3: On login, verify TOTP code
3684
- * const valid = await totp.verify('user-123', '654321')
3685
- * ```
3686
- */
3687
4267
  declare class TotpManager {
3688
4268
  private readonly store;
3689
4269
  private readonly issuer;
@@ -3739,7 +4319,19 @@ declare class TotpManager {
3739
4319
  * Get the number of remaining recovery codes for a user.
3740
4320
  */
3741
4321
  remainingRecoveryCodes(userId: string): Promise<number>;
3742
- private validateCode;
4322
+ private isLocked;
4323
+ private assertNotLocked;
4324
+ /**
4325
+ * Count a failed code check. After FREE_FAILURES consecutive failures, code
4326
+ * checks lock for an exponentially growing period (30s, 60s, ... up to 15
4327
+ * minutes), which bounds online guessing of a 6-digit code (AUTH-10) without
4328
+ * a permanent lockout an attacker could trigger at will.
4329
+ */
4330
+ private recordFailure;
4331
+ /** consumeCode plus failure accounting. */
4332
+ private checkTotp;
4333
+ /** A matching time-step that has not been consumed yet, or null. */
4334
+ private matchFreshTimeStep;
3743
4335
  /**
3744
4336
  * Find the TOTP time-step counter (within the acceptance window) whose
3745
4337
  * generated code matches `code`, or null if none matches. Comparison is
@@ -3922,6 +4514,19 @@ interface AdminApiConfig {
3922
4514
  sessionStore?: SessionStore;
3923
4515
  /** Audit logger (optional) */
3924
4516
  auditLogger?: AuditLogger;
4517
+ /**
4518
+ * Authorization check for the acting admin. When set, every method that
4519
+ * takes an `adminId` throws {@link AdminUnauthorizedError} unless it returns
4520
+ * true. Without it, the app must gate access to AdminApi itself.
4521
+ */
4522
+ isAdmin?: (adminId: string) => boolean | Promise<boolean>;
4523
+ /**
4524
+ * Revoke every credential of a user (defaults to the user store's token
4525
+ * revocation store). Called by `revokeUserSessions`, `suspend`-style flows
4526
+ * and `deleteUser`, so JWTs and refresh tokens die with the session rows.
4527
+ * Pass `authServer.revokeAllForUser` to also end live sync sessions.
4528
+ */
4529
+ revokeAllForUser?: (userId: string) => Promise<void>;
3925
4530
  }
3926
4531
  /**
3927
4532
  * Paginated result set.
@@ -3990,7 +4595,13 @@ declare class AdminApi {
3990
4595
  private readonly userStore;
3991
4596
  private readonly sessionStore;
3992
4597
  private readonly auditLogger;
4598
+ private readonly isAdmin;
4599
+ private readonly revokeAllForUserFn;
3993
4600
  constructor(config: AdminApiConfig);
4601
+ /** Throws AdminUnauthorizedError unless the configured `isAdmin` accepts the actor. */
4602
+ private authorize;
4603
+ /** Kill every JWT and refresh token of a user, not just session rows (AUTH-7b/AUTH-14). */
4604
+ private revokeCredentials;
3994
4605
  /**
3995
4606
  * Get a user by ID with full details.
3996
4607
  */
@@ -4150,9 +4761,24 @@ declare class InMemoryWebhookStore implements WebhookStore {
4150
4761
  declare class WebhookManager {
4151
4762
  private readonly store;
4152
4763
  private readonly fetchFn;
4764
+ private readonly allowPrivateTargets;
4765
+ private readonly resolveHost;
4766
+ /**
4767
+ * @param config.store - Endpoint and delivery storage
4768
+ * @param config.fetch - Custom fetch. A custom fetch (for example through an
4769
+ * egress proxy) owns its own network policy, so Kora skips its delivery-time
4770
+ * DNS check unless `resolveHost` is also given.
4771
+ * @param config.allowPrivateTargets - Development only: allow http:// and
4772
+ * loopback/private/link-local targets. Default false.
4773
+ * @param config.resolveHost - Resolve a hostname to addresses at delivery time
4774
+ * (defaults to node:dns when no custom fetch is given), so a public name that
4775
+ * points at a private address is refused (SSRF, DNS rebinding).
4776
+ */
4153
4777
  constructor(config: {
4154
4778
  store: WebhookStore;
4155
4779
  fetch?: typeof globalThis.fetch;
4780
+ allowPrivateTargets?: boolean;
4781
+ resolveHost?: (host: string) => Promise<string[]>;
4156
4782
  });
4157
4783
  /**
4158
4784
  * Register a new webhook endpoint.
@@ -4192,11 +4818,26 @@ declare class WebhookManager {
4192
4818
  */
4193
4819
  dispatch(event: WebhookEvent, data: Record<string, unknown>): Promise<void>;
4194
4820
  private deliverToEndpoint;
4821
+ private assertDeliverable;
4822
+ }
4823
+ /** Thrown when a webhook URL points at a non-public target. */
4824
+ declare class WebhookTargetError extends WebhookError {
4825
+ constructor(url: string);
4195
4826
  }
4196
4827
  /**
4197
- * Verify a webhook payload signature.
4198
- * Useful for consumers of webhooks to verify authenticity.
4828
+ * Verify a webhook payload signature (`X-Webhook-Signature: t=<unix>,v1=<hex>`).
4829
+ *
4830
+ * The HMAC covers `${t}.${payload}`, and a delivery older (or newer) than
4831
+ * `toleranceSeconds` is rejected, so a captured delivery cannot be replayed.
4832
+ *
4833
+ * @param payload - The raw request body
4834
+ * @param signature - The `X-Webhook-Signature` header
4835
+ * @param secret - The endpoint secret
4836
+ * @param options - `toleranceSeconds` (default 300)
4837
+ * @returns True when the signature is valid and fresh
4199
4838
  */
4200
- declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
4839
+ declare function verifyWebhookSignature(payload: string, signature: string, secret: string, options?: {
4840
+ toleranceSeconds?: number;
4841
+ }): Promise<boolean>;
4201
4842
 
4202
- export { AdminApi, type AdminApiConfig, AdminApiError, AdminUnauthorizedError, AdminUserNotFoundError, type AdminUserUpdate, type AuditAction, type AuditEntry, AuditLogError, type AuditLogQuery, type AuditLogStore, AuditLogger, type AuthDevice, type AuthProviderAdapter, AuthProviderError, type AuthRouteResponse, type AuthRoutesConfig, type AuthUser, type AuthenticationOptions, type AuthenticationVerificationResult, BUILT_IN_ROLES, BuiltInAuthRoutes, BuiltInProvider, CannotRemoveOwnerError, type ChallengeStore, CircularInheritanceError, type ClerkAdapterConfig, type CollectionScopeResolver, type CreateInvitationParams, type CreateKoraAuthServerOptions, type CreateOrgParams, type CreateSessionParams, DuplicateEmailError, DuplicateLinkedIdentityError, type EmailVerificationConfig, EmailVerificationError, EmailVerificationManager, type EmailVerificationStore, type EmailVerificationToken, ExternalAuthOperationNotSupportedError, ExternalJwtProvider, type ExternalJwtProviderConfig, ExternalTokenValidationError, type ExternalUserInfo, INVITATION_STATUSES, InMemoryAuditLogStore, InMemoryChallengeStore, InMemoryEmailVerificationStore, InMemoryLinkedIdentityStore, InMemoryOAuthStateStore, InMemoryOrgStore, InMemoryPasswordResetStore, InMemoryRateLimiter, InMemorySessionStore, InMemoryTokenRevocationStore, InMemoryTotpStore, InMemoryUserStore, InMemoryWebhookStore, InsufficientRoleError, InvalidPermissionError, InvitationExpiredError, InvitationNotFoundError, type InvitationStatus, type KoraAuthHttpRequest, type KoraAuthServer, type LinkedIdentity, type LinkedIdentityStore, MemberAlreadyExistsError, type Membership, MembershipNotFoundError, OAuthCodeExchangeError, OAuthError, OAuthManager, type OAuthManagerConfig, type OAuthProviderConfig, OAuthProviderNotFoundError, type OAuthServerConfig, type OAuthState, OAuthStateMismatchError, type OAuthStateStore, type OAuthTokens, type OAuthUserInfo, OAuthUserInfoError, ORG_ROLES, OrgError, type OrgInvitation, OrgNotFoundError, type OrgRole, type OrgRouteResponse, OrgRoutes, type OrgRoutesConfig, OrgScopeResolver, OrgSlugTakenError, type OrgStore, type Organization, type PaginatedResult, PasskeyVerificationError, type PasswordResetConfig, PasswordResetError, PasswordResetManager, type PasswordResetStore, type PasswordResetToken, type Permission, PostgresLinkedIdentityStore, PostgresOAuthStateStore, PostgresUserStore, ROLE_HIERARCHY, type RateLimiter, type RbacConfig, RbacEngine, RbacError, type RegistrationOptions, type RegistrationVerificationResult, ResetRateLimitedError, ResetTokenExpiredError, ResetTokenNotFoundError, type RoleDefinition, RoleNotFoundError, type ScopeContext, type ScopeFilter, type Session, SessionError, SessionExpiredError, SessionLimitExceededError, SessionManager, type SessionManagerConfig, SessionMfaRequiredError, SessionNotFoundError, type SessionStore, type SignInParams, type SignUpParams, SqliteLinkedIdentityStore, SqliteOAuthStateStore, SqliteUserStore, type StoredUser, type SupabaseAdapterConfig, type SyncScopes, TokenManager, type TokenManagerConfig, type TokenRevocationStore, TotpAlreadyEnabledError, type TotpConfig, TotpError, TotpInvalidCodeError, TotpManager, TotpNotEnabledError, TotpNotVerifiedError, TotpRecoveryExhaustedError, type TotpSecret, type TotpSetupResult, type TotpStore, type UpdateOrgParams, type UserListQuery, type UserStore, VerificationTokenExpiredError, VerificationTokenNotFoundError, type WebhookDelivery, type WebhookEndpoint, WebhookEndpointNotFoundError, WebhookError, type WebhookEvent, WebhookManager, type WebhookPayload, type WebhookStore, base32Decode, base32Encode, createClerkAdapter, createKoraAuthServer, createPostgresLinkedIdentityStore, createPostgresOAuthStateStore, createPostgresOAuthStores, createPostgresUserStore, createSqliteLinkedIdentityStore, createSqliteOAuthStateStore, createSqliteOAuthStores, createSqliteUserStore, createSupabaseAdapter, decodeJwt, defineRoles, encodeJwt, generateAuthenticationOptions, generateRegistrationOptions, githubProvider, googleProvider, hasRoleLevel, hashPassword, isExpired, microsoftProvider, parsePermission, permissionCovers, verifyAuthenticationResponse, verifyJwt, verifyPassword, verifyRegistrationResponse, verifyWebhookSignature };
4843
+ export { AdminApi, type AdminApiConfig, AdminApiError, AdminUnauthorizedError, AdminUserNotFoundError, type AdminUserUpdate, type AuditAction, type AuditEntry, AuditLogError, type AuditLogQuery, type AuditLogStore, AuditLogger, type AuthDevice, type AuthProviderAdapter, AuthProviderError, type AuthRevocationEvent, type AuthRouteResponse, type AuthRoutesConfig, type AuthUser, type AuthenticatedAccess, type AuthenticationOptions, type AuthenticationVerificationResult, BUILT_IN_ROLES, BuiltInAuthRoutes, BuiltInProvider, CannotRemoveOwnerError, type ChallengeStore, CircularInheritanceError, type ClerkAdapterConfig, type CollectionScopeResolver, type ConsumeResult, type CreateInvitationParams, type CreateKoraAuthServerOptions, type CreateOrgParams, type CreateSessionParams, DEFAULT_REFRESH_REUSE_GRACE_MS, DeviceOwnershipError, DuplicateEmailError, DuplicateLinkedIdentityError, type EmailVerificationConfig, EmailVerificationError, EmailVerificationManager, type EmailVerificationStore, type EmailVerificationToken, ExternalAuthOperationNotSupportedError, ExternalJwtProvider, type ExternalJwtProviderConfig, ExternalTokenValidationError, type ExternalUserInfo, INVITATION_STATUSES, InMemoryAuditLogStore, InMemoryAuthStoreError, InMemoryChallengeStore, InMemoryEmailVerificationStore, InMemoryLinkedIdentityStore, InMemoryOAuthStateStore, InMemoryOrgStore, InMemoryPasswordResetStore, InMemoryRateLimiter, InMemorySessionStore, InMemoryTokenRevocationStore, InMemoryTotpStore, InMemoryUserStore, InMemoryWebhookStore, InsufficientRoleError, InvalidPermissionError, InvitationExpiredError, InvitationNotFoundError, type InvitationStatus, type IssueTokenOptions, type KoraAuthHttpRequest, type KoraAuthServer, type LinkedIdentity, type LinkedIdentityStore, MemberAlreadyExistsError, type Membership, MembershipNotFoundError, type MfaChallenge, type MfaPendingPayload, type MfaVerifier, OAuthCodeExchangeError, OAuthError, OAuthManager, type OAuthManagerConfig, type OAuthProviderConfig, OAuthProviderNotFoundError, type OAuthServerConfig, type OAuthState, OAuthStateMismatchError, type OAuthStateStore, type OAuthTokens, type OAuthUserInfo, OAuthUserInfoError, ORG_ROLES, OrgError, type OrgInvitation, OrgNotFoundError, type OrgRole, type OrgRouteResponse, OrgRoutes, type OrgRoutesConfig, OrgScopeResolver, OrgSlugTakenError, type OrgStore, type Organization, type PaginatedResult, PasskeyVerificationError, type PasswordResetConfig, PasswordResetError, PasswordResetManager, type PasswordResetStore, type PasswordResetToken, type Permission, PostgresLinkedIdentityStore, PostgresOAuthStateStore, type PostgresRevocationClient, PostgresTokenRevocationStore, PostgresUserStore, type PostgresUserStoreOptions, ROLE_HIERARCHY, type RateLimiter, type RbacConfig, RbacEngine, RbacError, type RefreshFailureReason, type RefreshResult, type RegistrationOptions, type RegistrationVerificationResult, ResetRateLimitedError, ResetTokenExpiredError, ResetTokenNotFoundError, type RoleDefinition, RoleNotFoundError, type ScopeContext, type ScopeFilter, type Session, SessionError, SessionExpiredError, SessionLimitExceededError, SessionManager, type SessionManagerConfig, SessionMfaRequiredError, SessionNotFoundError, type SessionStore, type SignInParams, type SignInResult, type SignUpParams, SqliteLinkedIdentityStore, SqliteOAuthStateStore, type SqliteRevocationDatabase, SqliteTokenRevocationStore, SqliteUserStore, type StoredUser, type SupabaseAdapterConfig, type SyncAuthContext, type SyncAuthProvider, type SyncScopeOptions, type SyncScopes, type SyncSessionTerminator, TokenManager, type TokenManagerConfig, type TokenRevocationStore, TotpAlreadyEnabledError, type TotpConfig, TotpError, TotpInvalidCodeError, TotpLockedError, TotpManager, TotpNotEnabledError, TotpNotVerifiedError, TotpRecoveryExhaustedError, type TotpSecret, type TotpSetupResult, type TotpStore, type UpdateOrgParams, type UserListQuery, type UserStore, VerificationTokenExpiredError, VerificationTokenNotFoundError, type VerifiedSyncClaims, type WebhookDelivery, type WebhookEndpoint, WebhookEndpointNotFoundError, WebhookError, type WebhookEvent, WebhookManager, type WebhookPayload, type WebhookStore, WebhookTargetError, base32Decode, base32Encode, createClerkAdapter, createKoraAuthServer, createPostgresLinkedIdentityStore, createPostgresOAuthStateStore, createPostgresOAuthStores, createPostgresUserStore, createSqliteLinkedIdentityStore, createSqliteOAuthStateStore, createSqliteOAuthStores, createSqliteUserStore, createSupabaseAdapter, decodeJwt, defineRoles, encodeJwt, generateAuthenticationOptions, generateRegistrationOptions, githubProvider, googleProvider, hasRoleLevel, hashPassword, isExpired, microsoftProvider, parsePermission, permissionCovers, verifyAuthenticationResponse, verifyJwt, verifyPassword, verifyRegistrationResponse, verifyWebhookSignature };