@korajs/auth 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.ts CHANGED
@@ -1,32 +1,111 @@
1
- import { A as AuthTokens, T as TokenPayload } from './device-identity-DiwdLsUB.js';
2
- export { f as computePublicKeyThumbprint, v as verifyChallenge } from './device-identity-DiwdLsUB.js';
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
3
  import { KoraError } from '@korajs/core';
4
4
 
5
+ /**
6
+ * Interface for server-side token revocation storage.
7
+ *
8
+ * 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
12
+ *
13
+ * The in-memory implementation ({@link InMemoryTokenRevocationStore}) is suitable
14
+ * for development. Production deployments should use a persistent store (Redis, database).
15
+ */
16
+ interface TokenRevocationStore {
17
+ /**
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
21
+ */
22
+ isRevoked(jti: string): Promise<boolean>;
23
+ /**
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.
28
+ */
29
+ revoke(jti: string, expiresAt: number): Promise<void>;
30
+ /**
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
34
+ */
35
+ revokeAllForDevice(deviceId: string): Promise<void>;
36
+ }
37
+ /**
38
+ * In-memory token revocation store.
39
+ *
40
+ * Suitable for development and testing. Revoked tokens are stored in a Set
41
+ * and automatically cleaned up when they would have expired naturally.
42
+ *
43
+ * **Not suitable for production**: revocations are lost on server restart
44
+ * and not shared across server instances. Use a Redis or database-backed
45
+ * store in production.
46
+ */
47
+ declare class InMemoryTokenRevocationStore implements TokenRevocationStore {
48
+ private readonly revokedTokens;
49
+ private readonly revokedDevices;
50
+ isRevoked(jti: string): Promise<boolean>;
51
+ revoke(jti: string, expiresAt: number): Promise<void>;
52
+ revokeAllForDevice(deviceId: string): Promise<void>;
53
+ /**
54
+ * Check if a device has been revoked.
55
+ */
56
+ isDeviceRevoked(deviceId: string): boolean;
57
+ /**
58
+ * Remove expired revocations to prevent unbounded memory growth.
59
+ * Call periodically (e.g., every hour) in long-running servers.
60
+ */
61
+ cleanup(): void;
62
+ }
5
63
  /**
6
64
  * Configuration for the server-side TokenManager.
7
65
  */
8
66
  interface TokenManagerConfig {
9
- /** Secret key for signing JWTs (HMAC-SHA256) */
10
- secret: string;
67
+ /**
68
+ * Secret key for signing JWTs (HMAC-SHA256).
69
+ *
70
+ * Must be at least 32 characters (256 bits). Use {@link TokenManager.generateSecret}
71
+ * to create a cryptographically random secret.
72
+ *
73
+ * For key rotation, provide an array of secrets. The first secret is used for
74
+ * signing new tokens; all secrets are tried during verification (newest first).
75
+ * This allows graceful rotation: add the new secret at index 0, then remove
76
+ * the old secret after all tokens signed with it have expired.
77
+ */
78
+ secret: string | string[];
11
79
  /** Access token lifetime in milliseconds (default: 15 minutes) */
12
80
  accessTokenLifetime?: number;
13
81
  /** Refresh token lifetime in milliseconds (default: 90 days) */
14
82
  refreshTokenLifetime?: number;
15
83
  /** Device credential lifetime in milliseconds (default: 90 days) */
16
84
  deviceCredentialLifetime?: number;
85
+ /**
86
+ * Optional token revocation store. When provided, enables:
87
+ * - Individual token revocation via `revokeToken()`
88
+ * - Refresh token reuse detection (consumed tokens are tracked)
89
+ * - Device-level token invalidation via `revokeDeviceTokens()`
90
+ *
91
+ * Without a revocation store, tokens are valid until they expire.
92
+ */
93
+ revocationStore?: TokenRevocationStore;
17
94
  }
18
95
  /**
19
96
  * Server-side token manager responsible for issuing, refreshing, and validating
20
97
  * Kora authentication tokens.
21
98
  *
22
- * Uses HMAC-SHA256 signed JWTs. Supports three token types:
23
- * - **Access tokens**: Short-lived tokens for API authorization (default: 15 min)
24
- * - **Refresh tokens**: Longer-lived tokens for obtaining new access tokens (default: 90 days)
25
- * - **Device credentials**: Long-lived credentials bound to a device key pair (default: 90 days)
99
+ * Uses HMAC-SHA256 signed JWTs with unique `jti` identifiers for every token.
100
+ * Supports key rotation (multiple secrets), token revocation, and refresh token
101
+ * reuse detection.
26
102
  *
27
103
  * @example
28
104
  * ```typescript
29
- * const tokenManager = new TokenManager({ secret: 'my-signing-secret' })
105
+ * const tokenManager = new TokenManager({
106
+ * secret: TokenManager.generateSecret(),
107
+ * revocationStore: new InMemoryTokenRevocationStore(),
108
+ * })
30
109
  *
31
110
  * // Issue all tokens at once
32
111
  * const tokens = tokenManager.issueTokens('user-123', 'device-456')
@@ -35,15 +114,26 @@ interface TokenManagerConfig {
35
114
  * const payload = tokenManager.validateToken(tokens.accessToken)
36
115
  *
37
116
  * // Refresh when the access token expires
38
- * const newTokens = tokenManager.refreshAccessToken(tokens.refreshToken)
117
+ * const newTokens = await tokenManager.refreshAccessToken(tokens.refreshToken)
39
118
  * ```
40
119
  */
41
120
  declare class TokenManager {
42
- private readonly secret;
121
+ /** All signing/verification secrets (index 0 = current signing key) */
122
+ private readonly secrets;
43
123
  private readonly accessTokenLifetime;
44
124
  private readonly refreshTokenLifetime;
45
125
  private readonly deviceCredentialLifetime;
126
+ private readonly revocationStore;
46
127
  constructor(config: TokenManagerConfig);
128
+ /**
129
+ * Generate a cryptographically random secret suitable for HMAC-SHA256 signing.
130
+ *
131
+ * Returns a 64-character hex string (32 bytes / 256 bits of entropy).
132
+ * Store this securely (environment variable, secrets manager) — never in source code.
133
+ *
134
+ * @returns A random 256-bit hex-encoded secret
135
+ */
136
+ static generateSecret(): string;
47
137
  /**
48
138
  * Issue a signed JWT access token.
49
139
  *
@@ -97,7 +187,8 @@ declare class TokenManager {
97
187
  /**
98
188
  * Validate and decode a token.
99
189
  *
100
- * Verifies the HMAC-SHA256 signature and checks that the token has not expired.
190
+ * Verifies the HMAC-SHA256 signature (trying all configured secrets for key rotation),
191
+ * checks that the token has not expired, and validates all required claims.
101
192
  * Returns null (rather than throwing) for invalid or expired tokens, so callers
102
193
  * can handle authentication failure without try/catch.
103
194
  *
@@ -106,22 +197,52 @@ declare class TokenManager {
106
197
  * invalid, expired, or missing required claims
107
198
  */
108
199
  validateToken(token: string): TokenPayload | null;
200
+ /**
201
+ * Validate a token and check it against the revocation store.
202
+ *
203
+ * Like {@link validateToken}, but also checks whether the token's `jti` has been
204
+ * revoked. Requires a revocation store to be configured.
205
+ *
206
+ * @param token - The JWT string to validate
207
+ * @returns The decoded {@link TokenPayload} if valid and not revoked, or null otherwise
208
+ */
209
+ validateTokenWithRevocation(token: string): Promise<TokenPayload | null>;
210
+ /**
211
+ * Revoke a specific token by its JWT ID.
212
+ *
213
+ * Requires a revocation store to be configured. After revocation, the token
214
+ * will be rejected by {@link validateTokenWithRevocation}.
215
+ *
216
+ * @param jti - The JWT ID of the token to revoke
217
+ * @param expiresAt - The token's expiration time (seconds since epoch)
218
+ */
219
+ revokeToken(jti: string, expiresAt: number): Promise<void>;
220
+ /**
221
+ * Revoke all tokens for a specific device.
222
+ *
223
+ * Called when a device is revoked to ensure all its existing tokens
224
+ * (access, refresh, and device credentials) are invalidated.
225
+ *
226
+ * @param deviceId - The device ID whose tokens should be revoked
227
+ */
228
+ revokeDeviceTokens(deviceId: string): Promise<void>;
109
229
  /**
110
230
  * Refresh an access token using a valid refresh token.
111
231
  *
112
- * Implements **refresh token rotation**: a new refresh token is issued alongside
113
- * the new access token, and the old refresh token should be considered consumed.
114
- * This limits the window of vulnerability if a refresh token is compromised.
232
+ * Implements **refresh token rotation with reuse detection**: a new refresh token
233
+ * is issued alongside the new access token. The old refresh token's `jti` is
234
+ * recorded in the revocation store (if configured). If a previously consumed
235
+ * refresh token is presented again, it indicates potential token theft.
115
236
  *
116
237
  * Returns null if the provided token is invalid, expired, or not a refresh token.
117
238
  *
118
239
  * @param refreshToken - The refresh token JWT string
119
240
  * @returns A new access/refresh token pair, or null if the refresh token is invalid
120
241
  */
121
- refreshAccessToken(refreshToken: string): {
242
+ refreshAccessToken(refreshToken: string): Promise<{
122
243
  accessToken: string;
123
244
  refreshToken: string;
124
- } | null;
245
+ } | null>;
125
246
  }
126
247
 
127
248
  /**
@@ -135,6 +256,8 @@ interface AuthUser {
135
256
  email: string;
136
257
  /** User's display name */
137
258
  name: string;
259
+ /** Whether the user's email has been verified */
260
+ emailVerified: boolean;
138
261
  /** Timestamp when the user was created (milliseconds since epoch) */
139
262
  createdAt: number;
140
263
  }
