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

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 +2852 -1675
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +779 -168
  17. package/dist/server.d.ts +779 -168
  18. package/dist/server.js +2831 -1665
  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 +328 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +322 -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 +272 -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 +334 -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;
100
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[];
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>;
260
+ /**
261
+ * Redeem an `mfa_pending` token exactly once (atomic across instances).
262
+ *
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>;
193
267
  /**
194
- * Validate and decode a token.
268
+ * Validate and decode a token's signature, expiry and claims.
195
269
  *
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.
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).
314
+ *
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.
320
+ *
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).
237
327
  *
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.
328
+ * @param refreshToken - The refresh token JWT string
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.
242
334
  *
243
- * Returns null if the provided token is invalid, expired, or not a refresh token.
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.
244
338
  *
245
339
  * @param refreshToken - The refresh token JWT string
246
- * @returns A new access/refresh token pair, or null if the refresh token is invalid
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;
@@ -1711,7 +2201,10 @@ interface PostgresClient$1 {
1711
2201
  declare class PostgresUserStore implements UserStore {
1712
2202
  private readonly sql;
1713
2203
  private readonly ready;
2204
+ private revocationStore;
1714
2205
  constructor(sql: PostgresClient$1);
2206
+ /** Token revocations stored in the same Postgres database as the users. */
2207
+ getTokenRevocationStore(): TokenRevocationStore;
1715
2208
  private ensureTables;
1716
2209
  createUser(params: {
1717
2210
  email: string;
@@ -2044,6 +2537,22 @@ interface ExternalJwtProviderConfig {
2044
2537
  * ```
2045
2538
  */
2046
2539
  mapClaims?: (claims: Record<string, unknown>) => ExternalUserInfo;
2540
+ /**
2541
+ * Accepted audience(s) (`aud`). When set, tokens for any other audience are
2542
+ * rejected, even if they share the signing secret. Defaults to
2543
+ * `'authenticated'` when `providerName` is `'supabase'`.
2544
+ */
2545
+ audience?: string | string[];
2546
+ /** Accepted issuer(s) (`iss`). When set, tokens from any other issuer are rejected. */
2547
+ issuer?: string | string[];
2548
+ /**
2549
+ * Verified scope values for the sync grant, derived from the validated claims
2550
+ * (AUTH-1). Merged over `{ userId }`; every schema-scoped collection is bound
2551
+ * from them, and a collection whose binding is missing is denied.
2552
+ */
2553
+ scopeValues?: (claims: Record<string, unknown>) => Record<string, unknown>;
2554
+ /** Full explicit sync grant from the validated claims (replaces the default). */
2555
+ resolveScopes?: (claims: Record<string, unknown>) => ScopeMap | Promise<ScopeMap>;
2047
2556
  }
2048
2557
  /**
2049
2558
  * Authentication provider adapter for external JWT issuers.
@@ -2088,6 +2597,10 @@ declare class ExternalJwtProvider implements AuthProviderAdapter {
2088
2597
  private readonly jwtSecret;
2089
2598
  private readonly customValidateToken;
2090
2599
  private readonly mapClaims;
2600
+ private readonly audiences;
2601
+ private readonly issuers;
2602
+ private readonly scopeValues;
2603
+ private readonly resolveScopes;
2091
2604
  constructor(config: ExternalJwtProviderConfig);
2092
2605
  /**
2093
2606
  * Not supported for external providers.
@@ -2200,6 +2713,8 @@ declare class ExternalJwtProvider implements AuthProviderAdapter {
2200
2713
  * @returns The decoded claims object, or null if the token is invalid
2201
2714
  */
2202
2715
  private extractClaims;
2716
+ /** Enforce configured `aud` / `iss` so tokens for another service are refused. */
2717
+ private audienceAndIssuerMatch;
2203
2718
  }
2204
2719
 
2205
2720
  /**
@@ -2506,6 +3021,12 @@ declare function verifyRegistrationResponse(params: {
2506
3021
  expectedChallenge: string;
2507
3022
  expectedOrigin: string;
2508
3023
  expectedRpId: string;
3024
+ /**
3025
+ * Require the User Verified (UV) flag. Kora's options request
3026
+ * `userVerification: 'required'`, so the response must honour it.
3027
+ * @default true
3028
+ */
3029
+ requireUserVerification?: boolean;
2509
3030
  }): Promise<RegistrationVerificationResult>;
2510
3031
  /** Options returned by generateAuthenticationOptions for the client. */
2511
3032
  interface AuthenticationOptions {
@@ -2605,6 +3126,12 @@ declare function verifyAuthenticationResponse(params: {
2605
3126
  expectedRpId: string;
2606
3127
  publicKey: string;
2607
3128
  previousSignCount: number;
3129
+ /**
3130
+ * Require the User Verified (UV) flag. Kora's options request
3131
+ * `userVerification: 'required'`, so the assertion must honour it.
3132
+ * @default true
3133
+ */
3134
+ requireUserVerification?: boolean;
2608
3135
  }): Promise<AuthenticationVerificationResult>;
2609
3136
 
2610
3137
  /**
@@ -2808,7 +3335,11 @@ interface OrgStore {
2808
3335
  /** Consume an invitation (mark as accepted). Returns the invitation details. */
2809
3336
  consumeInvitation(token: string): Promise<OrgInvitation>;
2810
3337
  /** Revoke a pending invitation. */
2811
- revokeInvitation(invitationId: string): Promise<void>;
3338
+ /**
3339
+ * Revoke a pending invitation of `orgId`. Must throw InvitationNotFoundError
3340
+ * when the invitation belongs to another org (AUTH-4).
3341
+ */
3342
+ revokeInvitation(orgId: string, invitationId: string): Promise<void>;
2812
3343
  /** List pending invitations for an organization. */
2813
3344
  listPendingInvitations(orgId: string): Promise<OrgInvitation[]>;
2814
3345
  /** List pending invitations for a specific email address. */
@@ -2847,7 +3378,7 @@ declare class InMemoryOrgStore implements OrgStore {
2847
3378
  createInvitation(orgId: string, invitedBy: string, params: CreateInvitationParams): Promise<OrgInvitation>;
2848
3379
  getInvitationByToken(token: string): Promise<OrgInvitation | null>;
2849
3380
  consumeInvitation(token: string): Promise<OrgInvitation>;
2850
- revokeInvitation(invitationId: string): Promise<void>;
3381
+ revokeInvitation(orgId: string, invitationId: string): Promise<void>;
2851
3382
  listPendingInvitations(orgId: string): Promise<OrgInvitation[]>;
2852
3383
  listInvitationsForEmail(email: string): Promise<OrgInvitation[]>;
2853
3384
  cleanExpiredInvitations(): Promise<number>;
@@ -2873,7 +3404,26 @@ interface OrgRouteResponse<T> {
2873
3404
  interface OrgRoutesConfig {
2874
3405
  /** The organization store backing all org operations */
2875
3406
  orgStore: OrgStore;
3407
+ /**
3408
+ * Resolves a user's email and its verification status (a `UserStore`
3409
+ * satisfies this). Invitations are listed and accepted only for the caller's
3410
+ * own VERIFIED email; without a lookup, pass the verified identity to
3411
+ * `acceptInvitation` / `listMyInvitations` explicitly.
3412
+ */
3413
+ userLookup?: {
3414
+ findById(userId: string): Promise<{
3415
+ email: string;
3416
+ emailVerified: boolean;
3417
+ } | null>;
3418
+ };
3419
+ }
3420
+ /** A server-verified email identity used to match invitations. */
3421
+ interface VerifiedEmailIdentity {
3422
+ email: string;
3423
+ emailVerified: boolean;
2876
3424
  }
3425
+ /** Invitation as shown to its invitee: the secret token is never included. */
3426
+ type InviteeInvitation = Omit<OrgInvitation, 'token'>;
2877
3427
  /**
2878
3428
  * Server-side route handlers for organization management.
2879
3429
  *
@@ -2893,6 +3443,7 @@ interface OrgRoutesConfig {
2893
3443
  */
2894
3444
  declare class OrgRoutes {
2895
3445
  private readonly store;
3446
+ private readonly userLookup;
2896
3447
  constructor(config: OrgRoutesConfig);
2897
3448
  /**
2898
3449
  * Create a new organization. The authenticated user becomes the owner.
@@ -2965,11 +3516,17 @@ declare class OrgRoutes {
2965
3516
  role?: unknown;
2966
3517
  }): Promise<OrgRouteResponse<OrgInvitation>>;
2967
3518
  /**
2968
- * Accept an invitation by its token. The authenticated user joins the org.
3519
+ * Accept an invitation by its token. The authenticated user joins the org
3520
+ * only if the invitation was addressed to the user's own verified email.
3521
+ *
3522
+ * @param userId - The authenticated user
3523
+ * @param params - The invitation token
3524
+ * @param identity - The user's verified email identity; resolved through
3525
+ * `userLookup` when omitted
2969
3526
  */
2970
3527
  acceptInvitation(userId: string, params: {
2971
3528
  token?: unknown;
2972
- }): Promise<OrgRouteResponse<Membership>>;
3529
+ }, identity?: VerifiedEmailIdentity): Promise<OrgRouteResponse<Membership>>;
2973
3530
  /**
2974
3531
  * Revoke a pending invitation. Requires admin or higher.
2975
3532
  */
@@ -2981,9 +3538,15 @@ declare class OrgRoutes {
2981
3538
  */
2982
3539
  listPendingInvitations(userId: string, orgId: string): Promise<OrgRouteResponse<OrgInvitation[]>>;
2983
3540
  /**
2984
- * List pending invitations for the authenticated user's email.
3541
+ * List pending invitations addressed to the authenticated user's own
3542
+ * verified email. The email is resolved server-side (never taken from the
3543
+ * request) and invitation tokens are never returned.
3544
+ *
3545
+ * @param userId - The authenticated user
3546
+ * @param identity - The user's verified identity; resolved through `userLookup` when omitted
2985
3547
  */
2986
- listMyInvitations(email: string): Promise<OrgRouteResponse<OrgInvitation[]>>;
3548
+ listMyInvitations(userId: string, identity?: VerifiedEmailIdentity): Promise<OrgRouteResponse<InviteeInvitation[]>>;
3549
+ private resolveIdentity;
2987
3550
  /**
2988
3551
  * Check if the caller has the required role in the org.
2989
3552
  * Returns an error response if not authorized, or null if authorized.
@@ -3609,6 +4172,10 @@ interface TotpSecret {
3609
4172
  * until the first code is consumed.
3610
4173
  */
3611
4174
  lastUsedTimeStep?: number;
4175
+ /** Consecutive failed code checks (TOTP or recovery). Reset on success. */
4176
+ failedAttempts?: number;
4177
+ /** While set and in the future, every code check fails without being evaluated. */
4178
+ lockedUntil?: number;
3612
4179
  }
3613
4180
  /**
3614
4181
  * Setup result returned when enabling TOTP MFA.
@@ -3635,6 +4202,14 @@ interface TotpStore {
3635
4202
  declare class TotpError extends KoraError {
3636
4203
  constructor(message: string, code: string, context?: Record<string, unknown>);
3637
4204
  }
4205
+ /**
4206
+ * Thrown by `disable` / `regenerateRecoveryCodes` while code checks are locked
4207
+ * after repeated failures (AUTH-10). `verify` returns false instead.
4208
+ */
4209
+ declare class TotpLockedError extends TotpError {
4210
+ readonly lockedUntil: number;
4211
+ constructor(lockedUntil: number);
4212
+ }
3638
4213
  declare class TotpInvalidCodeError extends TotpError {
3639
4214
  constructor();
3640
4215
  }
@@ -3659,31 +4234,6 @@ declare class InMemoryTotpStore implements TotpStore {
3659
4234
  getByUserId(userId: string): Promise<TotpSecret | null>;
3660
4235
  delete(userId: string): Promise<void>;
3661
4236
  }
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
4237
  declare class TotpManager {
3688
4238
  private readonly store;
3689
4239
  private readonly issuer;
@@ -3739,7 +4289,19 @@ declare class TotpManager {
3739
4289
  * Get the number of remaining recovery codes for a user.
3740
4290
  */
3741
4291
  remainingRecoveryCodes(userId: string): Promise<number>;
3742
- private validateCode;
4292
+ private isLocked;
4293
+ private assertNotLocked;
4294
+ /**
4295
+ * Count a failed code check. After FREE_FAILURES consecutive failures, code
4296
+ * checks lock for an exponentially growing period (30s, 60s, ... up to 15
4297
+ * minutes), which bounds online guessing of a 6-digit code (AUTH-10) without
4298
+ * a permanent lockout an attacker could trigger at will.
4299
+ */
4300
+ private recordFailure;
4301
+ /** consumeCode plus failure accounting. */
4302
+ private checkTotp;
4303
+ /** A matching time-step that has not been consumed yet, or null. */
4304
+ private matchFreshTimeStep;
3743
4305
  /**
3744
4306
  * Find the TOTP time-step counter (within the acceptance window) whose
3745
4307
  * generated code matches `code`, or null if none matches. Comparison is
@@ -3922,6 +4484,19 @@ interface AdminApiConfig {
3922
4484
  sessionStore?: SessionStore;
3923
4485
  /** Audit logger (optional) */
3924
4486
  auditLogger?: AuditLogger;
4487
+ /**
4488
+ * Authorization check for the acting admin. When set, every method that
4489
+ * takes an `adminId` throws {@link AdminUnauthorizedError} unless it returns
4490
+ * true. Without it, the app must gate access to AdminApi itself.
4491
+ */
4492
+ isAdmin?: (adminId: string) => boolean | Promise<boolean>;
4493
+ /**
4494
+ * Revoke every credential of a user (defaults to the user store's token
4495
+ * revocation store). Called by `revokeUserSessions`, `suspend`-style flows
4496
+ * and `deleteUser`, so JWTs and refresh tokens die with the session rows.
4497
+ * Pass `authServer.revokeAllForUser` to also end live sync sessions.
4498
+ */
4499
+ revokeAllForUser?: (userId: string) => Promise<void>;
3925
4500
  }
3926
4501
  /**
3927
4502
  * Paginated result set.
@@ -3990,7 +4565,13 @@ declare class AdminApi {
3990
4565
  private readonly userStore;
3991
4566
  private readonly sessionStore;
3992
4567
  private readonly auditLogger;
4568
+ private readonly isAdmin;
4569
+ private readonly revokeAllForUserFn;
3993
4570
  constructor(config: AdminApiConfig);
4571
+ /** Throws AdminUnauthorizedError unless the configured `isAdmin` accepts the actor. */
4572
+ private authorize;
4573
+ /** Kill every JWT and refresh token of a user, not just session rows (AUTH-7b/AUTH-14). */
4574
+ private revokeCredentials;
3994
4575
  /**
3995
4576
  * Get a user by ID with full details.
3996
4577
  */
@@ -4150,9 +4731,24 @@ declare class InMemoryWebhookStore implements WebhookStore {
4150
4731
  declare class WebhookManager {
4151
4732
  private readonly store;
4152
4733
  private readonly fetchFn;
4734
+ private readonly allowPrivateTargets;
4735
+ private readonly resolveHost;
4736
+ /**
4737
+ * @param config.store - Endpoint and delivery storage
4738
+ * @param config.fetch - Custom fetch. A custom fetch (for example through an
4739
+ * egress proxy) owns its own network policy, so Kora skips its delivery-time
4740
+ * DNS check unless `resolveHost` is also given.
4741
+ * @param config.allowPrivateTargets - Development only: allow http:// and
4742
+ * loopback/private/link-local targets. Default false.
4743
+ * @param config.resolveHost - Resolve a hostname to addresses at delivery time
4744
+ * (defaults to node:dns when no custom fetch is given), so a public name that
4745
+ * points at a private address is refused (SSRF, DNS rebinding).
4746
+ */
4153
4747
  constructor(config: {
4154
4748
  store: WebhookStore;
4155
4749
  fetch?: typeof globalThis.fetch;
4750
+ allowPrivateTargets?: boolean;
4751
+ resolveHost?: (host: string) => Promise<string[]>;
4156
4752
  });
4157
4753
  /**
4158
4754
  * Register a new webhook endpoint.
@@ -4192,11 +4788,26 @@ declare class WebhookManager {
4192
4788
  */
4193
4789
  dispatch(event: WebhookEvent, data: Record<string, unknown>): Promise<void>;
4194
4790
  private deliverToEndpoint;
4791
+ private assertDeliverable;
4792
+ }
4793
+ /** Thrown when a webhook URL points at a non-public target. */
4794
+ declare class WebhookTargetError extends WebhookError {
4795
+ constructor(url: string);
4195
4796
  }
4196
4797
  /**
4197
- * Verify a webhook payload signature.
4198
- * Useful for consumers of webhooks to verify authenticity.
4798
+ * Verify a webhook payload signature (`X-Webhook-Signature: t=<unix>,v1=<hex>`).
4799
+ *
4800
+ * The HMAC covers `${t}.${payload}`, and a delivery older (or newer) than
4801
+ * `toleranceSeconds` is rejected, so a captured delivery cannot be replayed.
4802
+ *
4803
+ * @param payload - The raw request body
4804
+ * @param signature - The `X-Webhook-Signature` header
4805
+ * @param secret - The endpoint secret
4806
+ * @param options - `toleranceSeconds` (default 300)
4807
+ * @returns True when the signature is valid and fresh
4199
4808
  */
4200
- declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
4809
+ declare function verifyWebhookSignature(payload: string, signature: string, secret: string, options?: {
4810
+ toleranceSeconds?: number;
4811
+ }): Promise<boolean>;
4201
4812
 
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 };
4813
+ 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, 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 };