@@ -171,17 +294,72 @@ interface AuthDevice {
171
294
  * Thrown when a user account already exists with the given email.
172
295
  */
173
296
  declare class DuplicateEmailError extends KoraError {
174
- constructor(email: string);
297
+ constructor();
298
+ }
299
+ /**
300
+ * Generic interface for user and device persistence.
301
+ *
302
+ * Implement this interface to provide database-backed user storage for
303
+ * the built-in auth provider. All methods are async to support both
304
+ * synchronous (in-memory, SQLite) and asynchronous (PostgreSQL) backends.
305
+ *
306
+ * Built-in implementations:
307
+ * - {@link InMemoryUserStore} — development and testing (no persistence)
308
+ * - `SqliteUserStore` — SQLite via better-sqlite3 (from `@korajs/auth/server`)
309
+ * - `PostgresUserStore` — PostgreSQL via postgres-js (from `@korajs/auth/server`)
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * import { BuiltInAuthRoutes, SqliteUserStore } from '@korajs/auth/server'
314
+ *
315
+ * const userStore = await createSqliteUserStore({ filename: './auth.db' })
316
+ * const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
317
+ * ```
318
+ */
319
+ interface UserStore {
320
+ /** Create a new user account. Throws DuplicateEmailError if email exists. */
321
+ createUser(params: {
322
+ email: string;
323
+ passwordHash: string;
324
+ salt: string;
325
+ name: string;
326
+ }): Promise<AuthUser>;
327
+ /** Find a user by email address (case-insensitive). */
328
+ findByEmail(email: string): Promise<StoredUser | null>;
329
+ /** Find a user by ID. */
330
+ findById(id: string): Promise<StoredUser | null>;
331
+ /** Register a device for a user. Idempotent if device already exists and is not revoked. */
332
+ registerDevice(params: {
333
+ id: string;
334
+ userId: string;
335
+ publicKey: string;
336
+ name: string;
337
+ }): Promise<AuthDevice>;
338
+ /** Find a device by its ID. */
339
+ findDevice(deviceId: string): Promise<AuthDevice | null>;
340
+ /** List all devices registered for a user (includes revoked). */
341
+ listDevices(userId: string): Promise<AuthDevice[]>;
342
+ /** Soft-revoke a device. No-op if device does not exist. */
343
+ revokeDevice(deviceId: string): Promise<void>;
344
+ /** Set a user's email verification status. */
345
+ setEmailVerified(userId: string, verified: boolean): Promise<void>;
346
+ /** Update a user's password hash and salt. */
347
+ updatePassword(userId: string, passwordHash: string, salt: string): Promise<void>;
348
+ /** List all users. For admin/development use. */
349
+ listAll(): Promise<StoredUser[]>;
350
+ /** Update a stored user record. */
351
+ update(user: StoredUser): Promise<void>;
352
+ /** Delete a user and all associated devices. */
353
+ delete(userId: string): Promise<void>;
354
+ /** Update the last-seen timestamp for a device. No-op if device does not exist. */
355
+ touchDevice(deviceId: string): Promise<void>;
175
356
  }
176
357
  /**
177
358
  * In-memory user and device store for the built-in auth provider.
178
359
  *
179
360
  * This is a simple implementation suitable for development and testing.
180
- * Production applications should replace this with a database-backed store
181
- * implementing the same interface.
182
- *
183
- * All methods are async to match the interface that a real database store
184
- * would expose, even though the in-memory operations are synchronous.
361
+ * Production applications should use {@link SqliteUserStore} or
362
+ * {@link PostgresUserStore} for persistent storage.
185
363
  *
186
364
  * @example
187
365
  * ```typescript
@@ -194,7 +372,7 @@ declare class DuplicateEmailError extends KoraError {
194
372
  * })
195
373
  * ```
196
374
  */
197
- declare class InMemoryUserStore {
375
+ declare class InMemoryUserStore implements UserStore {
198
376
  /** Users indexed by ID */
199
377
  private readonly usersById;
200
378
  /** Users indexed by email (lowercase) for fast lookup */
@@ -277,6 +455,33 @@ declare class InMemoryUserStore {
277
455
  * @param deviceId - The ID of the device to revoke
278
456
  */
279
457
  revokeDevice(deviceId: string): Promise<void>;
458
+ /**
459
+ * Set a user's email verification status.
460
+ *
461
+ * @param userId - The user whose email to verify
462
+ * @param verified - Whether the email is verified
463
+ */
464
+ setEmailVerified(userId: string, verified: boolean): Promise<void>;
465
+ /**
466
+ * Update a user's password hash and salt.
467
+ *
468
+ * @param userId - The user whose password to update
469
+ * @param passwordHash - New hex-encoded PBKDF2 derived key
470
+ * @param salt - New hex-encoded salt
471
+ */
472
+ updatePassword(userId: string, passwordHash: string, salt: string): Promise<void>;
473
+ /**
474
+ * List all users. For admin/development use.
475
+ */
476
+ listAll(): Promise<StoredUser[]>;
477
+ /**
478
+ * Update a stored user record.
479
+ */
480
+ update(user: StoredUser): Promise<void>;
481
+ /**
482
+ * Delete a user and all associated devices.
483
+ */
484
+ delete(userId: string): Promise<void>;
280
485
  /**
281
486
  * Update the last-seen timestamp for a device.
282
487
  *
@@ -288,14 +493,105 @@ declare class InMemoryUserStore {
288
493
  touchDevice(deviceId: string): Promise<void>;
289
494
  }
290
495
 
496
+ /**
497
+ * Interface for server-side challenge storage.
498
+ *
499
+ * Challenges must be stored server-side with expiry and single-use semantics
500
+ * to prevent replay attacks on device verification.
501
+ */
502
+ interface ChallengeStore {
503
+ /**
504
+ * Store a challenge for later verification.
505
+ * @param challenge - The challenge string
506
+ * @param deviceId - The device this challenge is intended for
507
+ * @param expiresAt - Timestamp (ms since epoch) when this challenge expires
508
+ */
509
+ store(challenge: string, deviceId: string, expiresAt: number): Promise<void>;
510
+ /**
511
+ * Consume a challenge (single-use). Returns the associated device ID if the
512
+ * challenge is valid and not expired, or null if it doesn't exist, has expired,
513
+ * or was already consumed.
514
+ */
515
+ consume(challenge: string): Promise<{
516
+ deviceId: string;
517
+ } | null>;
518
+ }
519
+ /**
520
+ * In-memory challenge store with expiry and single-use semantics.
521
+ * Suitable for development and testing. Use Redis or a database in production.
522
+ */
523
+ declare class InMemoryChallengeStore implements ChallengeStore {
524
+ private readonly challenges;
525
+ store(challenge: string, deviceId: string, expiresAt: number): Promise<void>;
526
+ consume(challenge: string): Promise<{
527
+ deviceId: string;
528
+ } | null>;
529
+ /**
530
+ * Remove expired challenges to prevent unbounded memory growth.
531
+ */
532
+ cleanup(): void;
533
+ }
534
+ /**
535
+ * Interface for rate limiting auth endpoints.
536
+ *
537
+ * Rate limiting is critical for preventing brute-force password guessing
538
+ * and credential stuffing attacks.
539
+ */
540
+ interface RateLimiter {
541
+ /**
542
+ * Check if an action is allowed for the given key.
543
+ * @param key - Rate limit key (e.g., IP address, email, or composite key)
544
+ * @returns true if the action is allowed, false if rate limited
545
+ */
546
+ isAllowed(key: string): Promise<boolean>;
547
+ /**
548
+ * Record that an action was performed for the given key.
549
+ * Call this after each authentication attempt.
550
+ */
551
+ record(key: string): Promise<void>;
552
+ /**
553
+ * Reset the rate limit for a key (e.g., after a successful login).
554
+ */
555
+ reset(key: string): Promise<void>;
556
+ }
557
+ /**
558
+ * In-memory sliding window rate limiter.
559
+ * Suitable for development and single-server deployments.
560
+ * Use Redis-based rate limiting for multi-server production deployments.
561
+ */
562
+ declare class InMemoryRateLimiter implements RateLimiter {
563
+ private readonly attempts;
564
+ private readonly maxAttempts;
565
+ private readonly windowMs;
566
+ /**
567
+ * @param maxAttempts - Maximum number of attempts within the time window (default: 10)
568
+ * @param windowMs - Time window in milliseconds (default: 60,000 = 1 minute)
569
+ */
570
+ constructor(maxAttempts?: number, windowMs?: number);
571
+ isAllowed(key: string): Promise<boolean>;
572
+ record(key: string): Promise<void>;
573
+ reset(key: string): Promise<void>;
574
+ }
291
575
  /**
292
576
  * Configuration for building the built-in auth routes.
293
577
  */
294
578
  interface AuthRoutesConfig {
295
579
  /** The user/device store backing the auth routes */
296
- userStore: InMemoryUserStore;
580
+ userStore: UserStore;
297
581
  /** The token manager for issuing and validating JWTs */
298
582
  tokenManager: TokenManager;
583
+ /**
584
+ * Optional challenge store for device verification.
585
+ * Required for secure device proof-of-possession verification.
586
+ * If not provided, an in-memory store is created automatically.
587
+ */
588
+ challengeStore?: ChallengeStore;
589
+ /**
590
+ * Optional rate limiter for authentication endpoints.
591
+ * If not provided, an in-memory rate limiter is created with defaults
592
+ * (10 attempts per minute).
593
+ */
594
+ rateLimiter?: RateLimiter;
299
595
  }
300
596
  /**
301
597
  * Response envelope returned by all auth route handlers.
@@ -326,11 +622,18 @@ interface AuthRouteResponse<T> {
326
622
  * - Return `{ status, body: { data } }` on success
327
623
  * - Return `{ status, body: { error } }` on failure
328
624
  *
625
+ * Security features:
626
+ * - Rate limiting on sign-in/sign-up to prevent brute-force attacks
627
+ * - Server-side challenge store for device verification (single-use, time-limited)
628
+ * - Token revocation on sign-out and device revocation
629
+ * - Input sanitization on all name fields
630
+ * - Maximum password length to prevent hash-DoS
631
+ *
329
632
  * @example
330
633
  * ```typescript
331
634
  * const routes = new BuiltInAuthRoutes({
332
635
  * userStore: new InMemoryUserStore(),
333
- * tokenManager: new TokenManager({ secret: 'my-secret' }),
636
+ * tokenManager: new TokenManager({ secret: TokenManager.generateSecret() }),
334
637
  * })
335
638
  *
336
639
  * // Wire into an HTTP server:
@@ -343,6 +646,8 @@ interface AuthRouteResponse<T> {
343
646
  declare class BuiltInAuthRoutes {
344
647
  private readonly userStore;
345
648
  private readonly tokenManager;
649
+ private readonly challengeStore;
650
+ private readonly rateLimiter;
346
651
  constructor(config: AuthRoutesConfig);
347
652
  /**
348
653
  * Handle user sign-up (POST /auth/signup).
@@ -352,10 +657,11 @@ declare class BuiltInAuthRoutes {
352
657
  *
353
658
  * @param body - Sign-up request body
354
659
  * @param body.email - The user's email address
355
- * @param body.password - The plaintext password (min 8 characters)
660
+ * @param body.password - The plaintext password (8-128 characters)
356
661
  * @param body.name - Optional display name (defaults to email local part)
357
662
  * @param body.deviceId - Optional device ID to register
358
663
  * @param body.devicePublicKey - Optional device public key (base64url)
664
+ * @param clientIp - Optional client IP for rate limiting
359
665
  * @returns Auth response with the created user and tokens, or an error
360
666
  */
361
667
  handleSignUp(body: {
@@ -364,7 +670,7 @@ declare class BuiltInAuthRoutes {
364
670
  name?: string;
365
671
  deviceId?: string;
366
672
  devicePublicKey?: string;
367
- }): Promise<AuthRouteResponse<{
673
+ }, clientIp?: string): Promise<AuthRouteResponse<{
368
674
  user: AuthUser;
369
675
  tokens: AuthTokens;
370
676
  }>>;
@@ -379,6 +685,7 @@ declare class BuiltInAuthRoutes {
379
685
  * @param body.password - The plaintext password
380
686
  * @param body.deviceId - Optional device ID to register
381
687
  * @param body.devicePublicKey - Optional device public key (base64url)
688
+ * @param clientIp - Optional client IP for rate limiting
382
689
  * @returns Auth response with the user and tokens, or an error
383
690
  */
384
691
  handleSignIn(body: {
@@ -386,7 +693,7 @@ declare class BuiltInAuthRoutes {
386
693
  password: string;
387
694
  deviceId?: string;
388
695
  devicePublicKey?: string;
389
- }): Promise<AuthRouteResponse<{
696
+ }, clientIp?: string): Promise<AuthRouteResponse<{
390
697
  user: AuthUser;
391
698
  tokens: AuthTokens;
392
699
  }>>;
@@ -394,8 +701,8 @@ declare class BuiltInAuthRoutes {
394
701
  * Handle token refresh (POST /auth/refresh).
395
702
  *
396
703
  * Validates the provided refresh token and issues a new token pair
397
- * (refresh token rotation). The old refresh token should be considered
398
- * consumed after this call.
704
+ * (refresh token rotation with reuse detection). The old refresh token
705
+ * is marked as consumed in the revocation store.
399
706
  *
400
707
  * @param body - Refresh request body
401
708
  * @param body.refreshToken - The current refresh token
@@ -404,6 +711,23 @@ declare class BuiltInAuthRoutes {
404
711
  handleRefresh(body: {
405
712
  refreshToken: string;
406
713
  }): Promise<AuthRouteResponse<AuthTokens>>;
714
+ /**
715
+ * Handle sign-out (POST /auth/signout).
716
+ *
717
+ * Validates the access token and revokes the current refresh token
718
+ * (if a revocation store is configured). This ensures that stolen
719
+ * refresh tokens cannot be used after the user signs out.
720
+ *
721
+ * @param accessToken - The JWT access token (without "Bearer " prefix)
722
+ * @param body - Sign-out request body
723
+ * @param body.refreshToken - The current refresh token to revoke
724
+ * @returns Auth response with success flag, or an error
725
+ */
726
+ handleSignOut(accessToken: string, body: {
727
+ refreshToken?: string;
728
+ }): Promise<AuthRouteResponse<{
729
+ success: boolean;
730
+ }>>;
407
731
  /**
408
732
  * Handle get-current-user (GET /auth/me).
409
733
  *
@@ -425,8 +749,8 @@ declare class BuiltInAuthRoutes {
425
749
  /**
426
750
  * Handle device revocation (DELETE /auth/device/:id).
427
751
  *
428
- * Validates the access token and revokes the specified device.
429
- * Only the device's owner can revoke it.
752
+ * Validates the access token, revokes the specified device, and invalidates
753
+ * all tokens issued to that device. Only the device's owner can revoke it.
430
754
  *
431
755
  * @param accessToken - The JWT access token (without "Bearer " prefix)
432
756
  * @param deviceId - The ID of the device to revoke
@@ -435,6 +759,73 @@ declare class BuiltInAuthRoutes {
435
759
  handleRevokeDevice(accessToken: string, deviceId: string): Promise<AuthRouteResponse<{
436
760
  success: boolean;
437
761
  }>>;
762
+ /**
763
+ * Handle device registration (POST /auth/device/register).
764
+ *
765
+ * Requires a valid access token. Registers a new device for the authenticated
766
+ * user and issues a device credential token bound to the device's public key.
767
+ *
768
+ * @param accessToken - The JWT access token (without "Bearer " prefix)
769
+ * @param body - Device registration request body
770
+ * @param body.deviceId - Unique identifier for the device
771
+ * @param body.publicKey - The device's public key as a JWK JSON string
772
+ * @param body.name - Human-readable device name (e.g., "Chrome on MacBook")
773
+ * @returns Auth response with the registered device and device credential, or an error
774
+ */
775
+ handleDeviceRegister(accessToken: string, body: {
776
+ deviceId: string;
777
+ publicKey: string;
778
+ name: string;
779
+ }): Promise<AuthRouteResponse<{
780
+ device: AuthDevice;
781
+ deviceCredential: string;
782
+ }>>;
783
+ /**
784
+ * Generate a challenge for device proof-of-possession verification.
785
+ *
786
+ * Creates a cryptographically random challenge, stores it server-side with
787
+ * a 60-second TTL and the target device ID, and returns the challenge string.
788
+ * The client signs this challenge with its private key and submits it via
789
+ * {@link handleDeviceVerify}.
790
+ *
791
+ * @param accessToken - The JWT access token (without "Bearer " prefix)
792
+ * @param deviceId - The device this challenge is intended for
793
+ * @returns Auth response with the challenge string, or an error
794
+ */
795
+ handleDeviceChallenge(accessToken: string, deviceId: string): Promise<AuthRouteResponse<{
796
+ challenge: string;
797
+ }>>;
798
+ /**
799
+ * Handle device proof-of-possession verification (POST /auth/device/verify).
800
+ *
801
+ * Verifies that the device holds the private key corresponding to its registered
802
+ * public key by checking a signed challenge. The challenge must have been previously
803
+ * issued via {@link handleDeviceChallenge} and is single-use.
804
+ *
805
+ * On success, issues fresh tokens for the device.
806
+ *
807
+ * @param body - Device verification request body
808
+ * @param body.deviceId - The ID of the device to verify
809
+ * @param body.challenge - The challenge string (from handleDeviceChallenge)
810
+ * @param body.signature - The base64url-encoded ECDSA signature of the challenge
811
+ * @returns Auth response with fresh tokens on success, or an error
812
+ */
813
+ handleDeviceVerify(body: {
814
+ deviceId: string;
815
+ challenge: string;
816
+ signature: string;
817
+ }): Promise<AuthRouteResponse<{
818
+ tokens: AuthTokens;
819
+ }>>;
820
+ /**
821
+ * Generates a random challenge string for proof-of-possession verification.
822
+ *
823
+ * **Deprecated:** Use {@link handleDeviceChallenge} instead, which stores
824
+ * the challenge server-side with expiry and single-use semantics.
825
+ *
826
+ * @returns A 64-character hex string (32 random bytes)
827
+ */
828
+ static generateChallenge(): string;
438
829
  /**
439
830
  * Creates a sync server auth provider compatible with `@korajs/server`.
440
831
  *
@@ -443,6 +834,9 @@ declare class BuiltInAuthRoutes {
443
834
  * context containing the user ID and device metadata. This bridges
444
835
  * the built-in auth system with the sync server's authentication layer.
445
836
  *
837
+ * Also checks device revocation status during authentication, ensuring
838
+ * that revoked devices are rejected even if their tokens haven't expired.
839
+ *
446
840
  * @returns An object with an `authenticate` method suitable for KoraSyncServer's `auth` config
447
841
  *
448
842
  * @example
@@ -531,8 +925,9 @@ declare function verifyJwt(token: string, secret: string): Record<string, unknow
531
925
  * Checks whether a token payload has expired based on its `exp` claim.
532
926
  *
533
927
  * The `exp` claim is expected to be in seconds since the Unix epoch (per JWT spec).
534
- * If the `exp` claim is missing or is not a number, the token is considered
535
- * non-expiring and this function returns false.
928
+ * Includes a small clock skew tolerance (5 seconds) to handle minor time
929
+ * differences between servers. If the `exp` claim is missing or is not a number,
930
+ * the token is considered non-expiring and this function returns false.
536
931
  *
537
932
  * @param payload - An object with an optional `exp` field (seconds since epoch)
538
933
  * @returns true if the token has expired, false otherwise
@@ -597,91 +992,517 @@ declare function hashPassword(password: string): Promise<HashResult>;
597
992
  declare function verifyPassword(password: string, hash: string, salt: string): Promise<boolean>;
598
993
 
599
994
  /**
600
- * Parameters for signing up a new user.
995
+ * A pending password reset request.
601
996
  */
602
- interface SignUpParams {
603
- /** The user's email address */
997
+ interface PasswordResetToken {
998
+ /** Cryptographically random single-use token */
999
+ token: string;
1000
+ /** User ID the token was generated for */
1001
+ userId: string;
1002
+ /** Email the reset was requested for */
604
1003
  email: string;
605
- /** The plaintext password */
606
- password: string;
607
- /** Optional display name */
608
- name?: string;
609
- /** Optional device ID to register with the account */
610
- deviceId?: string;
611
- /** Optional device public key (base64url) */
612
- devicePublicKey?: string;
1004
+ /** When the token was created (ms since epoch) */
1005
+ createdAt: number;
1006
+ /** When the token expires (ms since epoch) */
1007
+ expiresAt: number;
1008
+ /** Whether the token has been consumed */
1009
+ consumed: boolean;
613
1010
  }
614
1011
  /**
615
- * Parameters for signing in an existing user.
1012
+ * Persistence interface for password reset tokens.
616
1013
  */
617
- interface SignInParams {
618
- /** The user's email address */
619
- email: string;
620
- /** The plaintext password */
621
- password: string;
622
- /** Optional device ID to register or associate */
623
- deviceId?: string;
624
- /** Optional device public key (base64url) */
625
- devicePublicKey?: string;
1014
+ interface PasswordResetStore {
1015
+ /** Store a reset token. */
1016
+ store(token: PasswordResetToken): Promise<void>;
1017
+ /** Look up a token. Returns null if not found. */
1018
+ get(token: string): Promise<PasswordResetToken | null>;
1019
+ /** Mark a token as consumed. */
1020
+ consume(token: string): Promise<void>;
1021
+ /** Count active (non-consumed, non-expired) tokens for an email. */
1022
+ countActiveForEmail(email: string): Promise<number>;
1023
+ /** Remove expired tokens. */
1024
+ cleanExpired(): Promise<number>;
626
1025
  }
627
1026
  /**
628
- * Abstraction for authentication providers.
629
- *
630
- * This interface allows swapping between the built-in email/password provider
631
- * and external providers (OAuth, SAML, custom) without changing application code.
632
- * Every provider must support the same core operations: sign up, sign in,
633
- * token refresh, token validation, user lookup, and device management.
1027
+ * Configuration for the password reset flow.
1028
+ */
1029
+ interface PasswordResetConfig {
1030
+ /** User store for looking up users and updating passwords */
1031
+ userStore: UserStore;
1032
+ /** Store for reset tokens. Defaults to InMemoryPasswordResetStore. */
1033
+ resetStore?: PasswordResetStore;
1034
+ /** Token TTL in milliseconds. Defaults to 1 hour. */
1035
+ tokenTtlMs?: number;
1036
+ /** Max reset requests per email in the TTL window. Defaults to 3. */
1037
+ maxRequestsPerEmail?: number;
1038
+ /**
1039
+ * Callback invoked when a reset is requested.
1040
+ * The developer must implement email sending.
1041
+ * If not provided, the token is returned in the route response (development mode).
1042
+ */
1043
+ onResetRequested?: (email: string, token: string, expiresAt: number) => void | Promise<void>;
1044
+ }
1045
+ declare class PasswordResetError extends KoraError {
1046
+ constructor(message: string, code: string, context?: Record<string, unknown>);
1047
+ }
1048
+ declare class ResetTokenExpiredError extends PasswordResetError {
1049
+ constructor();
1050
+ }
1051
+ declare class ResetTokenNotFoundError extends PasswordResetError {
1052
+ constructor();
1053
+ }
1054
+ declare class ResetRateLimitedError extends PasswordResetError {
1055
+ constructor();
1056
+ }
1057
+ declare class InMemoryPasswordResetStore implements PasswordResetStore {
1058
+ private tokens;
1059
+ store(token: PasswordResetToken): Promise<void>;
1060
+ get(token: string): Promise<PasswordResetToken | null>;
1061
+ consume(token: string): Promise<void>;
1062
+ countActiveForEmail(email: string): Promise<number>;
1063
+ cleanExpired(): Promise<number>;
1064
+ }
1065
+ /**
1066
+ * Manages the password reset flow.
634
1067
  *
635
1068
  * @example
636
1069
  * ```typescript
637
- * // Use the built-in provider
638
- * const provider: AuthProviderAdapter = new BuiltInProvider({
639
- * userStore: new InMemoryUserStore(),
640
- * tokenManager: new TokenManager({ secret: 'my-secret' }),
1070
+ * const resetManager = new PasswordResetManager({
1071
+ * userStore,
1072
+ * onResetRequested: async (email, token, expiresAt) => {
1073
+ * await sendEmail(email, `Reset link: https://app.com/reset?token=${token}`)
1074
+ * },
641
1075
  * })
642
1076
  *
643
- * const { user, tokens } = await provider.signUp({
644
- * email: 'alice@example.com',
645
- * password: 'secure-password-123',
646
- * })
1077
+ * // Request reset (always returns 200 to prevent email enumeration)
1078
+ * const response = await resetManager.requestReset('user@example.com')
1079
+ *
1080
+ * // Consume token and set new password
1081
+ * const response = await resetManager.resetPassword(token, 'newPassword123')
647
1082
  * ```
648
1083
  */
649
- interface AuthProviderAdapter {
1084
+ declare class PasswordResetManager {
1085
+ private readonly userStore;
1086
+ private readonly resetStore;
1087
+ private readonly tokenTtlMs;
1088
+ private readonly maxRequestsPerEmail;
1089
+ private readonly onResetRequested?;
1090
+ constructor(config: PasswordResetConfig);
650
1091
  /**
651
- * Create a new user account and issue authentication tokens.
1092
+ * Request a password reset for an email.
1093
+ * Always returns success to prevent email enumeration.
652
1094
  *
653
- * @param params - Sign-up parameters including email, password, and optional device info
654
- * @returns The created user and tokens
655
- * @throws If the email is already registered or input validation fails
1095
+ * If a callback is configured, invokes it with the token.
1096
+ * In development mode (no callback), returns the token in the response.
656
1097
  */
657
- signUp(params: SignUpParams): Promise<{
658
- user: AuthUser;
659
- tokens: AuthTokens;
1098
+ requestReset(email: string): Promise<{
1099
+ status: number;
1100
+ body: {
1101
+ data: {
1102
+ message: string;
1103
+ token?: string;
1104
+ };
1105
+ } | {
1106
+ error: string;
1107
+ };
660
1108
  }>;
661
1109
  /**
662
- * Authenticate an existing user and issue tokens.
663
- *
664
- * @param params - Sign-in parameters including email and password
665
- * @returns The authenticated user and tokens
666
- * @throws If the credentials are invalid
1110
+ * Consume a reset token and set a new password.
667
1111
  */
668
- signIn(params: SignInParams): Promise<{
669
- user: AuthUser;
670
- tokens: AuthTokens;
1112
+ resetPassword(token: string, newPassword: string): Promise<{
1113
+ status: number;
1114
+ body: {
1115
+ data: {
1116
+ message: string;
1117
+ };
1118
+ } | {
1119
+ error: string;
1120
+ };
671
1121
  }>;
672
1122
  /**
673
- * Exchange a refresh token for new tokens (rotation).
674
- *
675
- * @param refreshToken - The current refresh token
676
- * @returns A new token pair
677
- * @throws If the refresh token is invalid or expired
1123
+ * Change password for an authenticated user (requires current password verification).
678
1124
  */
679
- refreshTokens(refreshToken: string): Promise<AuthTokens>;
1125
+ changePassword(userId: string, currentPassword: string, newPassword: string): Promise<{
1126
+ status: number;
1127
+ body: {
1128
+ data: {
1129
+ message: string;
1130
+ };
1131
+ } | {
1132
+ error: string;
1133
+ };
1134
+ }>;
1135
+ }
1136
+
1137
+ /**
1138
+ * A pending email verification token.
1139
+ */
1140
+ interface EmailVerificationToken {
1141
+ /** Cryptographically random single-use token */
1142
+ token: string;
1143
+ /** User ID the token was generated for */
1144
+ userId: string;
1145
+ /** Email being verified */
1146
+ email: string;
1147
+ /** When the token was created (ms since epoch) */
1148
+ createdAt: number;
1149
+ /** When the token expires (ms since epoch) */
1150
+ expiresAt: number;
1151
+ /** Whether the token has been consumed */
1152
+ consumed: boolean;
1153
+ }
1154
+ /**
1155
+ * Persistence interface for email verification tokens.
1156
+ */
1157
+ interface EmailVerificationStore {
1158
+ /** Store a verification token. */
1159
+ store(token: EmailVerificationToken): Promise<void>;
1160
+ /** Look up a token. Returns null if not found. */
1161
+ get(token: string): Promise<EmailVerificationToken | null>;
1162
+ /** Mark a token as consumed. */
1163
+ consume(token: string): Promise<void>;
1164
+ /** Count active (non-consumed, non-expired) tokens for a user. */
1165
+ countActiveForUser(userId: string): Promise<number>;
1166
+ /** Remove expired tokens. */
1167
+ cleanExpired(): Promise<number>;
1168
+ }
1169
+ /**
1170
+ * Configuration for the email verification flow.
1171
+ */
1172
+ interface EmailVerificationConfig {
1173
+ /** User store for looking up users */
1174
+ userStore: UserStore;
1175
+ /** Store for verification tokens. Defaults to InMemoryEmailVerificationStore. */
1176
+ verificationStore?: EmailVerificationStore;
1177
+ /** Token TTL in milliseconds. Defaults to 24 hours. */
1178
+ tokenTtlMs?: number;
1179
+ /** Max verification requests per user per TTL window. Defaults to 3. */
1180
+ maxRequestsPerUser?: number;
680
1181
  /**
681
- * Validate an access token and extract identity claims.
682
- *
683
- * @param token - The JWT access token
684
- * @returns User ID and device ID if valid, or null if invalid/expired
1182
+ * Callback invoked when a verification email should be sent.
1183
+ * The developer must implement email sending.
1184
+ * If not provided, the token is returned in the route response (development mode).
1185
+ */
1186
+ onVerificationRequired?: (email: string, token: string, expiresAt: number) => void | Promise<void>;
1187
+ }
1188
+ declare class EmailVerificationError extends KoraError {
1189
+ constructor(message: string, code: string, context?: Record<string, unknown>);
1190
+ }
1191
+ declare class VerificationTokenExpiredError extends EmailVerificationError {
1192
+ constructor();
1193
+ }
1194
+ declare class VerificationTokenNotFoundError extends EmailVerificationError {
1195
+ constructor();
1196
+ }
1197
+ declare class InMemoryEmailVerificationStore implements EmailVerificationStore {
1198
+ private tokens;
1199
+ store(token: EmailVerificationToken): Promise<void>;
1200
+ get(token: string): Promise<EmailVerificationToken | null>;
1201
+ consume(token: string): Promise<void>;
1202
+ countActiveForUser(userId: string): Promise<number>;
1203
+ cleanExpired(): Promise<number>;
1204
+ }
1205
+ /**
1206
+ * Manages the email verification flow.
1207
+ *
1208
+ * @example
1209
+ * ```typescript
1210
+ * const verifier = new EmailVerificationManager({
1211
+ * userStore,
1212
+ * onVerificationRequired: async (email, token, expiresAt) => {
1213
+ * await sendEmail(email, `Verify: https://app.com/verify?token=${token}`)
1214
+ * },
1215
+ * })
1216
+ *
1217
+ * // Send verification email
1218
+ * await verifier.sendVerification('user-1', 'user@example.com')
1219
+ *
1220
+ * // Verify email
1221
+ * await verifier.verifyEmail(token)
1222
+ * ```
1223
+ */
1224
+ declare class EmailVerificationManager {
1225
+ private readonly userStore;
1226
+ private readonly verificationStore;
1227
+ private readonly tokenTtlMs;
1228
+ private readonly maxRequestsPerUser;
1229
+ private readonly onVerificationRequired?;
1230
+ constructor(config: EmailVerificationConfig);
1231
+ /**
1232
+ * Send a verification email for a user.
1233
+ * Rate-limited to prevent abuse.
1234
+ */
1235
+ sendVerification(userId: string, email: string): Promise<{
1236
+ status: number;
1237
+ body: {
1238
+ data: {
1239
+ message: string;
1240
+ token?: string;
1241
+ };
1242
+ } | {
1243
+ error: string;
1244
+ };
1245
+ }>;
1246
+ /**
1247
+ * Verify an email using a verification token.
1248
+ */
1249
+ verifyEmail(token: string): Promise<{
1250
+ status: number;
1251
+ body: {
1252
+ data: {
1253
+ message: string;
1254
+ userId: string;
1255
+ email: string;
1256
+ };
1257
+ } | {
1258
+ error: string;
1259
+ };
1260
+ }>;
1261
+ /**
1262
+ * Resend verification email for a user.
1263
+ * Delegates to sendVerification with rate limiting.
1264
+ */
1265
+ resendVerification(userId: string): Promise<{
1266
+ status: number;
1267
+ body: {
1268
+ data: {
1269
+ message: string;
1270
+ token?: string;
1271
+ };
1272
+ } | {
1273
+ error: string;
1274
+ };
1275
+ }>;
1276
+ }
1277
+
1278
+ /**
1279
+ * Minimal better-sqlite3 subset to avoid a hard dependency on the package.
1280
+ * The real Database instance satisfies this at runtime.
1281
+ */
1282
+ interface SqliteDatabase {
1283
+ pragma(source: string): unknown;
1284
+ exec(source: string): void;
1285
+ prepare(source: string): {
1286
+ run(...params: unknown[]): unknown;
1287
+ get(...params: unknown[]): unknown;
1288
+ all(...params: unknown[]): unknown[];
1289
+ };
1290
+ transaction<T>(fn: () => T): () => T;
1291
+ }
1292
+ /**
1293
+ * SQLite-backed user and device store using better-sqlite3.
1294
+ *
1295
+ * Provides persistent user storage suitable for single-server deployments,
1296
+ * Electron apps, and development environments. Uses WAL mode for concurrent
1297
+ * read/write performance.
1298
+ *
1299
+ * This implementation uses dynamic imports for better-sqlite3 so projects
1300
+ * that do not use SQLite server-side do not need to install it.
1301
+ *
1302
+ * @example
1303
+ * ```typescript
1304
+ * import { createSqliteUserStore } from '@korajs/auth/server'
1305
+ *
1306
+ * const userStore = await createSqliteUserStore({ filename: './auth.db' })
1307
+ * const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
1308
+ * ```
1309
+ */
1310
+ declare class SqliteUserStore implements UserStore {
1311
+ private readonly db;
1312
+ constructor(db: SqliteDatabase);
1313
+ private ensureTables;
1314
+ createUser(params: {
1315
+ email: string;
1316
+ passwordHash: string;
1317
+ salt: string;
1318
+ name: string;
1319
+ }): Promise<AuthUser>;
1320
+ findByEmail(email: string): Promise<StoredUser | null>;
1321
+ findById(id: string): Promise<StoredUser | null>;
1322
+ registerDevice(params: {
1323
+ id: string;
1324
+ userId: string;
1325
+ publicKey: string;
1326
+ name: string;
1327
+ }): Promise<AuthDevice>;
1328
+ findDevice(deviceId: string): Promise<AuthDevice | null>;
1329
+ listDevices(userId: string): Promise<AuthDevice[]>;
1330
+ revokeDevice(deviceId: string): Promise<void>;
1331
+ setEmailVerified(userId: string, verified: boolean): Promise<void>;
1332
+ updatePassword(userId: string, passwordHash: string, salt: string): Promise<void>;
1333
+ listAll(): Promise<StoredUser[]>;
1334
+ update(user: StoredUser): Promise<void>;
1335
+ delete(userId: string): Promise<void>;
1336
+ touchDevice(deviceId: string): Promise<void>;
1337
+ }
1338
+ /**
1339
+ * Creates a SqliteUserStore from a file path.
1340
+ *
1341
+ * Uses runtime dynamic imports so projects that do not use SQLite server-side
1342
+ * do not need to install `better-sqlite3`.
1343
+ *
1344
+ * @param options.filename - Path to the SQLite database file, or `:memory:` for in-memory
1345
+ */
1346
+ declare function createSqliteUserStore(options: {
1347
+ filename: string;
1348
+ }): Promise<SqliteUserStore>;
1349
+
1350
+ /**
1351
+ * Minimal typed subset of a postgres-js SQL tag for our queries.
1352
+ * Avoids a hard dependency on the postgres package.
1353
+ * Uses Record<string, unknown>[] for result type to match postgres-js return shape.
1354
+ */
1355
+ interface PostgresClient {
1356
+ begin<T>(fn: (sql: PostgresClient) => Promise<T>): Promise<T>;
1357
+ (template: TemplateStringsArray, ...args: unknown[]): Promise<Record<string, unknown>[]>;
1358
+ }
1359
+ /**
1360
+ * PostgreSQL-backed user and device store using postgres-js.
1361
+ *
1362
+ * Provides persistent user storage suitable for production multi-server
1363
+ * deployments. Uses parameterized queries for SQL injection safety and
1364
+ * transactions for atomic operations.
1365
+ *
1366
+ * This implementation uses dynamic imports for postgres so projects
1367
+ * that do not use PostgreSQL do not need to install it.
1368
+ *
1369
+ * @example
1370
+ * ```typescript
1371
+ * import { createPostgresUserStore } from '@korajs/auth/server'
1372
+ *
1373
+ * const userStore = await createPostgresUserStore({
1374
+ * connectionString: 'postgres://user:pass@localhost:5432/mydb',
1375
+ * })
1376
+ * const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
1377
+ * ```
1378
+ */
1379
+ declare class PostgresUserStore implements UserStore {
1380
+ private readonly sql;
1381
+ private readonly ready;
1382
+ constructor(sql: PostgresClient);
1383
+ private ensureTables;
1384
+ createUser(params: {
1385
+ email: string;
1386
+ passwordHash: string;
1387
+ salt: string;
1388
+ name: string;
1389
+ }): Promise<AuthUser>;
1390
+ findByEmail(email: string): Promise<StoredUser | null>;
1391
+ findById(id: string): Promise<StoredUser | null>;
1392
+ registerDevice(params: {
1393
+ id: string;
1394
+ userId: string;
1395
+ publicKey: string;
1396
+ name: string;
1397
+ }): Promise<AuthDevice>;
1398
+ findDevice(deviceId: string): Promise<AuthDevice | null>;
1399
+ listDevices(userId: string): Promise<AuthDevice[]>;
1400
+ revokeDevice(deviceId: string): Promise<void>;
1401
+ setEmailVerified(userId: string, verified: boolean): Promise<void>;
1402
+ updatePassword(userId: string, passwordHash: string, salt: string): Promise<void>;
1403
+ listAll(): Promise<StoredUser[]>;
1404
+ update(user: StoredUser): Promise<void>;
1405
+ delete(userId: string): Promise<void>;
1406
+ touchDevice(deviceId: string): Promise<void>;
1407
+ }
1408
+ /**
1409
+ * Creates a PostgresUserStore from a connection string.
1410
+ *
1411
+ * Uses runtime dynamic imports so projects that do not use PostgreSQL
1412
+ * do not need to install `postgres`.
1413
+ *
1414
+ * @param options.connectionString - PostgreSQL connection URL (e.g., `postgres://user:pass@host:5432/db`)
1415
+ */
1416
+ declare function createPostgresUserStore(options: {
1417
+ connectionString: string;
1418
+ }): Promise<PostgresUserStore>;
1419
+
1420
+ /**
1421
+ * Parameters for signing up a new user.
1422
+ */
1423
+ interface SignUpParams {
1424
+ /** The user's email address */
1425
+ email: string;
1426
+ /** The plaintext password */
1427
+ password: string;
1428
+ /** Optional display name */
1429
+ name?: string;
1430
+ /** Optional device ID to register with the account */
1431
+ deviceId?: string;
1432
+ /** Optional device public key (base64url) */
1433
+ devicePublicKey?: string;
1434
+ }
1435
+ /**
1436
+ * Parameters for signing in an existing user.
1437
+ */
1438
+ interface SignInParams {
1439
+ /** The user's email address */
1440
+ email: string;
1441
+ /** The plaintext password */
1442
+ password: string;
1443
+ /** Optional device ID to register or associate */
1444
+ deviceId?: string;
1445
+ /** Optional device public key (base64url) */
1446
+ devicePublicKey?: string;
1447
+ }
1448
+ /**
1449
+ * Abstraction for authentication providers.
1450
+ *
1451
+ * This interface allows swapping between the built-in email/password provider
1452
+ * and external providers (OAuth, SAML, custom) without changing application code.
1453
+ * Every provider must support the same core operations: sign up, sign in,
1454
+ * token refresh, token validation, user lookup, and device management.
1455
+ *
1456
+ * @example
1457
+ * ```typescript
1458
+ * // Use the built-in provider
1459
+ * const provider: AuthProviderAdapter = new BuiltInProvider({
1460
+ * userStore: new InMemoryUserStore(),
1461
+ * tokenManager: new TokenManager({ secret: 'my-secret' }),
1462
+ * })
1463
+ *
1464
+ * const { user, tokens } = await provider.signUp({
1465
+ * email: 'alice@example.com',
1466
+ * password: 'secure-password-123',
1467
+ * })
1468
+ * ```
1469
+ */
1470
+ interface AuthProviderAdapter {
1471
+ /**
1472
+ * Create a new user account and issue authentication tokens.
1473
+ *
1474
+ * @param params - Sign-up parameters including email, password, and optional device info
1475
+ * @returns The created user and tokens
1476
+ * @throws If the email is already registered or input validation fails
1477
+ */
1478
+ signUp(params: SignUpParams): Promise<{
1479
+ user: AuthUser;
1480
+ tokens: AuthTokens;
1481
+ }>;
1482
+ /**
1483
+ * Authenticate an existing user and issue tokens.
1484
+ *
1485
+ * @param params - Sign-in parameters including email and password
1486
+ * @returns The authenticated user and tokens
1487
+ * @throws If the credentials are invalid
1488
+ */
1489
+ signIn(params: SignInParams): Promise<{
1490
+ user: AuthUser;
1491
+ tokens: AuthTokens;
1492
+ }>;
1493
+ /**
1494
+ * Exchange a refresh token for new tokens (rotation).
1495
+ *
1496
+ * @param refreshToken - The current refresh token
1497
+ * @returns A new token pair
1498
+ * @throws If the refresh token is invalid or expired
1499
+ */
1500
+ refreshTokens(refreshToken: string): Promise<AuthTokens>;
1501
+ /**
1502
+ * Validate an access token and extract identity claims.
1503
+ *
1504
+ * @param token - The JWT access token
1505
+ * @returns User ID and device ID if valid, or null if invalid/expired
685
1506
  */
686
1507
  validateAccessToken(token: string): Promise<{
687
1508
  userId: string;
@@ -777,4 +1598,2390 @@ declare class BuiltInProvider implements AuthProviderAdapter {
777
1598
  listDevices(accessToken: string): Promise<AuthDevice[]>;
778
1599
  }
779
1600
 
780
- export { type AuthDevice, type AuthProviderAdapter, AuthProviderError, type AuthRouteResponse, type AuthRoutesConfig, type AuthUser, BuiltInAuthRoutes, BuiltInProvider, DuplicateEmailError, InMemoryUserStore, type SignInParams, type SignUpParams, type StoredUser, TokenManager, type TokenManagerConfig, decodeJwt, encodeJwt, hashPassword, isExpired, verifyJwt, verifyPassword };
1601
+ /**
1602
+ * Thrown when an operation is not supported by external auth providers.
1603
+ *
1604
+ * External providers delegate user management to the third-party service
1605
+ * (Clerk, Auth0, Supabase, etc.). Operations like sign-up and sign-in must
1606
+ * be performed through the external provider's SDK or UI, not through Kora.
1607
+ */
1608
+ declare class ExternalAuthOperationNotSupportedError extends KoraError {
1609
+ constructor(operation: string, provider: string);
1610
+ }
1611
+ /**
1612
+ * Thrown when an external JWT token fails validation.
1613
+ */
1614
+ declare class ExternalTokenValidationError extends KoraError {
1615
+ constructor(reason: string, context?: Record<string, unknown>);
1616
+ }
1617
+ /**
1618
+ * User information extracted from an external JWT token.
1619
+ * Returned by the claims mapping function.
1620
+ */
1621
+ interface ExternalUserInfo {
1622
+ /** Unique user identifier from the external provider */
1623
+ userId: string;
1624
+ /** User's email address, if available in the token claims */
1625
+ email?: string;
1626
+ /** User's display name, if available in the token claims */
1627
+ name?: string;
1628
+ /** Additional metadata from the token claims */
1629
+ metadata?: Record<string, unknown>;
1630
+ }
1631
+ /**
1632
+ * Configuration for the external JWT authentication provider.
1633
+ *
1634
+ * Supports two validation modes:
1635
+ * 1. **HMAC secret** (`jwtSecret`): For providers that sign tokens with a shared
1636
+ * secret (e.g., Supabase). Uses HS256 verification via the existing `verifyJwt` utility.
1637
+ * 2. **Custom validator** (`validateToken`): For providers that use asymmetric keys
1638
+ * (RS256, ES256) or require custom validation logic (e.g., Clerk with JWKS rotation).
1639
+ * The developer provides their own validation function.
1640
+ *
1641
+ * At least one of `jwtSecret` or `validateToken` must be provided.
1642
+ *
1643
+ * @example
1644
+ * ```typescript
1645
+ * // With a shared secret (e.g., Supabase)
1646
+ * const provider = new ExternalJwtProvider({
1647
+ * providerName: 'supabase',
1648
+ * jwtSecret: process.env.SUPABASE_JWT_SECRET,
1649
+ * })
1650
+ *
1651
+ * // With a custom validator (e.g., Clerk JWKS)
1652
+ * const provider = new ExternalJwtProvider({
1653
+ * providerName: 'clerk',
1654
+ * validateToken: async (token) => {
1655
+ * const claims = await clerkClient.verifyToken(token)
1656
+ * return claims ? { sub: claims.sub, ...claims } : null
1657
+ * },
1658
+ * })
1659
+ * ```
1660
+ */
1661
+ interface ExternalJwtProviderConfig {
1662
+ /**
1663
+ * Human-readable name of the external auth provider.
1664
+ * Used in error messages and DevTools for identification.
1665
+ * @example 'clerk', 'auth0', 'supabase', 'firebase'
1666
+ */
1667
+ providerName: string;
1668
+ /**
1669
+ * Shared secret for HS256 JWT verification.
1670
+ * Used when the external provider signs tokens with HMAC-SHA256.
1671
+ * Mutually exclusive with `validateToken` (if both are provided,
1672
+ * `validateToken` takes precedence).
1673
+ */
1674
+ jwtSecret?: string;
1675
+ /**
1676
+ * Custom token validator function.
1677
+ * When provided, this is used instead of HS256 HMAC verification.
1678
+ * Receives the raw JWT string and must return the decoded claims
1679
+ * (with at least a `sub` field) or null if the token is invalid.
1680
+ *
1681
+ * Use this for providers that use asymmetric signing (RS256, ES256)
1682
+ * or require JWKS-based key rotation.
1683
+ *
1684
+ * @param token - The raw JWT string to validate
1685
+ * @returns The decoded claims object with at least a `sub` field, or null if invalid
1686
+ */
1687
+ validateToken?: (token: string) => Promise<{
1688
+ sub: string;
1689
+ [key: string]: unknown;
1690
+ } | null>;
1691
+ /**
1692
+ * Map external JWT claims to Kora's expected user format.
1693
+ *
1694
+ * The default mapping extracts:
1695
+ * - `sub` -> `userId` (required)
1696
+ * - `email` -> `email` (optional)
1697
+ * - `name` -> `name` (optional)
1698
+ *
1699
+ * Override this to extract custom claims from your provider's tokens.
1700
+ *
1701
+ * @param claims - The decoded JWT claims object
1702
+ * @returns Kora-compatible user information
1703
+ *
1704
+ * @example
1705
+ * ```typescript
1706
+ * mapClaims: (claims) => ({
1707
+ * userId: claims.sub as string,
1708
+ * email: claims.email_address as string,
1709
+ * name: `${claims.first_name} ${claims.last_name}`,
1710
+ * metadata: { org: claims.org_id },
1711
+ * })
1712
+ * ```
1713
+ */
1714
+ mapClaims?: (claims: Record<string, unknown>) => ExternalUserInfo;
1715
+ }
1716
+ /**
1717
+ * Authentication provider adapter for external JWT issuers.
1718
+ *
1719
+ * This adapter validates JWTs issued by third-party auth services (Clerk, Auth0,
1720
+ * Supabase, Firebase, or any custom JWT issuer) and maps their claims to Kora's
1721
+ * internal auth context. It bridges external identity providers with Kora's
1722
+ * sync authentication layer.
1723
+ *
1724
+ * **How it works:**
1725
+ * 1. The client authenticates with the external provider and receives a JWT
1726
+ * 2. The client passes this JWT to Kora's sync server
1727
+ * 3. This adapter validates the JWT and extracts user identity
1728
+ * 4. Kora uses the extracted identity for sync authorization
1729
+ *
1730
+ * **What it does NOT do:**
1731
+ * - Sign up or sign in users (that happens through the external provider)
1732
+ * - Issue or refresh tokens (that is the external provider's responsibility)
1733
+ * - Manage devices (external providers handle their own device tracking)
1734
+ *
1735
+ * @example
1736
+ * ```typescript
1737
+ * import { ExternalJwtProvider } from '@korajs/auth/server'
1738
+ *
1739
+ * const auth = new ExternalJwtProvider({
1740
+ * providerName: 'clerk',
1741
+ * validateToken: async (token) => {
1742
+ * // Use Clerk's SDK or JWKS endpoint to verify
1743
+ * return verifiedClaims
1744
+ * },
1745
+ * })
1746
+ *
1747
+ * // Use with Kora sync server
1748
+ * const result = await auth.validateAccessToken('eyJhbG...')
1749
+ * if (result) {
1750
+ * console.log('User:', result.userId)
1751
+ * }
1752
+ * ```
1753
+ */
1754
+ declare class ExternalJwtProvider implements AuthProviderAdapter {
1755
+ private readonly providerName;
1756
+ private readonly jwtSecret;
1757
+ private readonly customValidateToken;
1758
+ private readonly mapClaims;
1759
+ constructor(config: ExternalJwtProviderConfig);
1760
+ /**
1761
+ * Not supported for external providers.
1762
+ *
1763
+ * User registration must be performed through the external auth provider's
1764
+ * SDK or UI. Kora does not manage user accounts for external providers.
1765
+ *
1766
+ * @throws {ExternalAuthOperationNotSupportedError} Always
1767
+ */
1768
+ signUp(_params: SignUpParams): Promise<{
1769
+ user: AuthUser;
1770
+ tokens: AuthTokens;
1771
+ }>;
1772
+ /**
1773
+ * Not supported for external providers.
1774
+ *
1775
+ * User authentication must be performed through the external auth provider's
1776
+ * SDK or UI. Kora does not manage credentials for external providers.
1777
+ *
1778
+ * @throws {ExternalAuthOperationNotSupportedError} Always
1779
+ */
1780
+ signIn(_params: SignInParams): Promise<{
1781
+ user: AuthUser;
1782
+ tokens: AuthTokens;
1783
+ }>;
1784
+ /**
1785
+ * Not supported for external providers.
1786
+ *
1787
+ * Token refresh must be performed through the external auth provider's SDK.
1788
+ * Kora does not manage token lifecycle for external providers.
1789
+ *
1790
+ * @throws {ExternalAuthOperationNotSupportedError} Always
1791
+ */
1792
+ refreshTokens(_refreshToken: string): Promise<AuthTokens>;
1793
+ /**
1794
+ * Validate an access token from the external auth provider.
1795
+ *
1796
+ * Uses either the custom `validateToken` function or HS256 HMAC verification
1797
+ * (depending on configuration) to validate the JWT. On success, maps the claims
1798
+ * to Kora's expected format and returns the user ID and device ID.
1799
+ *
1800
+ * The device ID for external providers is derived from the user ID with a
1801
+ * "external-" prefix, since external providers typically don't use Kora's
1802
+ * device identity system.
1803
+ *
1804
+ * @param token - The JWT access token issued by the external provider
1805
+ * @returns User ID and device ID if the token is valid, or null if invalid/expired
1806
+ */
1807
+ validateAccessToken(token: string): Promise<{
1808
+ userId: string;
1809
+ deviceId: string;
1810
+ } | null>;
1811
+ /**
1812
+ * Not supported for external providers.
1813
+ *
1814
+ * User lookup must be performed through the external auth provider's API.
1815
+ *
1816
+ * @throws {ExternalAuthOperationNotSupportedError} Always
1817
+ */
1818
+ getUser(_userId: string): Promise<AuthUser | null>;
1819
+ /**
1820
+ * Not supported for external providers.
1821
+ *
1822
+ * Device revocation must be managed through the external auth provider
1823
+ * or by revoking the user's tokens at the provider level.
1824
+ *
1825
+ * @throws {ExternalAuthOperationNotSupportedError} Always
1826
+ */
1827
+ revokeDevice(_accessToken: string, _deviceId: string): Promise<void>;
1828
+ /**
1829
+ * Not supported for external providers.
1830
+ *
1831
+ * Device listing must be performed through the external auth provider's API.
1832
+ *
1833
+ * @throws {ExternalAuthOperationNotSupportedError} Always
1834
+ */
1835
+ listDevices(_accessToken: string): Promise<AuthDevice[]>;
1836
+ /**
1837
+ * Creates a sync server auth provider compatible with `@korajs/server`.
1838
+ *
1839
+ * Returns an object with an `authenticate` method that validates the external
1840
+ * JWT and returns a Kora-compatible auth context. Use this to wire the external
1841
+ * auth provider into KoraSyncServer.
1842
+ *
1843
+ * @returns An object with an `authenticate` method for KoraSyncServer's `auth` config
1844
+ *
1845
+ * @example
1846
+ * ```typescript
1847
+ * const externalAuth = new ExternalJwtProvider({ ... })
1848
+ * const syncServer = new KoraSyncServer({
1849
+ * store,
1850
+ * auth: externalAuth.toSyncAuthProvider(),
1851
+ * })
1852
+ * ```
1853
+ */
1854
+ toSyncAuthProvider(): {
1855
+ authenticate(token: string): Promise<{
1856
+ userId: string;
1857
+ scopes?: Record<string, Record<string, unknown>>;
1858
+ metadata?: Record<string, unknown>;
1859
+ } | null>;
1860
+ };
1861
+ /**
1862
+ * Extract and validate claims from a JWT token.
1863
+ *
1864
+ * Uses the custom validator if configured, otherwise falls back to
1865
+ * HS256 HMAC verification with the configured secret.
1866
+ *
1867
+ * @param token - The raw JWT string
1868
+ * @returns The decoded claims object, or null if the token is invalid
1869
+ */
1870
+ private extractClaims;
1871
+ }
1872
+
1873
+ /**
1874
+ * Configuration for the Clerk authentication adapter.
1875
+ *
1876
+ * Clerk uses asymmetric signing (RS256) with JWKS key rotation, so this adapter
1877
+ * requires a custom `validateToken` function that handles JWKS-based verification.
1878
+ * The adapter provides sensible defaults for mapping Clerk's JWT claims to Kora's
1879
+ * expected format.
1880
+ *
1881
+ * Clerk JWTs typically contain:
1882
+ * - `sub`: User ID (e.g., "user_2abc123...")
1883
+ * - `email`: Primary email address (if configured in session claims)
1884
+ * - `first_name`, `last_name`: Name fields (if configured in session claims)
1885
+ * - `azp`: Authorized party (your frontend origin)
1886
+ * - `org_id`, `org_slug`, `org_role`: Organization claims (if using Clerk orgs)
1887
+ *
1888
+ * @example
1889
+ * ```typescript
1890
+ * import { createClerkAdapter } from '@korajs/auth/server'
1891
+ *
1892
+ * const clerkAuth = createClerkAdapter({
1893
+ * validateToken: async (token) => {
1894
+ * // Use Clerk's backend SDK or your own JWKS verification
1895
+ * const result = await clerkClient.verifyToken(token)
1896
+ * return result ? { sub: result.sub, ...result } : null
1897
+ * },
1898
+ * })
1899
+ *
1900
+ * // Use with Kora sync server
1901
+ * const syncServer = new KoraSyncServer({
1902
+ * store,
1903
+ * auth: clerkAuth.toSyncAuthProvider(),
1904
+ * })
1905
+ * ```
1906
+ */
1907
+ interface ClerkAdapterConfig {
1908
+ /**
1909
+ * Custom token validator for Clerk JWTs.
1910
+ *
1911
+ * Clerk uses RS256 signing with JWKS key rotation, which requires either
1912
+ * Clerk's backend SDK or a JWKS-based verifier. This function receives
1913
+ * the raw JWT and should return the decoded claims or null.
1914
+ *
1915
+ * @param token - The raw JWT string from the Clerk session
1916
+ * @returns Decoded claims with at least a `sub` field, or null if invalid
1917
+ */
1918
+ validateToken: (token: string) => Promise<{
1919
+ sub: string;
1920
+ [key: string]: unknown;
1921
+ } | null>;
1922
+ /**
1923
+ * Custom claim mapping override.
1924
+ *
1925
+ * By default, the Clerk adapter maps:
1926
+ * - `sub` -> `userId`
1927
+ * - `email` -> `email` (if present)
1928
+ * - `first_name` + `last_name` -> `name` (concatenated, if present)
1929
+ * - `org_id`, `org_slug`, `org_role` -> `metadata` (if present)
1930
+ *
1931
+ * Override this to customize how Clerk claims are mapped to Kora's format.
1932
+ */
1933
+ mapClaims?: ExternalJwtProviderConfig['mapClaims'];
1934
+ }
1935
+ /**
1936
+ * Creates an ExternalJwtProvider configured for Clerk authentication.
1937
+ *
1938
+ * Clerk uses RS256 signing with JWKS key rotation, so a custom `validateToken`
1939
+ * function must be provided. This function should use Clerk's backend SDK or
1940
+ * a JWKS-based JWT verifier to validate tokens.
1941
+ *
1942
+ * This adapter does NOT depend on `@clerk/backend` or any Clerk SDK. It only
1943
+ * provides sensible defaults for mapping Clerk's JWT claims to Kora's format.
1944
+ * The actual token verification is delegated to the provided `validateToken` function.
1945
+ *
1946
+ * @param config - Clerk adapter configuration
1947
+ * @returns An ExternalJwtProvider instance configured for Clerk
1948
+ *
1949
+ * @example
1950
+ * ```typescript
1951
+ * import { createClerkAdapter } from '@korajs/auth/server'
1952
+ *
1953
+ * const clerkAuth = createClerkAdapter({
1954
+ * validateToken: async (token) => {
1955
+ * // Your JWKS verification logic here
1956
+ * const payload = await verifyWithJwks(token, CLERK_JWKS_URL)
1957
+ * return payload
1958
+ * },
1959
+ * })
1960
+ *
1961
+ * const result = await clerkAuth.validateAccessToken(sessionToken)
1962
+ * ```
1963
+ */
1964
+ declare function createClerkAdapter(config: ClerkAdapterConfig): ExternalJwtProvider;
1965
+
1966
+ /**
1967
+ * Configuration for the Supabase authentication adapter.
1968
+ *
1969
+ * Supabase Auth signs JWTs with HS256 using the project's JWT secret,
1970
+ * which is available in your Supabase project settings under
1971
+ * Settings -> API -> JWT Secret.
1972
+ *
1973
+ * Supabase JWTs typically contain:
1974
+ * - `sub`: User UUID
1975
+ * - `email`: User's email address
1976
+ * - `role`: The database role (e.g., "authenticated", "anon")
1977
+ * - `aud`: Audience (usually "authenticated")
1978
+ * - `app_metadata`: Provider info, roles, etc.
1979
+ * - `user_metadata`: Custom user data (name, avatar, etc.)
1980
+ *
1981
+ * @example
1982
+ * ```typescript
1983
+ * import { createSupabaseAdapter } from '@korajs/auth/server'
1984
+ *
1985
+ * const supabaseAuth = createSupabaseAdapter({
1986
+ * jwtSecret: process.env.SUPABASE_JWT_SECRET,
1987
+ * })
1988
+ *
1989
+ * // Use with Kora sync server
1990
+ * const syncServer = new KoraSyncServer({
1991
+ * store,
1992
+ * auth: supabaseAuth.toSyncAuthProvider(),
1993
+ * })
1994
+ * ```
1995
+ */
1996
+ interface SupabaseAdapterConfig {
1997
+ /**
1998
+ * Supabase JWT secret from your project settings.
1999
+ *
2000
+ * Found in: Supabase Dashboard -> Settings -> API -> JWT Secret
2001
+ *
2002
+ * This is the HS256 HMAC secret used to sign and verify Supabase Auth JWTs.
2003
+ * Keep this secret secure and never expose it in client-side code.
2004
+ */
2005
+ jwtSecret: string;
2006
+ /**
2007
+ * Custom claim mapping override.
2008
+ *
2009
+ * By default, the Supabase adapter maps:
2010
+ * - `sub` -> `userId`
2011
+ * - `email` -> `email`
2012
+ * - `user_metadata.full_name` or `user_metadata.name` -> `name`
2013
+ * - `role`, `aud`, `app_metadata` -> `metadata`
2014
+ *
2015
+ * Override this to customize how Supabase claims are mapped to Kora's format.
2016
+ */
2017
+ mapClaims?: ExternalJwtProviderConfig['mapClaims'];
2018
+ }
2019
+ /**
2020
+ * Creates an ExternalJwtProvider configured for Supabase Auth.
2021
+ *
2022
+ * Supabase Auth uses HS256 signing with the project's JWT secret, making it
2023
+ * compatible with Kora's built-in `verifyJwt` utility. No external SDK is needed.
2024
+ *
2025
+ * This adapter validates the JWT signature and expiration, then maps Supabase's
2026
+ * standard claims (sub, email, role, user_metadata) to Kora's auth context format.
2027
+ *
2028
+ * @param config - Supabase adapter configuration
2029
+ * @returns An ExternalJwtProvider instance configured for Supabase
2030
+ *
2031
+ * @example
2032
+ * ```typescript
2033
+ * import { createSupabaseAdapter } from '@korajs/auth/server'
2034
+ *
2035
+ * const supabaseAuth = createSupabaseAdapter({
2036
+ * jwtSecret: process.env.SUPABASE_JWT_SECRET,
2037
+ * })
2038
+ *
2039
+ * // Validate a Supabase access token
2040
+ * const result = await supabaseAuth.validateAccessToken(supabaseAccessToken)
2041
+ * if (result) {
2042
+ * console.log('Supabase user:', result.userId)
2043
+ * }
2044
+ *
2045
+ * // Or use with the sync server
2046
+ * const syncServer = new KoraSyncServer({
2047
+ * store,
2048
+ * auth: supabaseAuth.toSyncAuthProvider(),
2049
+ * })
2050
+ * ```
2051
+ */
2052
+ declare function createSupabaseAdapter(config: SupabaseAdapterConfig): ExternalJwtProvider;
2053
+
2054
+ /**
2055
+ * Thrown when server-side passkey verification fails.
2056
+ */
2057
+ declare class PasskeyVerificationError extends KoraError {
2058
+ constructor(message: string, context?: Record<string, unknown>);
2059
+ }
2060
+ /** Options returned by generateRegistrationOptions for the client. */
2061
+ interface RegistrationOptions {
2062
+ /** Base64url-encoded random challenge (32 bytes) */
2063
+ challenge: string;
2064
+ /** Relying party ID (domain) */
2065
+ rpId: string;
2066
+ /** Relying party display name */
2067
+ rpName: string;
2068
+ /** Base64url-encoded user ID */
2069
+ userId: string;
2070
+ /** User's email or username */
2071
+ userName: string;
2072
+ /** Human-readable display name */
2073
+ userDisplayName: string;
2074
+ /** Credential IDs to exclude (prevents re-registration) */
2075
+ excludeCredentialIds: string[];
2076
+ /** Authenticator selection criteria */
2077
+ authenticatorSelection: {
2078
+ authenticatorAttachment: 'platform';
2079
+ residentKey: 'preferred';
2080
+ userVerification: 'required';
2081
+ };
2082
+ /** Timeout in milliseconds */
2083
+ timeout: number;
2084
+ }
2085
+ /**
2086
+ * Generate registration options for creating a new passkey.
2087
+ *
2088
+ * Creates a cryptographically random challenge and assembles the options
2089
+ * object that should be sent to the client for `createPasskeyCredential()`.
2090
+ *
2091
+ * The server must store the challenge (keyed by user session or similar)
2092
+ * for later verification when the client responds.
2093
+ *
2094
+ * @param params - Registration parameters
2095
+ * @param params.rpId - Relying party ID (your domain, e.g. "example.com")
2096
+ * @param params.rpName - Relying party display name
2097
+ * @param params.userId - Unique user identifier
2098
+ * @param params.userName - User's email or username
2099
+ * @param params.userDisplayName - Human-readable display name
2100
+ * @param params.existingCredentialIds - Base64url credential IDs to exclude
2101
+ * @returns Registration options to send to the client, including the challenge
2102
+ *
2103
+ * @example
2104
+ * ```typescript
2105
+ * const options = generateRegistrationOptions({
2106
+ * rpId: 'example.com',
2107
+ * rpName: 'My App',
2108
+ * userId: user.id,
2109
+ * userName: user.email,
2110
+ * userDisplayName: user.name,
2111
+ * })
2112
+ * // Store options.challenge in session for later verification
2113
+ * // Send options to client
2114
+ * ```
2115
+ */
2116
+ declare function generateRegistrationOptions(params: {
2117
+ rpId: string;
2118
+ rpName: string;
2119
+ userId: string;
2120
+ userName: string;
2121
+ userDisplayName: string;
2122
+ existingCredentialIds?: string[];
2123
+ }): RegistrationOptions;
2124
+ /** Result of verifying a registration response. */
2125
+ interface RegistrationVerificationResult {
2126
+ /** Whether the registration response was verified successfully */
2127
+ verified: boolean;
2128
+ /** Base64url-encoded credential ID */
2129
+ credentialId: string;
2130
+ /** Base64url-encoded COSE public key (store this for future authentication) */
2131
+ publicKey: string;
2132
+ /** Initial signature counter from the authenticator */
2133
+ signCount: number;
2134
+ }
2135
+ /**
2136
+ * Verify a registration response from the client.
2137
+ *
2138
+ * Validates the attestation object and clientDataJSON returned by the browser's
2139
+ * `navigator.credentials.create()` call. Extracts and returns the public key
2140
+ * and credential ID to store in your database.
2141
+ *
2142
+ * This implementation supports the "none" attestation format, which is the most
2143
+ * common and does not require trust in any attestation CA. For higher assurance
2144
+ * scenarios, extend this to verify packed/tpm/android attestation formats.
2145
+ *
2146
+ * @param params - Verification parameters
2147
+ * @param params.credential - The credential response from the client
2148
+ * @param params.expectedChallenge - The challenge that was sent to the client (base64url)
2149
+ * @param params.expectedOrigin - The expected origin (e.g. "https://example.com")
2150
+ * @param params.expectedRpId - The expected relying party ID (e.g. "example.com")
2151
+ * @returns Verification result with the credential ID and public key to store
2152
+ * @throws {PasskeyVerificationError} If the response is invalid or tampered with
2153
+ *
2154
+ * @example
2155
+ * ```typescript
2156
+ * const result = await verifyRegistrationResponse({
2157
+ * credential: clientResponse,
2158
+ * expectedChallenge: storedChallenge,
2159
+ * expectedOrigin: 'https://example.com',
2160
+ * expectedRpId: 'example.com',
2161
+ * })
2162
+ * if (result.verified) {
2163
+ * // Store result.credentialId, result.publicKey, result.signCount
2164
+ * }
2165
+ * ```
2166
+ */
2167
+ declare function verifyRegistrationResponse(params: {
2168
+ credential: {
2169
+ credentialId: string;
2170
+ publicKey: string;
2171
+ clientDataJSON: string;
2172
+ attestationObject: string;
2173
+ };
2174
+ expectedChallenge: string;
2175
+ expectedOrigin: string;
2176
+ expectedRpId: string;
2177
+ }): Promise<RegistrationVerificationResult>;
2178
+ /** Options returned by generateAuthenticationOptions for the client. */
2179
+ interface AuthenticationOptions {
2180
+ /** Base64url-encoded random challenge (32 bytes) */
2181
+ challenge: string;
2182
+ /** Relying party ID */
2183
+ rpId: string;
2184
+ /** Credential IDs to allow (limit to specific credentials) */
2185
+ allowCredentialIds?: string[];
2186
+ /** User verification requirement */
2187
+ userVerification: 'preferred';
2188
+ /** Timeout in milliseconds */
2189
+ timeout: number;
2190
+ }
2191
+ /**
2192
+ * Generate authentication options for signing in with a passkey.
2193
+ *
2194
+ * Creates a cryptographically random challenge and assembles the options
2195
+ * object that should be sent to the client for `authenticateWithPasskey()`.
2196
+ *
2197
+ * The server must store the challenge for later verification.
2198
+ *
2199
+ * @param params - Authentication parameters
2200
+ * @param params.rpId - Relying party ID (your domain)
2201
+ * @param params.allowCredentialIds - Base64url credential IDs to allow (optional)
2202
+ * @returns Authentication options to send to the client
2203
+ *
2204
+ * @example
2205
+ * ```typescript
2206
+ * const options = generateAuthenticationOptions({
2207
+ * rpId: 'example.com',
2208
+ * allowCredentialIds: user.credentialIds,
2209
+ * })
2210
+ * // Store options.challenge in session
2211
+ * // Send options to client
2212
+ * ```
2213
+ */
2214
+ declare function generateAuthenticationOptions(params: {
2215
+ rpId: string;
2216
+ allowCredentialIds?: string[];
2217
+ }): AuthenticationOptions;
2218
+ /** Result of verifying an authentication response. */
2219
+ interface AuthenticationVerificationResult {
2220
+ /** Whether the authentication response was verified successfully */
2221
+ verified: boolean;
2222
+ /** Updated signature counter (store this to detect cloned authenticators) */
2223
+ newSignCount: number;
2224
+ }
2225
+ /**
2226
+ * Verify an authentication response from the client.
2227
+ *
2228
+ * Validates the signed assertion returned by the browser's
2229
+ * `navigator.credentials.get()` call. Checks the signature against the
2230
+ * stored public key, verifies the challenge and origin, and validates
2231
+ * the signature counter to detect cloned authenticators.
2232
+ *
2233
+ * This implementation supports ECDSA P-256 (ES256, COSE algorithm -7)
2234
+ * signatures, which is the most common algorithm used by platform
2235
+ * authenticators (Touch ID, Face ID, Windows Hello).
2236
+ *
2237
+ * @param params - Verification parameters
2238
+ * @param params.assertion - The assertion response from the client
2239
+ * @param params.expectedChallenge - The challenge that was sent to the client (base64url)
2240
+ * @param params.expectedOrigin - The expected origin (e.g. "https://example.com")
2241
+ * @param params.expectedRpId - The expected relying party ID
2242
+ * @param params.publicKey - The stored COSE public key (base64url, from registration)
2243
+ * @param params.previousSignCount - The previously stored signature counter
2244
+ * @returns Verification result with the new signature counter
2245
+ * @throws {PasskeyVerificationError} If the assertion is invalid
2246
+ *
2247
+ * @example
2248
+ * ```typescript
2249
+ * const result = await verifyAuthenticationResponse({
2250
+ * assertion: clientAssertion,
2251
+ * expectedChallenge: storedChallenge,
2252
+ * expectedOrigin: 'https://example.com',
2253
+ * expectedRpId: 'example.com',
2254
+ * publicKey: storedCredential.publicKey,
2255
+ * previousSignCount: storedCredential.signCount,
2256
+ * })
2257
+ * if (result.verified) {
2258
+ * // Update stored sign count: storedCredential.signCount = result.newSignCount
2259
+ * // Issue session tokens
2260
+ * }
2261
+ * ```
2262
+ */
2263
+ declare function verifyAuthenticationResponse(params: {
2264
+ assertion: {
2265
+ credentialId: string;
2266
+ authenticatorData: string;
2267
+ clientDataJSON: string;
2268
+ signature: string;
2269
+ userHandle: string | null;
2270
+ };
2271
+ expectedChallenge: string;
2272
+ expectedOrigin: string;
2273
+ expectedRpId: string;
2274
+ publicKey: string;
2275
+ previousSignCount: number;
2276
+ }): Promise<AuthenticationVerificationResult>;
2277
+
2278
+ /**
2279
+ * An organization (workspace/team) that groups users together.
2280
+ *
2281
+ * Organizations are the fundamental unit of multi-tenancy in Kora.
2282
+ * Data is scoped to organizations, and users access data through
2283
+ * their organization memberships and roles.
2284
+ */
2285
+ interface Organization {
2286
+ /** Unique identifier (UUID v7) */
2287
+ id: string;
2288
+ /** Display name of the organization */
2289
+ name: string;
2290
+ /** URL-friendly identifier (unique, lowercase, alphanumeric + hyphens) */
2291
+ slug: string;
2292
+ /** User ID of the organization owner */
2293
+ ownerId: string;
2294
+ /** When the organization was created (ms since epoch) */
2295
+ createdAt: number;
2296
+ /** When the organization was last updated (ms since epoch) */
2297
+ updatedAt: number;
2298
+ /** Arbitrary metadata (plan, billing info, settings, etc.) */
2299
+ metadata: Record<string, unknown>;
2300
+ }
2301
+ /**
2302
+ * Parameters for creating a new organization.
2303
+ */
2304
+ interface CreateOrgParams {
2305
+ /** Display name */
2306
+ name: string;
2307
+ /** URL-friendly slug (auto-generated from name if omitted) */
2308
+ slug?: string;
2309
+ /** Optional metadata to attach */
2310
+ metadata?: Record<string, unknown>;
2311
+ }
2312
+ /**
2313
+ * Parameters for updating an existing organization.
2314
+ */
2315
+ interface UpdateOrgParams {
2316
+ /** New display name */
2317
+ name?: string;
2318
+ /** New slug */
2319
+ slug?: string;
2320
+ /** Metadata to merge (shallow merge) */
2321
+ metadata?: Record<string, unknown>;
2322
+ }
2323
+ /**
2324
+ * Built-in organization roles, ordered by decreasing privilege.
2325
+ *
2326
+ * - **owner**: Full control, can delete org, transfer ownership, manage billing
2327
+ * - **admin**: Manage members and settings, full data access
2328
+ * - **member**: Read + write own data, read shared data
2329
+ * - **viewer**: Read-only access to shared data
2330
+ * - **billing**: Billing management only, no data access
2331
+ */
2332
+ declare const ORG_ROLES: readonly ["owner", "admin", "member", "viewer", "billing"];
2333
+ type OrgRole = (typeof ORG_ROLES)[number];
2334
+ /**
2335
+ * Role hierarchy for permission inheritance.
2336
+ * Higher number = more privilege.
2337
+ */
2338
+ declare const ROLE_HIERARCHY: Record<OrgRole, number>;
2339
+ /**
2340
+ * Check if one role has at least the privilege level of another.
2341
+ *
2342
+ * @param userRole - The user's current role
2343
+ * @param requiredRole - The minimum role required
2344
+ * @returns true if userRole >= requiredRole in the hierarchy
2345
+ */
2346
+ declare function hasRoleLevel(userRole: OrgRole, requiredRole: OrgRole): boolean;
2347
+ /**
2348
+ * A user's membership in an organization.
2349
+ */
2350
+ interface Membership {
2351
+ /** Unique identifier for this membership record */
2352
+ id: string;
2353
+ /** Organization this membership belongs to */
2354
+ orgId: string;
2355
+ /** User who is a member */
2356
+ userId: string;
2357
+ /** Role within the organization */
2358
+ role: OrgRole;
2359
+ /** User who invited this member (null if founder) */
2360
+ invitedBy: string | null;
2361
+ /** When the user joined the organization (ms since epoch) */
2362
+ joinedAt: number;
2363
+ /** Arbitrary metadata (department, title, etc.) */
2364
+ metadata: Record<string, unknown>;
2365
+ }
2366
+ /**
2367
+ * Invitation status lifecycle.
2368
+ */
2369
+ declare const INVITATION_STATUSES: readonly ["pending", "accepted", "revoked", "expired"];
2370
+ type InvitationStatus = (typeof INVITATION_STATUSES)[number];
2371
+ /**
2372
+ * An invitation to join an organization.
2373
+ *
2374
+ * Invitations are sent by email and include a single-use token.
2375
+ * They expire after a configurable duration (default: 7 days).
2376
+ */
2377
+ interface OrgInvitation {
2378
+ /** Unique identifier */
2379
+ id: string;
2380
+ /** Organization the invitation is for */
2381
+ orgId: string;
2382
+ /** Email address of the invitee */
2383
+ email: string;
2384
+ /** Role the invitee will receive upon accepting */
2385
+ role: OrgRole;
2386
+ /** User who created the invitation */
2387
+ invitedBy: string;
2388
+ /** Cryptographically random single-use token */
2389
+ token: string;
2390
+ /** When the invitation was created (ms since epoch) */
2391
+ createdAt: number;
2392
+ /** When the invitation expires (ms since epoch) */
2393
+ expiresAt: number;
2394
+ /** Current status */
2395
+ status: InvitationStatus;
2396
+ }
2397
+ /**
2398
+ * Parameters for creating an invitation.
2399
+ */
2400
+ interface CreateInvitationParams {
2401
+ /** Email address to invite */
2402
+ email: string;
2403
+ /** Role to assign when the invitation is accepted */
2404
+ role: OrgRole;
2405
+ }
2406
+ /**
2407
+ * Base error for organization-related operations.
2408
+ */
2409
+ declare class OrgError extends KoraError {
2410
+ constructor(message: string, code: string, context?: Record<string, unknown>);
2411
+ }
2412
+ declare class OrgNotFoundError extends OrgError {
2413
+ constructor();
2414
+ }
2415
+ declare class OrgSlugTakenError extends OrgError {
2416
+ constructor();
2417
+ }
2418
+ declare class MembershipNotFoundError extends OrgError {
2419
+ constructor();
2420
+ }
2421
+ declare class MemberAlreadyExistsError extends OrgError {
2422
+ constructor();
2423
+ }
2424
+ declare class InsufficientRoleError extends OrgError {
2425
+ constructor(required: OrgRole);
2426
+ }
2427
+ declare class CannotRemoveOwnerError extends OrgError {
2428
+ constructor();
2429
+ }
2430
+ declare class InvitationNotFoundError extends OrgError {
2431
+ constructor();
2432
+ }
2433
+ declare class InvitationExpiredError extends OrgError {
2434
+ constructor();
2435
+ }
2436
+
2437
+ /**
2438
+ * Persistence interface for organizations, memberships, and invitations.
2439
+ *
2440
+ * Implement this interface to back organizations with any storage:
2441
+ * - `InMemoryOrgStore` for development and testing
2442
+ * - PostgreSQL/MySQL via Drizzle for production
2443
+ * - SQLite for self-hosted or embedded scenarios
2444
+ *
2445
+ * All methods are async to support any storage backend.
2446
+ */
2447
+ interface OrgStore {
2448
+ /** Create a new organization. The caller becomes the owner. */
2449
+ createOrg(ownerId: string, params: CreateOrgParams): Promise<Organization>;
2450
+ /** Get an organization by ID. Returns null if not found. */
2451
+ getOrg(orgId: string): Promise<Organization | null>;
2452
+ /** Get an organization by slug. Returns null if not found. */
2453
+ getOrgBySlug(slug: string): Promise<Organization | null>;
2454
+ /** Update an organization's mutable fields. */
2455
+ updateOrg(orgId: string, params: UpdateOrgParams): Promise<Organization>;
2456
+ /** Delete an organization and all its memberships and invitations. */
2457
+ deleteOrg(orgId: string): Promise<void>;
2458
+ /** List all organizations a user is a member of. */
2459
+ listUserOrgs(userId: string): Promise<Organization[]>;
2460
+ /** Add a user as a member of an organization. */
2461
+ addMember(orgId: string, userId: string, role: OrgRole, invitedBy: string | null): Promise<Membership>;
2462
+ /** Remove a user from an organization. Cannot remove the owner. */
2463
+ removeMember(orgId: string, userId: string): Promise<void>;
2464
+ /** Update a member's role within an organization. */
2465
+ updateMemberRole(orgId: string, userId: string, role: OrgRole): Promise<Membership>;
2466
+ /** List all members of an organization. */
2467
+ listMembers(orgId: string): Promise<Membership[]>;
2468
+ /** Get a specific user's membership in an organization. Returns null if not a member. */
2469
+ getMembership(orgId: string, userId: string): Promise<Membership | null>;
2470
+ /** Transfer ownership of an organization to another member. */
2471
+ transferOwnership(orgId: string, newOwnerId: string): Promise<void>;
2472
+ /** Create an invitation to join an organization. */
2473
+ createInvitation(orgId: string, invitedBy: string, params: CreateInvitationParams): Promise<OrgInvitation>;
2474
+ /** Look up an invitation by its single-use token. Returns null if not found or already consumed. */
2475
+ getInvitationByToken(token: string): Promise<OrgInvitation | null>;
2476
+ /** Consume an invitation (mark as accepted). Returns the invitation details. */
2477
+ consumeInvitation(token: string): Promise<OrgInvitation>;
2478
+ /** Revoke a pending invitation. */
2479
+ revokeInvitation(invitationId: string): Promise<void>;
2480
+ /** List pending invitations for an organization. */
2481
+ listPendingInvitations(orgId: string): Promise<OrgInvitation[]>;
2482
+ /** List pending invitations for a specific email address. */
2483
+ listInvitationsForEmail(email: string): Promise<OrgInvitation[]>;
2484
+ /** Remove expired invitations. Returns count of removed invitations. */
2485
+ cleanExpiredInvitations(): Promise<number>;
2486
+ }
2487
+ /**
2488
+ * In-memory implementation of OrgStore for development and testing.
2489
+ *
2490
+ * Data is lost when the process exits. For production, implement OrgStore
2491
+ * with a persistent backend (PostgreSQL, MySQL, SQLite via Drizzle).
2492
+ *
2493
+ * @example
2494
+ * ```typescript
2495
+ * const orgStore = new InMemoryOrgStore()
2496
+ * const org = await orgStore.createOrg('user-1', { name: 'Acme Inc', slug: 'acme' })
2497
+ * ```
2498
+ */
2499
+ declare class InMemoryOrgStore implements OrgStore {
2500
+ private orgs;
2501
+ private memberships;
2502
+ private invitations;
2503
+ createOrg(ownerId: string, params: CreateOrgParams): Promise<Organization>;
2504
+ getOrg(orgId: string): Promise<Organization | null>;
2505
+ getOrgBySlug(slug: string): Promise<Organization | null>;
2506
+ updateOrg(orgId: string, params: UpdateOrgParams): Promise<Organization>;
2507
+ deleteOrg(orgId: string): Promise<void>;
2508
+ listUserOrgs(userId: string): Promise<Organization[]>;
2509
+ addMember(orgId: string, userId: string, role: OrgRole, invitedBy: string | null): Promise<Membership>;
2510
+ removeMember(orgId: string, userId: string): Promise<void>;
2511
+ updateMemberRole(orgId: string, userId: string, role: OrgRole): Promise<Membership>;
2512
+ listMembers(orgId: string): Promise<Membership[]>;
2513
+ getMembership(orgId: string, userId: string): Promise<Membership | null>;
2514
+ transferOwnership(orgId: string, newOwnerId: string): Promise<void>;
2515
+ createInvitation(orgId: string, invitedBy: string, params: CreateInvitationParams): Promise<OrgInvitation>;
2516
+ getInvitationByToken(token: string): Promise<OrgInvitation | null>;
2517
+ consumeInvitation(token: string): Promise<OrgInvitation>;
2518
+ revokeInvitation(invitationId: string): Promise<void>;
2519
+ listPendingInvitations(orgId: string): Promise<OrgInvitation[]>;
2520
+ listInvitationsForEmail(email: string): Promise<OrgInvitation[]>;
2521
+ cleanExpiredInvitations(): Promise<number>;
2522
+ }
2523
+
2524
+ /**
2525
+ * Response envelope returned by all org route handlers.
2526
+ * Mirrors AuthRouteResponse for consistency.
2527
+ */
2528
+ interface OrgRouteResponse<T> {
2529
+ /** HTTP status code */
2530
+ status: number;
2531
+ /** Either the success payload or an error message */
2532
+ body: {
2533
+ data: T;
2534
+ } | {
2535
+ error: string;
2536
+ };
2537
+ }
2538
+ /**
2539
+ * Configuration for the org route handlers.
2540
+ */
2541
+ interface OrgRoutesConfig {
2542
+ /** The organization store backing all org operations */
2543
+ orgStore: OrgStore;
2544
+ }
2545
+ /**
2546
+ * Server-side route handlers for organization management.
2547
+ *
2548
+ * These handlers enforce authorization (role checks), input validation,
2549
+ * and produce transport-agnostic response objects. Wire them to your
2550
+ * HTTP framework (Express, Hono, Fastify, etc.):
2551
+ *
2552
+ * @example
2553
+ * ```typescript
2554
+ * const orgRoutes = new OrgRoutes({ orgStore: new InMemoryOrgStore() })
2555
+ *
2556
+ * app.post('/orgs', async (req, res) => {
2557
+ * const result = await orgRoutes.createOrg(req.userId, req.body)
2558
+ * res.status(result.status).json(result.body)
2559
+ * })
2560
+ * ```
2561
+ */
2562
+ declare class OrgRoutes {
2563
+ private readonly store;
2564
+ constructor(config: OrgRoutesConfig);
2565
+ /**
2566
+ * Create a new organization. The authenticated user becomes the owner.
2567
+ */
2568
+ createOrg(userId: string, params: {
2569
+ name?: unknown;
2570
+ slug?: unknown;
2571
+ metadata?: unknown;
2572
+ }): Promise<OrgRouteResponse<Organization>>;
2573
+ /**
2574
+ * Get an organization by ID. Requires membership.
2575
+ */
2576
+ getOrg(userId: string, orgId: string): Promise<OrgRouteResponse<Organization>>;
2577
+ /**
2578
+ * Update an organization. Requires admin or higher.
2579
+ */
2580
+ updateOrg(userId: string, orgId: string, params: {
2581
+ name?: unknown;
2582
+ slug?: unknown;
2583
+ metadata?: unknown;
2584
+ }): Promise<OrgRouteResponse<Organization>>;
2585
+ /**
2586
+ * Delete an organization. Requires owner.
2587
+ */
2588
+ deleteOrg(userId: string, orgId: string): Promise<OrgRouteResponse<{
2589
+ deleted: true;
2590
+ }>>;
2591
+ /**
2592
+ * List all organizations the authenticated user belongs to.
2593
+ */
2594
+ listUserOrgs(userId: string): Promise<OrgRouteResponse<Organization[]>>;
2595
+ /**
2596
+ * Add a member to an organization. Requires admin or higher.
2597
+ */
2598
+ addMember(userId: string, orgId: string, params: {
2599
+ targetUserId?: unknown;
2600
+ role?: unknown;
2601
+ }): Promise<OrgRouteResponse<Membership>>;
2602
+ /**
2603
+ * Remove a member from an organization. Requires admin or higher.
2604
+ * Members can also remove themselves (leave).
2605
+ */
2606
+ removeMember(userId: string, orgId: string, targetUserId: string): Promise<OrgRouteResponse<{
2607
+ removed: true;
2608
+ }>>;
2609
+ /**
2610
+ * Update a member's role. Requires admin or higher.
2611
+ */
2612
+ updateMemberRole(userId: string, orgId: string, params: {
2613
+ targetUserId?: unknown;
2614
+ role?: unknown;
2615
+ }): Promise<OrgRouteResponse<Membership>>;
2616
+ /**
2617
+ * List all members of an organization. Requires membership.
2618
+ */
2619
+ listMembers(userId: string, orgId: string): Promise<OrgRouteResponse<Membership[]>>;
2620
+ /**
2621
+ * Transfer ownership to another member. Requires owner.
2622
+ */
2623
+ transferOwnership(userId: string, orgId: string, params: {
2624
+ newOwnerId?: unknown;
2625
+ }): Promise<OrgRouteResponse<{
2626
+ transferred: true;
2627
+ }>>;
2628
+ /**
2629
+ * Create an invitation to join the organization. Requires admin or higher.
2630
+ */
2631
+ createInvitation(userId: string, orgId: string, params: {
2632
+ email?: unknown;
2633
+ role?: unknown;
2634
+ }): Promise<OrgRouteResponse<OrgInvitation>>;
2635
+ /**
2636
+ * Accept an invitation by its token. The authenticated user joins the org.
2637
+ */
2638
+ acceptInvitation(userId: string, params: {
2639
+ token?: unknown;
2640
+ }): Promise<OrgRouteResponse<Membership>>;
2641
+ /**
2642
+ * Revoke a pending invitation. Requires admin or higher.
2643
+ */
2644
+ revokeInvitation(userId: string, orgId: string, invitationId: string): Promise<OrgRouteResponse<{
2645
+ revoked: true;
2646
+ }>>;
2647
+ /**
2648
+ * List pending invitations for an organization. Requires admin or higher.
2649
+ */
2650
+ listPendingInvitations(userId: string, orgId: string): Promise<OrgRouteResponse<OrgInvitation[]>>;
2651
+ /**
2652
+ * List pending invitations for the authenticated user's email.
2653
+ */
2654
+ listMyInvitations(email: string): Promise<OrgRouteResponse<OrgInvitation[]>>;
2655
+ /**
2656
+ * Check if the caller has the required role in the org.
2657
+ * Returns an error response if not authorized, or null if authorized.
2658
+ */
2659
+ private requireRole;
2660
+ }
2661
+
2662
+ /**
2663
+ * A permission is a `resource:action` string.
2664
+ *
2665
+ * Resources are typically collection names or system resources.
2666
+ * Actions are the operations allowed on that resource.
2667
+ *
2668
+ * @example
2669
+ * ```typescript
2670
+ * const permission: Permission = 'todos:write'
2671
+ * const adminPerm: Permission = 'org:manage-members'
2672
+ * const wildcard: Permission = 'todos:*' // all actions on todos
2673
+ * const superAdmin: Permission = '*:*' // all actions on all resources
2674
+ * ```
2675
+ */
2676
+ type Permission = `${string}:${string}`;
2677
+ /**
2678
+ * Parse a permission string into its resource and action parts.
2679
+ */
2680
+ declare function parsePermission(permission: Permission): {
2681
+ resource: string;
2682
+ action: string;
2683
+ };
2684
+ /**
2685
+ * Check if a granted permission covers a required permission.
2686
+ * Supports wildcards: `todos:*` covers `todos:read`, `*:*` covers everything.
2687
+ */
2688
+ declare function permissionCovers(granted: Permission, required: Permission): boolean;
2689
+ /**
2690
+ * A role definition describes a named set of permissions with optional inheritance.
2691
+ *
2692
+ * Roles form a hierarchy via `inherits`. When evaluating permissions,
2693
+ * all inherited roles' permissions are included transitively.
2694
+ *
2695
+ * @example
2696
+ * ```typescript
2697
+ * const roles: RoleDefinition[] = [
2698
+ * { name: 'viewer', permissions: ['todos:read', 'projects:read'] },
2699
+ * { name: 'member', permissions: ['todos:write', 'projects:write'], inherits: ['viewer'] },
2700
+ * { name: 'admin', permissions: ['org:manage-members'], inherits: ['member'] },
2701
+ * ]
2702
+ * ```
2703
+ */
2704
+ interface RoleDefinition {
2705
+ /** Unique name for this role */
2706
+ name: string;
2707
+ /** Permissions directly granted to this role */
2708
+ permissions: Permission[];
2709
+ /** Roles this role inherits from (all their permissions are included) */
2710
+ inherits?: string[];
2711
+ }
2712
+ /**
2713
+ * Default built-in roles for organizations.
2714
+ *
2715
+ * These can be overridden or extended using `defineRoles()`.
2716
+ */
2717
+ declare const BUILT_IN_ROLES: readonly RoleDefinition[];
2718
+ /**
2719
+ * Configuration for the RBAC engine.
2720
+ */
2721
+ interface RbacConfig {
2722
+ /** Role definitions. Defaults to BUILT_IN_ROLES if not provided. */
2723
+ roles?: RoleDefinition[];
2724
+ }
2725
+ /**
2726
+ * A sync scope filter for a single collection.
2727
+ * The keys are field names and values are the required values.
2728
+ * A special `__readonly` key can be set to restrict to read-only access.
2729
+ *
2730
+ * @example
2731
+ * ```typescript
2732
+ * // Only sync todos belonging to the user within the org
2733
+ * const scope: ScopeFilter = { orgId: 'org-123', userId: 'user-456' }
2734
+ *
2735
+ * // Read-only scope for viewers
2736
+ * const readOnlyScope: ScopeFilter = { orgId: 'org-123', __readonly: true }
2737
+ * ```
2738
+ */
2739
+ interface ScopeFilter {
2740
+ [field: string]: unknown;
2741
+ }
2742
+ /**
2743
+ * A complete set of sync scopes, keyed by collection name.
2744
+ *
2745
+ * @example
2746
+ * ```typescript
2747
+ * const scopes: SyncScopes = {
2748
+ * todos: { orgId: 'org-123' },
2749
+ * projects: { orgId: 'org-123' },
2750
+ * }
2751
+ * ```
2752
+ */
2753
+ type SyncScopes = Record<string, ScopeFilter>;
2754
+ /**
2755
+ * Custom scope resolver for a specific collection.
2756
+ * Given the user context, returns the scope filter for that collection.
2757
+ */
2758
+ type CollectionScopeResolver = (ctx: ScopeContext) => ScopeFilter | null;
2759
+ /**
2760
+ * Context passed to scope resolvers.
2761
+ */
2762
+ interface ScopeContext {
2763
+ userId: string;
2764
+ orgId: string;
2765
+ role: string;
2766
+ permissions: Permission[];
2767
+ }
2768
+ declare class RbacError extends KoraError {
2769
+ constructor(message: string, code: string, context?: Record<string, unknown>);
2770
+ }
2771
+ declare class InvalidPermissionError extends RbacError {
2772
+ constructor(permission: string);
2773
+ }
2774
+ declare class RoleNotFoundError extends RbacError {
2775
+ constructor(role: string);
2776
+ }
2777
+ declare class CircularInheritanceError extends RbacError {
2778
+ constructor(chain: string[]);
2779
+ }
2780
+
2781
+ /**
2782
+ * Permission evaluation engine for role-based access control.
2783
+ *
2784
+ * The engine resolves permissions through role inheritance, supports
2785
+ * wildcard matching, and integrates with the OrgStore for membership lookups.
2786
+ *
2787
+ * @example
2788
+ * ```typescript
2789
+ * const rbac = new RbacEngine({ orgStore })
2790
+ *
2791
+ * // Check a permission
2792
+ * const canWrite = await rbac.hasPermission('user-1', 'org-1', 'todos:write')
2793
+ *
2794
+ * // Get all permissions for a user in an org
2795
+ * const perms = await rbac.getUserPermissions('user-1', 'org-1')
2796
+ *
2797
+ * // Resolve sync scopes
2798
+ * const scopes = await rbac.resolveScopes('user-1', 'org-1')
2799
+ * ```
2800
+ */
2801
+ declare class RbacEngine {
2802
+ private readonly orgStore;
2803
+ private readonly roleMap;
2804
+ private readonly resolvedPermissions;
2805
+ private readonly collectionResolvers;
2806
+ constructor(orgStore: OrgStore, config?: RbacConfig);
2807
+ /**
2808
+ * Check if a user has a specific permission in an organization.
2809
+ *
2810
+ * Resolves the user's role from the org membership, then evaluates
2811
+ * the role's permissions (including inherited ones) against the required permission.
2812
+ *
2813
+ * Returns false (not an error) if the user is not a member.
2814
+ */
2815
+ hasPermission(userId: string, orgId: string, permission: Permission): Promise<boolean>;
2816
+ /**
2817
+ * Get all effective permissions for a user in an organization.
2818
+ *
2819
+ * Returns an empty array if the user is not a member.
2820
+ */
2821
+ getUserPermissions(userId: string, orgId: string): Promise<Permission[]>;
2822
+ /**
2823
+ * Get all effective permissions for a role name.
2824
+ * Includes permissions from inherited roles.
2825
+ *
2826
+ * @throws {RoleNotFoundError} if the role is not defined
2827
+ */
2828
+ getRolePermissions(roleName: string): Permission[];
2829
+ /**
2830
+ * Check if a role has a specific permission.
2831
+ */
2832
+ roleHasPermission(roleName: string, permission: Permission): boolean;
2833
+ /**
2834
+ * Register a custom scope resolver for a collection.
2835
+ *
2836
+ * Custom resolvers override the default scope logic for that collection.
2837
+ */
2838
+ registerScopeResolver(collection: string, resolver: CollectionScopeResolver): void;
2839
+ /**
2840
+ * Resolve sync scopes for a user in an organization.
2841
+ *
2842
+ * The scopes determine what data the user can see and modify during sync.
2843
+ * - Owner/Admin: all org data
2844
+ * - Member: all org data (read + write)
2845
+ * - Viewer: all org data (read-only)
2846
+ * - Billing: no data scopes (billing-only access)
2847
+ *
2848
+ * Custom collection resolvers can override the defaults.
2849
+ *
2850
+ * @param collections - List of collection names to resolve scopes for.
2851
+ * If not provided, only custom-registered collections are included.
2852
+ */
2853
+ resolveScopes(userId: string, orgId: string, collections?: string[]): Promise<SyncScopes | null>;
2854
+ /**
2855
+ * Get all defined role names.
2856
+ */
2857
+ getRoleNames(): string[];
2858
+ /**
2859
+ * Get a role definition by name.
2860
+ */
2861
+ getRoleDefinition(roleName: string): RoleDefinition | null;
2862
+ /**
2863
+ * Validate all role definitions for circular inheritance and unknown references.
2864
+ */
2865
+ private validateRoles;
2866
+ /**
2867
+ * Detect circular inheritance in role hierarchy.
2868
+ */
2869
+ private detectCircularInheritance;
2870
+ /**
2871
+ * Recursively resolve all permissions for a role, including inherited permissions.
2872
+ */
2873
+ private resolvePermissionsForRole;
2874
+ }
2875
+ /**
2876
+ * Builder for defining custom roles.
2877
+ *
2878
+ * @example
2879
+ * ```typescript
2880
+ * const roles = defineRoles()
2881
+ * .role('viewer', ['*:read'])
2882
+ * .role('editor', ['*:write'], { inherits: ['viewer'] })
2883
+ * .role('admin', ['org:manage-members'], { inherits: ['editor'] })
2884
+ * .build()
2885
+ * ```
2886
+ */
2887
+ declare function defineRoles(): RoleBuilder;
2888
+ declare class RoleBuilder {
2889
+ private roles;
2890
+ /**
2891
+ * Add a role definition.
2892
+ */
2893
+ role(name: string, permissions: Permission[], options?: {
2894
+ inherits?: string[];
2895
+ }): RoleBuilder;
2896
+ /**
2897
+ * Include the built-in roles as a base.
2898
+ */
2899
+ withBuiltInRoles(): RoleBuilder;
2900
+ /**
2901
+ * Build and return the role definitions array.
2902
+ */
2903
+ build(): RoleDefinition[];
2904
+ }
2905
+
2906
+ /**
2907
+ * Resolves sync scopes for org-aware data filtering.
2908
+ *
2909
+ * Given a user, an organization, and a set of collections, this resolver
2910
+ * determines what data the user should receive during sync:
2911
+ *
2912
+ * - **Owner/Admin**: All data in the org (no field-level filtering)
2913
+ * - **Member**: All org data with read/write access
2914
+ * - **Viewer**: All org data with read-only flag
2915
+ * - **Billing**: No data access (empty scopes)
2916
+ *
2917
+ * Developers can register per-collection scope resolvers for fine-grained control.
2918
+ *
2919
+ * @example
2920
+ * ```typescript
2921
+ * const resolver = new OrgScopeResolver(orgStore, rbacEngine)
2922
+ *
2923
+ * // Custom scope: members only see their own todos
2924
+ * resolver.registerCollectionScope('todos', (ctx) => {
2925
+ * if (ctx.role === 'member') {
2926
+ * return { orgId: ctx.orgId, userId: ctx.userId }
2927
+ * }
2928
+ * return { orgId: ctx.orgId } // admins/owners see all
2929
+ * })
2930
+ *
2931
+ * const scopes = await resolver.resolve('user-1', 'org-1', ['todos', 'projects'])
2932
+ * // { todos: { orgId: 'org-1', userId: 'user-1' }, projects: { orgId: 'org-1' } }
2933
+ * ```
2934
+ */
2935
+ declare class OrgScopeResolver {
2936
+ private readonly orgStore;
2937
+ private readonly rbac;
2938
+ private readonly collectionScopes;
2939
+ constructor(orgStore: OrgStore, rbac: RbacEngine);
2940
+ /**
2941
+ * Register a custom scope resolver for a collection.
2942
+ * This overrides the default orgId-based filtering for that collection.
2943
+ */
2944
+ registerCollectionScope(collection: string, resolver: CollectionScopeResolver): void;
2945
+ /**
2946
+ * Resolve sync scopes for all specified collections.
2947
+ *
2948
+ * Returns null if the user is not a member of the organization.
2949
+ * Returns an empty object if the user has no data access (e.g., billing role).
2950
+ */
2951
+ resolve(userId: string, orgId: string, collections: string[]): Promise<SyncScopes | null>;
2952
+ /**
2953
+ * Check if a user can write to a specific collection in an org.
2954
+ */
2955
+ canWrite(userId: string, orgId: string, collection: string): Promise<boolean>;
2956
+ /**
2957
+ * Check if a user can read from a specific collection in an org.
2958
+ */
2959
+ canRead(userId: string, orgId: string, collection: string): Promise<boolean>;
2960
+ private resolveCollectionScope;
2961
+ }
2962
+
2963
+ /**
2964
+ * Configuration for an OAuth 2.0 provider.
2965
+ */
2966
+ interface OAuthProviderConfig {
2967
+ /** Provider identifier (e.g., 'google', 'github', 'microsoft') */
2968
+ providerId: string;
2969
+ /** OAuth client ID */
2970
+ clientId: string;
2971
+ /** OAuth client secret */
2972
+ clientSecret: string;
2973
+ /** Authorization endpoint URL */
2974
+ authorizationUrl: string;
2975
+ /** Token exchange endpoint URL */
2976
+ tokenUrl: string;
2977
+ /** User info endpoint URL */
2978
+ userInfoUrl: string;
2979
+ /** OAuth scopes to request */
2980
+ scopes: string[];
2981
+ /** Redirect URI for the callback */
2982
+ redirectUri: string;
2983
+ }
2984
+ /**
2985
+ * Tokens returned by the OAuth provider after code exchange.
2986
+ */
2987
+ interface OAuthTokens {
2988
+ /** OAuth access token */
2989
+ accessToken: string;
2990
+ /** Token type (usually 'Bearer') */
2991
+ tokenType: string;
2992
+ /** Access token expiry in seconds (if provided) */
2993
+ expiresIn?: number;
2994
+ /** Refresh token (if provided) */
2995
+ refreshToken?: string;
2996
+ /** ID token (if provided, e.g., OpenID Connect) */
2997
+ idToken?: string;
2998
+ /** Granted scopes (may differ from requested scopes) */
2999
+ scope?: string;
3000
+ }
3001
+ /**
3002
+ * User information from the OAuth provider.
3003
+ */
3004
+ interface OAuthUserInfo {
3005
+ /** Provider-specific user ID */
3006
+ providerId: string;
3007
+ /** Provider name (e.g., 'google', 'github') */
3008
+ provider: string;
3009
+ /** User's email address (may be null if not granted) */
3010
+ email: string | null;
3011
+ /** Whether the email is verified by the provider */
3012
+ emailVerified: boolean;
3013
+ /** User's display name */
3014
+ name: string | null;
3015
+ /** URL to the user's avatar/profile picture */
3016
+ avatarUrl: string | null;
3017
+ /** Raw profile data from the provider */
3018
+ rawProfile: Record<string, unknown>;
3019
+ }
3020
+ /**
3021
+ * State stored during the OAuth flow for CSRF protection.
3022
+ */
3023
+ interface OAuthState {
3024
+ /** Random state parameter for CSRF protection */
3025
+ state: string;
3026
+ /** Provider ID */
3027
+ provider: string;
3028
+ /** Redirect URI used for this flow */
3029
+ redirectUri: string;
3030
+ /** When this state was created (ms since epoch) */
3031
+ createdAt: number;
3032
+ /** When this state expires (ms since epoch) */
3033
+ expiresAt: number;
3034
+ /** Optional: user-defined data to pass through the flow */
3035
+ metadata?: Record<string, unknown>;
3036
+ }
3037
+ /**
3038
+ * Store for OAuth state parameters.
3039
+ */
3040
+ interface OAuthStateStore {
3041
+ /** Store a state parameter for later validation. */
3042
+ store(state: OAuthState): Promise<void>;
3043
+ /** Consume a state parameter (single-use). Returns null if not found or expired. */
3044
+ consume(stateValue: string): Promise<OAuthState | null>;
3045
+ /** Clean up expired states. */
3046
+ cleanExpired(): Promise<number>;
3047
+ }
3048
+ /**
3049
+ * A linked OAuth identity for a user.
3050
+ * Users can have multiple linked identities (e.g., Google + GitHub).
3051
+ */
3052
+ interface LinkedIdentity {
3053
+ /** Unique ID of this link */
3054
+ id: string;
3055
+ /** Kora user ID */
3056
+ userId: string;
3057
+ /** OAuth provider name */
3058
+ provider: string;
3059
+ /** Provider-specific user ID */
3060
+ providerUserId: string;
3061
+ /** Provider email (at time of linking) */
3062
+ email: string | null;
3063
+ /** When this identity was linked */
3064
+ linkedAt: number;
3065
+ }
3066
+ declare class OAuthError extends KoraError {
3067
+ constructor(message: string, code: string, context?: Record<string, unknown>);
3068
+ }
3069
+ declare class OAuthStateMismatchError extends OAuthError {
3070
+ constructor();
3071
+ }
3072
+ declare class OAuthCodeExchangeError extends OAuthError {
3073
+ constructor(details?: string);
3074
+ }
3075
+ declare class OAuthUserInfoError extends OAuthError {
3076
+ constructor(details?: string);
3077
+ }
3078
+ declare class OAuthProviderNotFoundError extends OAuthError {
3079
+ constructor(provider: string);
3080
+ }
3081
+
3082
+ /**
3083
+ * In-memory OAuth state store for development.
3084
+ * Use Redis or a database in production for multi-server deployments.
3085
+ */
3086
+ declare class InMemoryOAuthStateStore implements OAuthStateStore {
3087
+ private readonly states;
3088
+ store(state: OAuthState): Promise<void>;
3089
+ consume(stateValue: string): Promise<OAuthState | null>;
3090
+ cleanExpired(): Promise<number>;
3091
+ }
3092
+ /**
3093
+ * Configuration for the OAuth manager.
3094
+ */
3095
+ interface OAuthManagerConfig {
3096
+ /** Registered OAuth providers */
3097
+ providers: OAuthProviderConfig[];
3098
+ /** State store. Defaults to InMemoryOAuthStateStore. */
3099
+ stateStore?: OAuthStateStore;
3100
+ /** State TTL in milliseconds. Defaults to 10 minutes. */
3101
+ stateTtlMs?: number;
3102
+ /**
3103
+ * Custom fetch function. Defaults to global fetch.
3104
+ * Useful for testing or custom HTTP clients.
3105
+ */
3106
+ fetch?: typeof globalThis.fetch;
3107
+ }
3108
+ /**
3109
+ * Manages the OAuth 2.0 authorization code flow.
3110
+ *
3111
+ * Supports any standard OAuth 2.0 / OpenID Connect provider.
3112
+ * Pre-built configurations available for Google, GitHub, and Microsoft.
3113
+ *
3114
+ * @example
3115
+ * ```typescript
3116
+ * const oauth = new OAuthManager({
3117
+ * providers: [
3118
+ * googleProvider({ clientId: '...', clientSecret: '...', redirectUri: '...' }),
3119
+ * githubProvider({ clientId: '...', clientSecret: '...', redirectUri: '...' }),
3120
+ * ],
3121
+ * })
3122
+ *
3123
+ * // Step 1: Generate authorization URL
3124
+ * const { url, state } = await oauth.getAuthorizationUrl('google')
3125
+ * // Redirect user to url...
3126
+ *
3127
+ * // Step 2: Handle callback
3128
+ * const { tokens, userInfo } = await oauth.handleCallback('google', code, stateParam)
3129
+ * ```
3130
+ */
3131
+ declare class OAuthManager {
3132
+ private readonly providers;
3133
+ private readonly stateStore;
3134
+ private readonly stateTtlMs;
3135
+ private readonly fetchFn;
3136
+ constructor(config: OAuthManagerConfig);
3137
+ /**
3138
+ * Generate an authorization URL for the user to visit.
3139
+ * Returns the URL and the state parameter for CSRF validation.
3140
+ */
3141
+ getAuthorizationUrl(providerId: string, metadata?: Record<string, unknown>): Promise<{
3142
+ url: string;
3143
+ state: string;
3144
+ }>;
3145
+ /**
3146
+ * Handle the OAuth callback after the user authorizes.
3147
+ * Validates the state parameter, exchanges the code for tokens,
3148
+ * and fetches user info.
3149
+ *
3150
+ * @param providerId - The OAuth provider
3151
+ * @param code - The authorization code from the callback
3152
+ * @param state - The state parameter from the callback
3153
+ * @returns Tokens and user info from the provider
3154
+ */
3155
+ handleCallback(providerId: string, code: string, state: string): Promise<{
3156
+ tokens: OAuthTokens;
3157
+ userInfo: OAuthUserInfo;
3158
+ stateMetadata?: Record<string, unknown>;
3159
+ }>;
3160
+ /**
3161
+ * Get a registered provider by ID.
3162
+ */
3163
+ getProvider(providerId: string): OAuthProviderConfig;
3164
+ /**
3165
+ * List all registered provider IDs.
3166
+ */
3167
+ getProviderIds(): string[];
3168
+ private exchangeCodeForTokens;
3169
+ private fetchUserInfo;
3170
+ }
3171
+ interface ProviderFactoryConfig {
3172
+ clientId: string;
3173
+ clientSecret: string;
3174
+ redirectUri: string;
3175
+ scopes?: string[];
3176
+ }
3177
+ /**
3178
+ * Create a Google OAuth provider configuration.
3179
+ */
3180
+ declare function googleProvider(config: ProviderFactoryConfig): OAuthProviderConfig;
3181
+ /**
3182
+ * Create a GitHub OAuth provider configuration.
3183
+ */
3184
+ declare function githubProvider(config: ProviderFactoryConfig): OAuthProviderConfig;
3185
+ /**
3186
+ * Create a Microsoft OAuth provider configuration.
3187
+ */
3188
+ declare function microsoftProvider(config: ProviderFactoryConfig & {
3189
+ tenantId?: string;
3190
+ }): OAuthProviderConfig;
3191
+
3192
+ /**
3193
+ * A user session with metadata.
3194
+ */
3195
+ interface Session {
3196
+ /** Unique session ID */
3197
+ id: string;
3198
+ /** User ID */
3199
+ userId: string;
3200
+ /** Device ID (if available) */
3201
+ deviceId: string | null;
3202
+ /** IP address of the client (for display, not for security decisions) */
3203
+ ipAddress: string | null;
3204
+ /** User agent string */
3205
+ userAgent: string | null;
3206
+ /** When the session was created */
3207
+ createdAt: number;
3208
+ /** When the session was last active */
3209
+ lastActiveAt: number;
3210
+ /** When the session expires (absolute expiry) */
3211
+ expiresAt: number;
3212
+ /** Whether MFA has been completed for this session */
3213
+ mfaVerified: boolean;
3214
+ /** Custom metadata */
3215
+ metadata?: Record<string, unknown>;
3216
+ }
3217
+ /**
3218
+ * Configuration for the session manager.
3219
+ */
3220
+ interface SessionManagerConfig {
3221
+ /** Session store implementation */
3222
+ store: SessionStore;
3223
+ /** Session TTL in milliseconds. Default: 7 days */
3224
+ sessionTtlMs?: number;
3225
+ /** Idle timeout in milliseconds. Default: 30 minutes */
3226
+ idleTimeoutMs?: number;
3227
+ /** Maximum concurrent sessions per user. Default: 10 */
3228
+ maxSessionsPerUser?: number;
3229
+ /** Whether to extend session on activity. Default: true */
3230
+ slidingWindow?: boolean;
3231
+ }
3232
+ /**
3233
+ * Parameters for creating a new session.
3234
+ */
3235
+ interface CreateSessionParams {
3236
+ userId: string;
3237
+ deviceId?: string;
3238
+ ipAddress?: string;
3239
+ userAgent?: string;
3240
+ mfaVerified?: boolean;
3241
+ metadata?: Record<string, unknown>;
3242
+ }
3243
+ /**
3244
+ * Store for session data.
3245
+ */
3246
+ interface SessionStore {
3247
+ /** Create a new session */
3248
+ create(session: Session): Promise<void>;
3249
+ /** Get a session by ID */
3250
+ getById(sessionId: string): Promise<Session | null>;
3251
+ /** Update a session */
3252
+ update(session: Session): Promise<void>;
3253
+ /** Delete a session */
3254
+ delete(sessionId: string): Promise<void>;
3255
+ /** Get all sessions for a user */
3256
+ listByUserId(userId: string): Promise<Session[]>;
3257
+ /** Delete all sessions for a user */
3258
+ deleteAllForUser(userId: string): Promise<number>;
3259
+ /** Delete all sessions for a user except one */
3260
+ deleteAllExcept(userId: string, keepSessionId: string): Promise<number>;
3261
+ /** Clean up expired sessions */
3262
+ cleanExpired(): Promise<number>;
3263
+ }
3264
+ declare class SessionError extends KoraError {
3265
+ constructor(message: string, code: string, context?: Record<string, unknown>);
3266
+ }
3267
+ declare class SessionNotFoundError extends SessionError {
3268
+ constructor(sessionId: string);
3269
+ }
3270
+ declare class SessionExpiredError extends SessionError {
3271
+ constructor(sessionId: string);
3272
+ }
3273
+ declare class SessionLimitExceededError extends SessionError {
3274
+ constructor(userId: string, limit: number);
3275
+ }
3276
+ declare class SessionMfaRequiredError extends SessionError {
3277
+ constructor(sessionId: string);
3278
+ }
3279
+ /**
3280
+ * In-memory session store for development and testing.
3281
+ */
3282
+ declare class InMemorySessionStore implements SessionStore {
3283
+ private readonly sessions;
3284
+ create(session: Session): Promise<void>;
3285
+ getById(sessionId: string): Promise<Session | null>;
3286
+ update(session: Session): Promise<void>;
3287
+ delete(sessionId: string): Promise<void>;
3288
+ listByUserId(userId: string): Promise<Session[]>;
3289
+ deleteAllForUser(userId: string): Promise<number>;
3290
+ deleteAllExcept(userId: string, keepSessionId: string): Promise<number>;
3291
+ cleanExpired(): Promise<number>;
3292
+ }
3293
+ /**
3294
+ * Manages user sessions with support for:
3295
+ * - Session creation, validation, and revocation
3296
+ * - Sliding window expiry (extends on activity)
3297
+ * - Idle timeout detection
3298
+ * - Max concurrent sessions per user
3299
+ * - MFA verification tracking
3300
+ * - "Sign out everywhere" support
3301
+ *
3302
+ * @example
3303
+ * ```typescript
3304
+ * const sessions = new SessionManager({
3305
+ * store: new InMemorySessionStore(),
3306
+ * sessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7 days
3307
+ * idleTimeoutMs: 30 * 60 * 1000, // 30 minutes
3308
+ * maxSessionsPerUser: 5,
3309
+ * })
3310
+ *
3311
+ * // Create session on login
3312
+ * const session = await sessions.create({
3313
+ * userId: 'user-123',
3314
+ * ipAddress: '192.168.1.1',
3315
+ * userAgent: 'Mozilla/5.0...',
3316
+ * })
3317
+ *
3318
+ * // Validate session on each request
3319
+ * const valid = await sessions.validate(session.id)
3320
+ *
3321
+ * // Touch session to extend idle timeout
3322
+ * await sessions.touch(session.id)
3323
+ * ```
3324
+ */
3325
+ declare class SessionManager {
3326
+ private readonly store;
3327
+ private readonly sessionTtlMs;
3328
+ private readonly idleTimeoutMs;
3329
+ private readonly maxSessionsPerUser;
3330
+ private readonly slidingWindow;
3331
+ constructor(config: SessionManagerConfig);
3332
+ /**
3333
+ * Create a new session for a user.
3334
+ * Enforces the maximum concurrent sessions limit.
3335
+ */
3336
+ create(params: CreateSessionParams): Promise<Session>;
3337
+ /**
3338
+ * Validate a session.
3339
+ * Returns the session if valid, throws if not found or expired.
3340
+ */
3341
+ validate(sessionId: string): Promise<Session>;
3342
+ /**
3343
+ * Touch a session to update its last activity time.
3344
+ * If sliding window is enabled, also extends the absolute expiry.
3345
+ */
3346
+ touch(sessionId: string): Promise<Session>;
3347
+ /**
3348
+ * Mark a session as MFA-verified.
3349
+ */
3350
+ markMfaVerified(sessionId: string): Promise<Session>;
3351
+ /**
3352
+ * Require MFA verification on a session.
3353
+ * Throws SessionMfaRequiredError if MFA is not verified.
3354
+ */
3355
+ requireMfa(sessionId: string): Promise<Session>;
3356
+ /**
3357
+ * Revoke (delete) a session.
3358
+ */
3359
+ revoke(sessionId: string): Promise<void>;
3360
+ /**
3361
+ * Revoke all sessions for a user (sign out everywhere).
3362
+ * Returns the number of sessions revoked.
3363
+ */
3364
+ revokeAll(userId: string): Promise<number>;
3365
+ /**
3366
+ * Revoke all sessions for a user except the current one.
3367
+ * Returns the number of sessions revoked.
3368
+ */
3369
+ revokeOthers(userId: string, currentSessionId: string): Promise<number>;
3370
+ /**
3371
+ * List all active sessions for a user.
3372
+ */
3373
+ listSessions(userId: string): Promise<Session[]>;
3374
+ /**
3375
+ * Clean up expired sessions.
3376
+ * Returns the number of sessions cleaned.
3377
+ */
3378
+ cleanExpired(): Promise<number>;
3379
+ }
3380
+
3381
+ /**
3382
+ * Configuration for TOTP MFA.
3383
+ */
3384
+ interface TotpConfig {
3385
+ /** Issuer name shown in authenticator apps (e.g., "MyApp") */
3386
+ issuer: string;
3387
+ /** Number of digits in the TOTP code. Default: 6 */
3388
+ digits?: number;
3389
+ /** Time step in seconds. Default: 30 */
3390
+ period?: number;
3391
+ /** Hash algorithm. Default: 'SHA-1' (most compatible with authenticator apps) */
3392
+ algorithm?: 'SHA-1' | 'SHA-256' | 'SHA-512';
3393
+ /** Number of time windows to check before/after current. Default: 1 */
3394
+ window?: number;
3395
+ /** Number of recovery codes to generate. Default: 8 */
3396
+ recoveryCodes?: number;
3397
+ }
3398
+ /**
3399
+ * A TOTP secret with metadata for a user.
3400
+ */
3401
+ interface TotpSecret {
3402
+ /** User ID */
3403
+ userId: string;
3404
+ /** Raw secret bytes (base32-encoded for display) */
3405
+ secret: string;
3406
+ /** Whether MFA is verified (user has confirmed setup with a valid code) */
3407
+ verified: boolean;
3408
+ /** Hashed recovery codes (unused ones only) */
3409
+ recoveryCodes: string[];
3410
+ /** When this secret was created */
3411
+ createdAt: number;
3412
+ /** When MFA was verified (confirmed with first valid code) */
3413
+ verifiedAt: number | null;
3414
+ }
3415
+ /**
3416
+ * Setup result returned when enabling TOTP MFA.
3417
+ */
3418
+ interface TotpSetupResult {
3419
+ /** The raw secret in base32 encoding (for manual entry) */
3420
+ secret: string;
3421
+ /** otpauth:// URI for QR code generation */
3422
+ uri: string;
3423
+ /** Plaintext recovery codes (shown once, then discarded) */
3424
+ recoveryCodes: string[];
3425
+ }
3426
+ /**
3427
+ * Store for TOTP secrets.
3428
+ */
3429
+ interface TotpStore {
3430
+ /** Save or update a TOTP secret for a user */
3431
+ save(secret: TotpSecret): Promise<void>;
3432
+ /** Get a TOTP secret by user ID */
3433
+ getByUserId(userId: string): Promise<TotpSecret | null>;
3434
+ /** Delete TOTP secret for a user (disable MFA) */
3435
+ delete(userId: string): Promise<void>;
3436
+ }
3437
+ declare class TotpError extends KoraError {
3438
+ constructor(message: string, code: string, context?: Record<string, unknown>);
3439
+ }
3440
+ declare class TotpInvalidCodeError extends TotpError {
3441
+ constructor();
3442
+ }
3443
+ declare class TotpNotEnabledError extends TotpError {
3444
+ constructor(userId: string);
3445
+ }
3446
+ declare class TotpAlreadyEnabledError extends TotpError {
3447
+ constructor(userId: string);
3448
+ }
3449
+ declare class TotpNotVerifiedError extends TotpError {
3450
+ constructor(userId: string);
3451
+ }
3452
+ declare class TotpRecoveryExhaustedError extends TotpError {
3453
+ constructor();
3454
+ }
3455
+ /**
3456
+ * In-memory TOTP store for development and testing.
3457
+ */
3458
+ declare class InMemoryTotpStore implements TotpStore {
3459
+ private readonly secrets;
3460
+ save(secret: TotpSecret): Promise<void>;
3461
+ getByUserId(userId: string): Promise<TotpSecret | null>;
3462
+ delete(userId: string): Promise<void>;
3463
+ }
3464
+ /**
3465
+ * Manages TOTP-based Multi-Factor Authentication.
3466
+ *
3467
+ * Implements RFC 6238 (TOTP) and RFC 4226 (HOTP) with Web Crypto API.
3468
+ * Compatible with Google Authenticator, Authy, 1Password, and other
3469
+ * TOTP-compatible authenticator apps.
3470
+ *
3471
+ * @example
3472
+ * ```typescript
3473
+ * const totp = new TotpManager({
3474
+ * issuer: 'MyApp',
3475
+ * store: new InMemoryTotpStore(),
3476
+ * })
3477
+ *
3478
+ * // Step 1: Enable MFA (returns QR code URI and recovery codes)
3479
+ * const setup = await totp.enable('user-123', 'alice@example.com')
3480
+ * // Show setup.uri as QR code, show setup.recoveryCodes once
3481
+ *
3482
+ * // Step 2: Verify setup with a code from authenticator app
3483
+ * await totp.verifySetup('user-123', '123456')
3484
+ *
3485
+ * // Step 3: On login, verify TOTP code
3486
+ * const valid = await totp.verify('user-123', '654321')
3487
+ * ```
3488
+ */
3489
+ declare class TotpManager {
3490
+ private readonly store;
3491
+ private readonly issuer;
3492
+ private readonly digits;
3493
+ private readonly period;
3494
+ private readonly algorithm;
3495
+ private readonly window;
3496
+ private readonly recoveryCodeCount;
3497
+ constructor(config: TotpConfig & {
3498
+ store: TotpStore;
3499
+ });
3500
+ /**
3501
+ * Enable TOTP MFA for a user.
3502
+ * Returns the secret URI (for QR code) and recovery codes.
3503
+ * The user must verify setup with a valid code before MFA is active.
3504
+ */
3505
+ enable(userId: string, accountName: string): Promise<TotpSetupResult>;
3506
+ /**
3507
+ * Verify the TOTP setup by confirming the user can generate a valid code.
3508
+ * Must be called after `enable()` and before MFA is enforced.
3509
+ */
3510
+ verifySetup(userId: string, code: string): Promise<boolean>;
3511
+ /**
3512
+ * Verify a TOTP code during login.
3513
+ * Returns true if the code is valid, false otherwise.
3514
+ */
3515
+ verify(userId: string, code: string): Promise<boolean>;
3516
+ /**
3517
+ * Verify a recovery code as an alternative to TOTP.
3518
+ * Recovery codes are single-use.
3519
+ */
3520
+ verifyRecoveryCode(userId: string, recoveryCode: string): Promise<boolean>;
3521
+ /**
3522
+ * Regenerate recovery codes. Requires a valid TOTP code for authorization.
3523
+ * Replaces all existing recovery codes.
3524
+ */
3525
+ regenerateRecoveryCodes(userId: string, totpCode: string): Promise<string[]>;
3526
+ /**
3527
+ * Disable TOTP MFA for a user.
3528
+ * Requires a valid TOTP code or recovery code for authorization.
3529
+ */
3530
+ disable(userId: string, code: string): Promise<void>;
3531
+ /**
3532
+ * Check if a user has TOTP MFA enabled and verified.
3533
+ */
3534
+ isEnabled(userId: string): Promise<boolean>;
3535
+ /**
3536
+ * Get the number of remaining recovery codes for a user.
3537
+ */
3538
+ remainingRecoveryCodes(userId: string): Promise<number>;
3539
+ private validateCode;
3540
+ }
3541
+ /**
3542
+ * Encode bytes to base32 string (RFC 4648, no padding).
3543
+ */
3544
+ declare function base32Encode(data: Uint8Array): string;
3545
+ /**
3546
+ * Decode a base32 string to bytes (RFC 4648).
3547
+ */
3548
+ declare function base32Decode(encoded: string): Uint8Array;
3549
+
3550
+ /**
3551
+ * Actions that can be audited.
3552
+ */
3553
+ declare const AUDIT_ACTIONS: readonly ["user.signup", "user.signin", "user.signout", "user.token_refresh", "user.password_change", "user.password_reset_request", "user.password_reset", "user.email_verify", "mfa.enable", "mfa.verify_setup", "mfa.verify", "mfa.recovery_used", "mfa.recovery_regenerate", "mfa.disable", "session.create", "session.revoke", "session.revoke_all", "session.expired", "oauth.authorize", "oauth.callback", "oauth.link", "oauth.unlink", "user.create", "user.update", "user.delete", "user.suspend", "user.unsuspend", "org.create", "org.update", "org.delete", "org.member_add", "org.member_remove", "org.member_role_change", "org.ownership_transfer", "org.invitation_create", "org.invitation_accept", "org.invitation_revoke", "admin.user_lookup", "admin.impersonate", "admin.config_change"];
3554
+ type AuditAction = (typeof AUDIT_ACTIONS)[number];
3555
+ /**
3556
+ * An audit log entry.
3557
+ */
3558
+ interface AuditEntry {
3559
+ /** Unique entry ID */
3560
+ id: string;
3561
+ /** When this event occurred */
3562
+ timestamp: number;
3563
+ /** The action that was performed */
3564
+ action: AuditAction;
3565
+ /** Who performed the action (user ID, "system", or "anonymous") */
3566
+ actorId: string;
3567
+ /** The type of actor */
3568
+ actorType: 'user' | 'admin' | 'system';
3569
+ /** The target of the action (e.g., user ID, org ID, session ID) */
3570
+ targetId: string | null;
3571
+ /** The type of target */
3572
+ targetType: string | null;
3573
+ /** IP address of the actor */
3574
+ ipAddress: string | null;
3575
+ /** User agent */
3576
+ userAgent: string | null;
3577
+ /** Whether the action succeeded */
3578
+ success: boolean;
3579
+ /** Error message if the action failed */
3580
+ errorMessage: string | null;
3581
+ /** Additional structured context */
3582
+ metadata?: Record<string, unknown>;
3583
+ }
3584
+ /**
3585
+ * Query parameters for searching audit logs.
3586
+ */
3587
+ interface AuditLogQuery {
3588
+ /** Filter by actor ID */
3589
+ actorId?: string;
3590
+ /** Filter by target ID */
3591
+ targetId?: string;
3592
+ /** Filter by action(s) */
3593
+ actions?: AuditAction[];
3594
+ /** Filter by success/failure */
3595
+ success?: boolean;
3596
+ /** Start time (inclusive) */
3597
+ startTime?: number;
3598
+ /** End time (inclusive) */
3599
+ endTime?: number;
3600
+ /** Maximum number of entries to return */
3601
+ limit?: number;
3602
+ /** Offset for pagination */
3603
+ offset?: number;
3604
+ }
3605
+ /**
3606
+ * Store for audit log entries.
3607
+ */
3608
+ interface AuditLogStore {
3609
+ /** Append an audit entry */
3610
+ append(entry: AuditEntry): Promise<void>;
3611
+ /** Query audit entries */
3612
+ query(params: AuditLogQuery): Promise<AuditEntry[]>;
3613
+ /** Count matching entries */
3614
+ count(params: AuditLogQuery): Promise<number>;
3615
+ /** Delete entries older than a given timestamp */
3616
+ purgeOlderThan(timestamp: number): Promise<number>;
3617
+ }
3618
+ declare class AuditLogError extends KoraError {
3619
+ constructor(message: string, code: string, context?: Record<string, unknown>);
3620
+ }
3621
+ /**
3622
+ * In-memory audit log store for development and testing.
3623
+ */
3624
+ declare class InMemoryAuditLogStore implements AuditLogStore {
3625
+ private readonly entries;
3626
+ append(entry: AuditEntry): Promise<void>;
3627
+ query(params: AuditLogQuery): Promise<AuditEntry[]>;
3628
+ count(params: AuditLogQuery): Promise<number>;
3629
+ purgeOlderThan(timestamp: number): Promise<number>;
3630
+ private filterEntries;
3631
+ }
3632
+ /**
3633
+ * Structured audit logger for authentication events.
3634
+ *
3635
+ * Records all security-relevant actions for compliance and debugging.
3636
+ * Designed to be plugged into the auth system at key decision points.
3637
+ *
3638
+ * @example
3639
+ * ```typescript
3640
+ * const auditLog = new AuditLogger({
3641
+ * store: new InMemoryAuditLogStore(),
3642
+ * })
3643
+ *
3644
+ * await auditLog.log({
3645
+ * action: 'user.signin',
3646
+ * actorId: 'user-123',
3647
+ * actorType: 'user',
3648
+ * success: true,
3649
+ * ipAddress: '192.168.1.1',
3650
+ * })
3651
+ *
3652
+ * const entries = await auditLog.query({ actorId: 'user-123', limit: 50 })
3653
+ * ```
3654
+ */
3655
+ declare class AuditLogger {
3656
+ private readonly store;
3657
+ private readonly retentionMs;
3658
+ constructor(config: {
3659
+ store: AuditLogStore;
3660
+ retentionDays?: number;
3661
+ });
3662
+ /**
3663
+ * Log an audit event.
3664
+ */
3665
+ log(params: {
3666
+ action: AuditAction;
3667
+ actorId: string;
3668
+ actorType?: 'user' | 'admin' | 'system';
3669
+ targetId?: string;
3670
+ targetType?: string;
3671
+ ipAddress?: string;
3672
+ userAgent?: string;
3673
+ success?: boolean;
3674
+ errorMessage?: string;
3675
+ metadata?: Record<string, unknown>;
3676
+ }): Promise<AuditEntry>;
3677
+ /**
3678
+ * Query audit log entries.
3679
+ */
3680
+ query(params: AuditLogQuery): Promise<AuditEntry[]>;
3681
+ /**
3682
+ * Count matching audit entries.
3683
+ */
3684
+ count(params: AuditLogQuery): Promise<number>;
3685
+ /**
3686
+ * Purge old entries based on retention policy.
3687
+ * Returns the number of entries purged.
3688
+ */
3689
+ purge(): Promise<number>;
3690
+ /**
3691
+ * Get recent activity for a user.
3692
+ */
3693
+ getUserActivity(userId: string, limit?: number): Promise<AuditEntry[]>;
3694
+ /**
3695
+ * Get failed login attempts for a user within a time window.
3696
+ */
3697
+ getFailedLogins(userId: string, windowMs: number): Promise<AuditEntry[]>;
3698
+ }
3699
+
3700
+ /**
3701
+ * Configuration for the Admin API.
3702
+ */
3703
+ interface AdminApiConfig {
3704
+ /** User store for managing users */
3705
+ userStore: UserStore;
3706
+ /** Session store for managing sessions (optional) */
3707
+ sessionStore?: SessionStore;
3708
+ /** Audit logger (optional) */
3709
+ auditLogger?: AuditLogger;
3710
+ }
3711
+ /**
3712
+ * Paginated result set.
3713
+ */
3714
+ interface PaginatedResult<T> {
3715
+ data: T[];
3716
+ total: number;
3717
+ limit: number;
3718
+ offset: number;
3719
+ }
3720
+ /**
3721
+ * User list query parameters.
3722
+ */
3723
+ interface UserListQuery {
3724
+ /** Search by email (substring match) */
3725
+ email?: string;
3726
+ /** Filter by email verified status */
3727
+ emailVerified?: boolean;
3728
+ /** Maximum number of results */
3729
+ limit?: number;
3730
+ /** Offset for pagination */
3731
+ offset?: number;
3732
+ }
3733
+ /**
3734
+ * Admin-level user update (different from self-update).
3735
+ */
3736
+ interface AdminUserUpdate {
3737
+ /** Update display name */
3738
+ name?: string;
3739
+ /** Set email verified status */
3740
+ emailVerified?: boolean;
3741
+ /** Update email (admin override) */
3742
+ email?: string;
3743
+ }
3744
+ declare class AdminApiError extends KoraError {
3745
+ constructor(message: string, code: string, context?: Record<string, unknown>);
3746
+ }
3747
+ declare class AdminUserNotFoundError extends AdminApiError {
3748
+ constructor(userId: string);
3749
+ }
3750
+ declare class AdminUnauthorizedError extends AdminApiError {
3751
+ constructor();
3752
+ }
3753
+ /**
3754
+ * Administrative API for managing users, sessions, and system configuration.
3755
+ *
3756
+ * Provides elevated operations that should only be accessible to administrators.
3757
+ * All operations are audit-logged when an AuditLogger is configured.
3758
+ *
3759
+ * @example
3760
+ * ```typescript
3761
+ * const admin = new AdminApi({
3762
+ * userStore: myUserStore,
3763
+ * sessionStore: mySessionStore,
3764
+ * auditLogger: myAuditLogger,
3765
+ * })
3766
+ *
3767
+ * // List users
3768
+ * const { data, total } = await admin.listUsers({ limit: 20 })
3769
+ *
3770
+ * // Suspend a user (revokes all sessions)
3771
+ * await admin.suspendUser('admin-user-id', 'target-user-id', 'Policy violation')
3772
+ * ```
3773
+ */
3774
+ declare class AdminApi {
3775
+ private readonly userStore;
3776
+ private readonly sessionStore;
3777
+ private readonly auditLogger;
3778
+ constructor(config: AdminApiConfig);
3779
+ /**
3780
+ * Get a user by ID with full details.
3781
+ */
3782
+ getUser(adminId: string, userId: string): Promise<AuthUser>;
3783
+ /**
3784
+ * List users with optional filtering and pagination.
3785
+ */
3786
+ listUsers(query?: UserListQuery): Promise<PaginatedResult<AuthUser>>;
3787
+ /**
3788
+ * Update a user's profile (admin-level).
3789
+ */
3790
+ updateUser(adminId: string, userId: string, updates: AdminUserUpdate): Promise<AuthUser>;
3791
+ /**
3792
+ * Delete a user and all associated sessions.
3793
+ */
3794
+ deleteUser(adminId: string, userId: string): Promise<void>;
3795
+ /**
3796
+ * Get all active sessions for a user.
3797
+ */
3798
+ getUserSessions(userId: string): Promise<Session[]>;
3799
+ /**
3800
+ * Revoke all sessions for a user.
3801
+ */
3802
+ revokeUserSessions(adminId: string, userId: string): Promise<number>;
3803
+ /**
3804
+ * Revoke a specific session.
3805
+ */
3806
+ revokeSession(adminId: string, sessionId: string): Promise<void>;
3807
+ /**
3808
+ * Get system statistics.
3809
+ */
3810
+ getStats(): Promise<{
3811
+ totalUsers: number;
3812
+ verifiedUsers: number;
3813
+ unverifiedUsers: number;
3814
+ }>;
3815
+ private audit;
3816
+ }
3817
+
3818
+ /**
3819
+ * Events that can trigger webhooks.
3820
+ */
3821
+ declare const WEBHOOK_EVENTS: readonly ["user.created", "user.updated", "user.deleted", "user.signin", "user.signout", "user.password_changed", "user.email_verified", "mfa.enabled", "mfa.disabled", "session.created", "session.revoked", "org.created", "org.updated", "org.deleted", "org.member_added", "org.member_removed", "org.invitation_created", "org.invitation_accepted"];
3822
+ type WebhookEvent = (typeof WEBHOOK_EVENTS)[number];
3823
+ /**
3824
+ * A webhook endpoint configuration.
3825
+ */
3826
+ interface WebhookEndpoint {
3827
+ /** Unique endpoint ID */
3828
+ id: string;
3829
+ /** Destination URL */
3830
+ url: string;
3831
+ /** Events this endpoint subscribes to */
3832
+ events: WebhookEvent[];
3833
+ /** Signing secret for HMAC verification */
3834
+ secret: string;
3835
+ /** Whether this endpoint is active */
3836
+ active: boolean;
3837
+ /** When this endpoint was created */
3838
+ createdAt: number;
3839
+ /** Custom metadata */
3840
+ metadata?: Record<string, unknown>;
3841
+ }
3842
+ /**
3843
+ * A webhook delivery attempt.
3844
+ */
3845
+ interface WebhookDelivery {
3846
+ /** Unique delivery ID */
3847
+ id: string;
3848
+ /** Endpoint ID */
3849
+ endpointId: string;
3850
+ /** Event that triggered this delivery */
3851
+ event: WebhookEvent;
3852
+ /** Request payload (JSON) */
3853
+ payload: string;
3854
+ /** HTTP response status (null if network error) */
3855
+ responseStatus: number | null;
3856
+ /** Whether the delivery was successful (2xx response) */
3857
+ success: boolean;
3858
+ /** Error message if delivery failed */
3859
+ error: string | null;
3860
+ /** Number of attempts made */
3861
+ attempts: number;
3862
+ /** When the delivery was first attempted */
3863
+ createdAt: number;
3864
+ /** When the delivery was last attempted */
3865
+ lastAttemptAt: number;
3866
+ }
3867
+ /**
3868
+ * Payload sent to webhook endpoints.
3869
+ */
3870
+ interface WebhookPayload {
3871
+ /** Unique event ID */
3872
+ id: string;
3873
+ /** Event type */
3874
+ event: WebhookEvent;
3875
+ /** When the event occurred */
3876
+ timestamp: number;
3877
+ /** Event data */
3878
+ data: Record<string, unknown>;
3879
+ }
3880
+ /**
3881
+ * Store for webhook endpoints and deliveries.
3882
+ */
3883
+ interface WebhookStore {
3884
+ /** Save a webhook endpoint */
3885
+ saveEndpoint(endpoint: WebhookEndpoint): Promise<void>;
3886
+ /** Get an endpoint by ID */
3887
+ getEndpoint(id: string): Promise<WebhookEndpoint | null>;
3888
+ /** List all endpoints */
3889
+ listEndpoints(): Promise<WebhookEndpoint[]>;
3890
+ /** Delete an endpoint */
3891
+ deleteEndpoint(id: string): Promise<void>;
3892
+ /** Save a delivery record */
3893
+ saveDelivery(delivery: WebhookDelivery): Promise<void>;
3894
+ /** List recent deliveries for an endpoint */
3895
+ listDeliveries(endpointId: string, limit?: number): Promise<WebhookDelivery[]>;
3896
+ }
3897
+ declare class WebhookError extends KoraError {
3898
+ constructor(message: string, code: string, context?: Record<string, unknown>);
3899
+ }
3900
+ declare class WebhookEndpointNotFoundError extends WebhookError {
3901
+ constructor(id: string);
3902
+ }
3903
+ declare class InMemoryWebhookStore implements WebhookStore {
3904
+ private readonly endpoints;
3905
+ private readonly deliveries;
3906
+ saveEndpoint(endpoint: WebhookEndpoint): Promise<void>;
3907
+ getEndpoint(id: string): Promise<WebhookEndpoint | null>;
3908
+ listEndpoints(): Promise<WebhookEndpoint[]>;
3909
+ deleteEndpoint(id: string): Promise<void>;
3910
+ saveDelivery(delivery: WebhookDelivery): Promise<void>;
3911
+ listDeliveries(endpointId: string, limit?: number): Promise<WebhookDelivery[]>;
3912
+ }
3913
+ /**
3914
+ * Manages webhook registrations and event delivery.
3915
+ *
3916
+ * Sends HTTP POST requests to registered endpoints when auth events occur.
3917
+ * Includes HMAC-SHA256 signature verification and retry logic.
3918
+ *
3919
+ * @example
3920
+ * ```typescript
3921
+ * const webhooks = new WebhookManager({
3922
+ * store: new InMemoryWebhookStore(),
3923
+ * })
3924
+ *
3925
+ * // Register an endpoint
3926
+ * const endpoint = await webhooks.register({
3927
+ * url: 'https://myapp.com/webhooks/auth',
3928
+ * events: ['user.created', 'user.signin'],
3929
+ * })
3930
+ *
3931
+ * // Dispatch an event (usually called internally by auth system)
3932
+ * await webhooks.dispatch('user.created', { userId: 'user-123', email: 'alice@example.com' })
3933
+ * ```
3934
+ */
3935
+ declare class WebhookManager {
3936
+ private readonly store;
3937
+ private readonly fetchFn;
3938
+ constructor(config: {
3939
+ store: WebhookStore;
3940
+ fetch?: typeof globalThis.fetch;
3941
+ });
3942
+ /**
3943
+ * Register a new webhook endpoint.
3944
+ */
3945
+ register(params: {
3946
+ url: string;
3947
+ events: WebhookEvent[];
3948
+ metadata?: Record<string, unknown>;
3949
+ }): Promise<WebhookEndpoint>;
3950
+ /**
3951
+ * Update a webhook endpoint.
3952
+ */
3953
+ update(id: string, updates: {
3954
+ url?: string;
3955
+ events?: WebhookEvent[];
3956
+ active?: boolean;
3957
+ }): Promise<WebhookEndpoint>;
3958
+ /**
3959
+ * Delete a webhook endpoint.
3960
+ */
3961
+ remove(id: string): Promise<void>;
3962
+ /**
3963
+ * List all webhook endpoints.
3964
+ */
3965
+ list(): Promise<WebhookEndpoint[]>;
3966
+ /**
3967
+ * Get a specific endpoint.
3968
+ */
3969
+ get(id: string): Promise<WebhookEndpoint>;
3970
+ /**
3971
+ * Get recent deliveries for an endpoint.
3972
+ */
3973
+ getDeliveries(endpointId: string, limit?: number): Promise<WebhookDelivery[]>;
3974
+ /**
3975
+ * Dispatch an event to all matching webhook endpoints.
3976
+ * Delivery is best-effort with retries.
3977
+ */
3978
+ dispatch(event: WebhookEvent, data: Record<string, unknown>): Promise<void>;
3979
+ private deliverToEndpoint;
3980
+ }
3981
+ /**
3982
+ * Verify a webhook payload signature.
3983
+ * Useful for consumers of webhooks to verify authenticity.
3984
+ */
3985
+ declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
3986
+
3987
+ 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 CreateOrgParams, type CreateSessionParams, DuplicateEmailError, type EmailVerificationConfig, EmailVerificationError, EmailVerificationManager, type EmailVerificationStore, type EmailVerificationToken, ExternalAuthOperationNotSupportedError, ExternalJwtProvider, type ExternalJwtProviderConfig, ExternalTokenValidationError, type ExternalUserInfo, INVITATION_STATUSES, InMemoryAuditLogStore, InMemoryChallengeStore, InMemoryEmailVerificationStore, InMemoryOAuthStateStore, InMemoryOrgStore, InMemoryPasswordResetStore, InMemoryRateLimiter, InMemorySessionStore, InMemoryTokenRevocationStore, InMemoryTotpStore, InMemoryUserStore, InMemoryWebhookStore, InsufficientRoleError, InvalidPermissionError, InvitationExpiredError, InvitationNotFoundError, type InvitationStatus, type LinkedIdentity, MemberAlreadyExistsError, type Membership, MembershipNotFoundError, OAuthCodeExchangeError, OAuthError, OAuthManager, type OAuthManagerConfig, type OAuthProviderConfig, OAuthProviderNotFoundError, 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, 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, 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, createPostgresUserStore, createSqliteUserStore, createSupabaseAdapter, decodeJwt, defineRoles, encodeJwt, generateAuthenticationOptions, generateRegistrationOptions, githubProvider, googleProvider, hasRoleLevel, hashPassword, isExpired, microsoftProvider, parsePermission, permissionCovers, verifyAuthenticationResponse, verifyJwt, verifyPassword, verifyRegistrationResponse, verifyWebhookSignature };