@korajs/auth 0.1.0 → 0.3.0
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/README.md +69 -30
- package/dist/chunk-FSU4SK32.js +786 -0
- package/dist/chunk-FSU4SK32.js.map +1 -0
- package/dist/chunk-HOZXDR6Y.js +52 -0
- package/dist/chunk-HOZXDR6Y.js.map +1 -0
- package/dist/index.cjs +1538 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +708 -5
- package/dist/index.d.ts +708 -5
- package/dist/index.js +926 -3
- package/dist/index.js.map +1 -1
- package/dist/{device-identity-DiwdLsUB.d.cts → operation-encryptor-DRmKNWpF.d.cts} +148 -2
- package/dist/{device-identity-DiwdLsUB.d.ts → operation-encryptor-DRmKNWpF.d.ts} +148 -2
- package/dist/{auth-client-CrDNuh10.d.cts → org-client-BVTLKcIk.d.cts} +177 -3
- package/dist/{auth-client-CrDNuh10.d.ts → org-client-BVTLKcIk.d.ts} +177 -3
- package/dist/password-hash-HDH6VQCQ.js +9 -0
- package/dist/password-hash-HDH6VQCQ.js.map +1 -0
- package/dist/react.cjs +183 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +107 -2
- package/dist/react.d.ts +107 -2
- package/dist/react.js +186 -1
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +4945 -224
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3039 -29
- package/dist/server.d.ts +3039 -29
- package/dist/server.js +4272 -92
- package/dist/server.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-L554ZDPY.js +0 -174
- package/dist/chunk-L554ZDPY.js.map +0 -1
package/dist/server.d.cts
CHANGED
|
@@ -1,32 +1,111 @@
|
|
|
1
|
-
import { A as AuthTokens, T as TokenPayload } from './
|
|
2
|
-
export { f as computePublicKeyThumbprint, v as verifyChallenge } from './
|
|
1
|
+
import { A as AuthTokens, T as TokenPayload } from './operation-encryptor-DRmKNWpF.cjs';
|
|
2
|
+
export { O as OperationEncryptionError, e as OperationEncryptor, f as OperationEncryptorConfig, h as computePublicKeyThumbprint, l as isEncryptedField, v as verifyChallenge } from './operation-encryptor-DRmKNWpF.cjs';
|
|
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
|
-
/**
|
|
10
|
-
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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({
|
|
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
|
-
|
|
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
|
|
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
|
|
113
|
-
* the new access token
|
|
114
|
-
*
|
|
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,7 +294,7 @@ 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(
|
|
297
|
+
constructor();
|
|
175
298
|
}
|
|
176
299
|
/**
|
|
177
300
|
* In-memory user and device store for the built-in auth provider.
|
|
@@ -277,6 +400,33 @@ declare class InMemoryUserStore {
|
|
|
277
400
|
* @param deviceId - The ID of the device to revoke
|
|
278
401
|
*/
|
|
279
402
|
revokeDevice(deviceId: string): Promise<void>;
|
|
403
|
+
/**
|
|
404
|
+
* Set a user's email verification status.
|
|
405
|
+
*
|
|
406
|
+
* @param userId - The user whose email to verify
|
|
407
|
+
* @param verified - Whether the email is verified
|
|
408
|
+
*/
|
|
409
|
+
setEmailVerified(userId: string, verified: boolean): Promise<void>;
|
|
410
|
+
/**
|
|
411
|
+
* Update a user's password hash and salt.
|
|
412
|
+
*
|
|
413
|
+
* @param userId - The user whose password to update
|
|
414
|
+
* @param passwordHash - New hex-encoded PBKDF2 derived key
|
|
415
|
+
* @param salt - New hex-encoded salt
|
|
416
|
+
*/
|
|
417
|
+
updatePassword(userId: string, passwordHash: string, salt: string): Promise<void>;
|
|
418
|
+
/**
|
|
419
|
+
* List all users. For admin/development use.
|
|
420
|
+
*/
|
|
421
|
+
listAll(): Promise<StoredUser[]>;
|
|
422
|
+
/**
|
|
423
|
+
* Update a stored user record.
|
|
424
|
+
*/
|
|
425
|
+
update(user: StoredUser): Promise<void>;
|
|
426
|
+
/**
|
|
427
|
+
* Delete a user and all associated devices.
|
|
428
|
+
*/
|
|
429
|
+
delete(userId: string): Promise<void>;
|
|
280
430
|
/**
|
|
281
431
|
* Update the last-seen timestamp for a device.
|
|
282
432
|
*
|
|
@@ -288,6 +438,85 @@ declare class InMemoryUserStore {
|
|
|
288
438
|
touchDevice(deviceId: string): Promise<void>;
|
|
289
439
|
}
|
|
290
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Interface for server-side challenge storage.
|
|
443
|
+
*
|
|
444
|
+
* Challenges must be stored server-side with expiry and single-use semantics
|
|
445
|
+
* to prevent replay attacks on device verification.
|
|
446
|
+
*/
|
|
447
|
+
interface ChallengeStore {
|
|
448
|
+
/**
|
|
449
|
+
* Store a challenge for later verification.
|
|
450
|
+
* @param challenge - The challenge string
|
|
451
|
+
* @param deviceId - The device this challenge is intended for
|
|
452
|
+
* @param expiresAt - Timestamp (ms since epoch) when this challenge expires
|
|
453
|
+
*/
|
|
454
|
+
store(challenge: string, deviceId: string, expiresAt: number): Promise<void>;
|
|
455
|
+
/**
|
|
456
|
+
* Consume a challenge (single-use). Returns the associated device ID if the
|
|
457
|
+
* challenge is valid and not expired, or null if it doesn't exist, has expired,
|
|
458
|
+
* or was already consumed.
|
|
459
|
+
*/
|
|
460
|
+
consume(challenge: string): Promise<{
|
|
461
|
+
deviceId: string;
|
|
462
|
+
} | null>;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* In-memory challenge store with expiry and single-use semantics.
|
|
466
|
+
* Suitable for development and testing. Use Redis or a database in production.
|
|
467
|
+
*/
|
|
468
|
+
declare class InMemoryChallengeStore implements ChallengeStore {
|
|
469
|
+
private readonly challenges;
|
|
470
|
+
store(challenge: string, deviceId: string, expiresAt: number): Promise<void>;
|
|
471
|
+
consume(challenge: string): Promise<{
|
|
472
|
+
deviceId: string;
|
|
473
|
+
} | null>;
|
|
474
|
+
/**
|
|
475
|
+
* Remove expired challenges to prevent unbounded memory growth.
|
|
476
|
+
*/
|
|
477
|
+
cleanup(): void;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Interface for rate limiting auth endpoints.
|
|
481
|
+
*
|
|
482
|
+
* Rate limiting is critical for preventing brute-force password guessing
|
|
483
|
+
* and credential stuffing attacks.
|
|
484
|
+
*/
|
|
485
|
+
interface RateLimiter {
|
|
486
|
+
/**
|
|
487
|
+
* Check if an action is allowed for the given key.
|
|
488
|
+
* @param key - Rate limit key (e.g., IP address, email, or composite key)
|
|
489
|
+
* @returns true if the action is allowed, false if rate limited
|
|
490
|
+
*/
|
|
491
|
+
isAllowed(key: string): Promise<boolean>;
|
|
492
|
+
/**
|
|
493
|
+
* Record that an action was performed for the given key.
|
|
494
|
+
* Call this after each authentication attempt.
|
|
495
|
+
*/
|
|
496
|
+
record(key: string): Promise<void>;
|
|
497
|
+
/**
|
|
498
|
+
* Reset the rate limit for a key (e.g., after a successful login).
|
|
499
|
+
*/
|
|
500
|
+
reset(key: string): Promise<void>;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* In-memory sliding window rate limiter.
|
|
504
|
+
* Suitable for development and single-server deployments.
|
|
505
|
+
* Use Redis-based rate limiting for multi-server production deployments.
|
|
506
|
+
*/
|
|
507
|
+
declare class InMemoryRateLimiter implements RateLimiter {
|
|
508
|
+
private readonly attempts;
|
|
509
|
+
private readonly maxAttempts;
|
|
510
|
+
private readonly windowMs;
|
|
511
|
+
/**
|
|
512
|
+
* @param maxAttempts - Maximum number of attempts within the time window (default: 10)
|
|
513
|
+
* @param windowMs - Time window in milliseconds (default: 60,000 = 1 minute)
|
|
514
|
+
*/
|
|
515
|
+
constructor(maxAttempts?: number, windowMs?: number);
|
|
516
|
+
isAllowed(key: string): Promise<boolean>;
|
|
517
|
+
record(key: string): Promise<void>;
|
|
518
|
+
reset(key: string): Promise<void>;
|
|
519
|
+
}
|
|
291
520
|
/**
|
|
292
521
|
* Configuration for building the built-in auth routes.
|
|
293
522
|
*/
|
|
@@ -296,6 +525,18 @@ interface AuthRoutesConfig {
|
|
|
296
525
|
userStore: InMemoryUserStore;
|
|
297
526
|
/** The token manager for issuing and validating JWTs */
|
|
298
527
|
tokenManager: TokenManager;
|
|
528
|
+
/**
|
|
529
|
+
* Optional challenge store for device verification.
|
|
530
|
+
* Required for secure device proof-of-possession verification.
|
|
531
|
+
* If not provided, an in-memory store is created automatically.
|
|
532
|
+
*/
|
|
533
|
+
challengeStore?: ChallengeStore;
|
|
534
|
+
/**
|
|
535
|
+
* Optional rate limiter for authentication endpoints.
|
|
536
|
+
* If not provided, an in-memory rate limiter is created with defaults
|
|
537
|
+
* (10 attempts per minute).
|
|
538
|
+
*/
|
|
539
|
+
rateLimiter?: RateLimiter;
|
|
299
540
|
}
|
|
300
541
|
/**
|
|
301
542
|
* Response envelope returned by all auth route handlers.
|
|
@@ -326,11 +567,18 @@ interface AuthRouteResponse<T> {
|
|
|
326
567
|
* - Return `{ status, body: { data } }` on success
|
|
327
568
|
* - Return `{ status, body: { error } }` on failure
|
|
328
569
|
*
|
|
570
|
+
* Security features:
|
|
571
|
+
* - Rate limiting on sign-in/sign-up to prevent brute-force attacks
|
|
572
|
+
* - Server-side challenge store for device verification (single-use, time-limited)
|
|
573
|
+
* - Token revocation on sign-out and device revocation
|
|
574
|
+
* - Input sanitization on all name fields
|
|
575
|
+
* - Maximum password length to prevent hash-DoS
|
|
576
|
+
*
|
|
329
577
|
* @example
|
|
330
578
|
* ```typescript
|
|
331
579
|
* const routes = new BuiltInAuthRoutes({
|
|
332
580
|
* userStore: new InMemoryUserStore(),
|
|
333
|
-
* tokenManager: new TokenManager({ secret:
|
|
581
|
+
* tokenManager: new TokenManager({ secret: TokenManager.generateSecret() }),
|
|
334
582
|
* })
|
|
335
583
|
*
|
|
336
584
|
* // Wire into an HTTP server:
|
|
@@ -343,6 +591,8 @@ interface AuthRouteResponse<T> {
|
|
|
343
591
|
declare class BuiltInAuthRoutes {
|
|
344
592
|
private readonly userStore;
|
|
345
593
|
private readonly tokenManager;
|
|
594
|
+
private readonly challengeStore;
|
|
595
|
+
private readonly rateLimiter;
|
|
346
596
|
constructor(config: AuthRoutesConfig);
|
|
347
597
|
/**
|
|
348
598
|
* Handle user sign-up (POST /auth/signup).
|
|
@@ -352,10 +602,11 @@ declare class BuiltInAuthRoutes {
|
|
|
352
602
|
*
|
|
353
603
|
* @param body - Sign-up request body
|
|
354
604
|
* @param body.email - The user's email address
|
|
355
|
-
* @param body.password - The plaintext password (
|
|
605
|
+
* @param body.password - The plaintext password (8-128 characters)
|
|
356
606
|
* @param body.name - Optional display name (defaults to email local part)
|
|
357
607
|
* @param body.deviceId - Optional device ID to register
|
|
358
608
|
* @param body.devicePublicKey - Optional device public key (base64url)
|
|
609
|
+
* @param clientIp - Optional client IP for rate limiting
|
|
359
610
|
* @returns Auth response with the created user and tokens, or an error
|
|
360
611
|
*/
|
|
361
612
|
handleSignUp(body: {
|
|
@@ -364,7 +615,7 @@ declare class BuiltInAuthRoutes {
|
|
|
364
615
|
name?: string;
|
|
365
616
|
deviceId?: string;
|
|
366
617
|
devicePublicKey?: string;
|
|
367
|
-
}): Promise<AuthRouteResponse<{
|
|
618
|
+
}, clientIp?: string): Promise<AuthRouteResponse<{
|
|
368
619
|
user: AuthUser;
|
|
369
620
|
tokens: AuthTokens;
|
|
370
621
|
}>>;
|
|
@@ -379,6 +630,7 @@ declare class BuiltInAuthRoutes {
|
|
|
379
630
|
* @param body.password - The plaintext password
|
|
380
631
|
* @param body.deviceId - Optional device ID to register
|
|
381
632
|
* @param body.devicePublicKey - Optional device public key (base64url)
|
|
633
|
+
* @param clientIp - Optional client IP for rate limiting
|
|
382
634
|
* @returns Auth response with the user and tokens, or an error
|
|
383
635
|
*/
|
|
384
636
|
handleSignIn(body: {
|
|
@@ -386,7 +638,7 @@ declare class BuiltInAuthRoutes {
|
|
|
386
638
|
password: string;
|
|
387
639
|
deviceId?: string;
|
|
388
640
|
devicePublicKey?: string;
|
|
389
|
-
}): Promise<AuthRouteResponse<{
|
|
641
|
+
}, clientIp?: string): Promise<AuthRouteResponse<{
|
|
390
642
|
user: AuthUser;
|
|
391
643
|
tokens: AuthTokens;
|
|
392
644
|
}>>;
|
|
@@ -394,8 +646,8 @@ declare class BuiltInAuthRoutes {
|
|
|
394
646
|
* Handle token refresh (POST /auth/refresh).
|
|
395
647
|
*
|
|
396
648
|
* Validates the provided refresh token and issues a new token pair
|
|
397
|
-
* (refresh token rotation). The old refresh token
|
|
398
|
-
* consumed
|
|
649
|
+
* (refresh token rotation with reuse detection). The old refresh token
|
|
650
|
+
* is marked as consumed in the revocation store.
|
|
399
651
|
*
|
|
400
652
|
* @param body - Refresh request body
|
|
401
653
|
* @param body.refreshToken - The current refresh token
|
|
@@ -404,6 +656,23 @@ declare class BuiltInAuthRoutes {
|
|
|
404
656
|
handleRefresh(body: {
|
|
405
657
|
refreshToken: string;
|
|
406
658
|
}): Promise<AuthRouteResponse<AuthTokens>>;
|
|
659
|
+
/**
|
|
660
|
+
* Handle sign-out (POST /auth/signout).
|
|
661
|
+
*
|
|
662
|
+
* Validates the access token and revokes the current refresh token
|
|
663
|
+
* (if a revocation store is configured). This ensures that stolen
|
|
664
|
+
* refresh tokens cannot be used after the user signs out.
|
|
665
|
+
*
|
|
666
|
+
* @param accessToken - The JWT access token (without "Bearer " prefix)
|
|
667
|
+
* @param body - Sign-out request body
|
|
668
|
+
* @param body.refreshToken - The current refresh token to revoke
|
|
669
|
+
* @returns Auth response with success flag, or an error
|
|
670
|
+
*/
|
|
671
|
+
handleSignOut(accessToken: string, body: {
|
|
672
|
+
refreshToken?: string;
|
|
673
|
+
}): Promise<AuthRouteResponse<{
|
|
674
|
+
success: boolean;
|
|
675
|
+
}>>;
|
|
407
676
|
/**
|
|
408
677
|
* Handle get-current-user (GET /auth/me).
|
|
409
678
|
*
|
|
@@ -425,8 +694,8 @@ declare class BuiltInAuthRoutes {
|
|
|
425
694
|
/**
|
|
426
695
|
* Handle device revocation (DELETE /auth/device/:id).
|
|
427
696
|
*
|
|
428
|
-
* Validates the access token
|
|
429
|
-
* Only the device's owner can revoke it.
|
|
697
|
+
* Validates the access token, revokes the specified device, and invalidates
|
|
698
|
+
* all tokens issued to that device. Only the device's owner can revoke it.
|
|
430
699
|
*
|
|
431
700
|
* @param accessToken - The JWT access token (without "Bearer " prefix)
|
|
432
701
|
* @param deviceId - The ID of the device to revoke
|
|
@@ -435,6 +704,73 @@ declare class BuiltInAuthRoutes {
|
|
|
435
704
|
handleRevokeDevice(accessToken: string, deviceId: string): Promise<AuthRouteResponse<{
|
|
436
705
|
success: boolean;
|
|
437
706
|
}>>;
|
|
707
|
+
/**
|
|
708
|
+
* Handle device registration (POST /auth/device/register).
|
|
709
|
+
*
|
|
710
|
+
* Requires a valid access token. Registers a new device for the authenticated
|
|
711
|
+
* user and issues a device credential token bound to the device's public key.
|
|
712
|
+
*
|
|
713
|
+
* @param accessToken - The JWT access token (without "Bearer " prefix)
|
|
714
|
+
* @param body - Device registration request body
|
|
715
|
+
* @param body.deviceId - Unique identifier for the device
|
|
716
|
+
* @param body.publicKey - The device's public key as a JWK JSON string
|
|
717
|
+
* @param body.name - Human-readable device name (e.g., "Chrome on MacBook")
|
|
718
|
+
* @returns Auth response with the registered device and device credential, or an error
|
|
719
|
+
*/
|
|
720
|
+
handleDeviceRegister(accessToken: string, body: {
|
|
721
|
+
deviceId: string;
|
|
722
|
+
publicKey: string;
|
|
723
|
+
name: string;
|
|
724
|
+
}): Promise<AuthRouteResponse<{
|
|
725
|
+
device: AuthDevice;
|
|
726
|
+
deviceCredential: string;
|
|
727
|
+
}>>;
|
|
728
|
+
/**
|
|
729
|
+
* Generate a challenge for device proof-of-possession verification.
|
|
730
|
+
*
|
|
731
|
+
* Creates a cryptographically random challenge, stores it server-side with
|
|
732
|
+
* a 60-second TTL and the target device ID, and returns the challenge string.
|
|
733
|
+
* The client signs this challenge with its private key and submits it via
|
|
734
|
+
* {@link handleDeviceVerify}.
|
|
735
|
+
*
|
|
736
|
+
* @param accessToken - The JWT access token (without "Bearer " prefix)
|
|
737
|
+
* @param deviceId - The device this challenge is intended for
|
|
738
|
+
* @returns Auth response with the challenge string, or an error
|
|
739
|
+
*/
|
|
740
|
+
handleDeviceChallenge(accessToken: string, deviceId: string): Promise<AuthRouteResponse<{
|
|
741
|
+
challenge: string;
|
|
742
|
+
}>>;
|
|
743
|
+
/**
|
|
744
|
+
* Handle device proof-of-possession verification (POST /auth/device/verify).
|
|
745
|
+
*
|
|
746
|
+
* Verifies that the device holds the private key corresponding to its registered
|
|
747
|
+
* public key by checking a signed challenge. The challenge must have been previously
|
|
748
|
+
* issued via {@link handleDeviceChallenge} and is single-use.
|
|
749
|
+
*
|
|
750
|
+
* On success, issues fresh tokens for the device.
|
|
751
|
+
*
|
|
752
|
+
* @param body - Device verification request body
|
|
753
|
+
* @param body.deviceId - The ID of the device to verify
|
|
754
|
+
* @param body.challenge - The challenge string (from handleDeviceChallenge)
|
|
755
|
+
* @param body.signature - The base64url-encoded ECDSA signature of the challenge
|
|
756
|
+
* @returns Auth response with fresh tokens on success, or an error
|
|
757
|
+
*/
|
|
758
|
+
handleDeviceVerify(body: {
|
|
759
|
+
deviceId: string;
|
|
760
|
+
challenge: string;
|
|
761
|
+
signature: string;
|
|
762
|
+
}): Promise<AuthRouteResponse<{
|
|
763
|
+
tokens: AuthTokens;
|
|
764
|
+
}>>;
|
|
765
|
+
/**
|
|
766
|
+
* Generates a random challenge string for proof-of-possession verification.
|
|
767
|
+
*
|
|
768
|
+
* **Deprecated:** Use {@link handleDeviceChallenge} instead, which stores
|
|
769
|
+
* the challenge server-side with expiry and single-use semantics.
|
|
770
|
+
*
|
|
771
|
+
* @returns A 64-character hex string (32 random bytes)
|
|
772
|
+
*/
|
|
773
|
+
static generateChallenge(): string;
|
|
438
774
|
/**
|
|
439
775
|
* Creates a sync server auth provider compatible with `@korajs/server`.
|
|
440
776
|
*
|
|
@@ -443,6 +779,9 @@ declare class BuiltInAuthRoutes {
|
|
|
443
779
|
* context containing the user ID and device metadata. This bridges
|
|
444
780
|
* the built-in auth system with the sync server's authentication layer.
|
|
445
781
|
*
|
|
782
|
+
* Also checks device revocation status during authentication, ensuring
|
|
783
|
+
* that revoked devices are rejected even if their tokens haven't expired.
|
|
784
|
+
*
|
|
446
785
|
* @returns An object with an `authenticate` method suitable for KoraSyncServer's `auth` config
|
|
447
786
|
*
|
|
448
787
|
* @example
|
|
@@ -531,8 +870,9 @@ declare function verifyJwt(token: string, secret: string): Record<string, unknow
|
|
|
531
870
|
* Checks whether a token payload has expired based on its `exp` claim.
|
|
532
871
|
*
|
|
533
872
|
* The `exp` claim is expected to be in seconds since the Unix epoch (per JWT spec).
|
|
534
|
-
*
|
|
535
|
-
*
|
|
873
|
+
* Includes a small clock skew tolerance (5 seconds) to handle minor time
|
|
874
|
+
* differences between servers. If the `exp` claim is missing or is not a number,
|
|
875
|
+
* the token is considered non-expiring and this function returns false.
|
|
536
876
|
*
|
|
537
877
|
* @param payload - An object with an optional `exp` field (seconds since epoch)
|
|
538
878
|
* @returns true if the token has expired, false otherwise
|
|
@@ -596,6 +936,290 @@ declare function hashPassword(password: string): Promise<HashResult>;
|
|
|
596
936
|
*/
|
|
597
937
|
declare function verifyPassword(password: string, hash: string, salt: string): Promise<boolean>;
|
|
598
938
|
|
|
939
|
+
/**
|
|
940
|
+
* A pending password reset request.
|
|
941
|
+
*/
|
|
942
|
+
interface PasswordResetToken {
|
|
943
|
+
/** Cryptographically random single-use token */
|
|
944
|
+
token: string;
|
|
945
|
+
/** User ID the token was generated for */
|
|
946
|
+
userId: string;
|
|
947
|
+
/** Email the reset was requested for */
|
|
948
|
+
email: string;
|
|
949
|
+
/** When the token was created (ms since epoch) */
|
|
950
|
+
createdAt: number;
|
|
951
|
+
/** When the token expires (ms since epoch) */
|
|
952
|
+
expiresAt: number;
|
|
953
|
+
/** Whether the token has been consumed */
|
|
954
|
+
consumed: boolean;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Persistence interface for password reset tokens.
|
|
958
|
+
*/
|
|
959
|
+
interface PasswordResetStore {
|
|
960
|
+
/** Store a reset token. */
|
|
961
|
+
store(token: PasswordResetToken): Promise<void>;
|
|
962
|
+
/** Look up a token. Returns null if not found. */
|
|
963
|
+
get(token: string): Promise<PasswordResetToken | null>;
|
|
964
|
+
/** Mark a token as consumed. */
|
|
965
|
+
consume(token: string): Promise<void>;
|
|
966
|
+
/** Count active (non-consumed, non-expired) tokens for an email. */
|
|
967
|
+
countActiveForEmail(email: string): Promise<number>;
|
|
968
|
+
/** Remove expired tokens. */
|
|
969
|
+
cleanExpired(): Promise<number>;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Configuration for the password reset flow.
|
|
973
|
+
*/
|
|
974
|
+
interface PasswordResetConfig {
|
|
975
|
+
/** User store for looking up users and updating passwords */
|
|
976
|
+
userStore: InMemoryUserStore;
|
|
977
|
+
/** Store for reset tokens. Defaults to InMemoryPasswordResetStore. */
|
|
978
|
+
resetStore?: PasswordResetStore;
|
|
979
|
+
/** Token TTL in milliseconds. Defaults to 1 hour. */
|
|
980
|
+
tokenTtlMs?: number;
|
|
981
|
+
/** Max reset requests per email in the TTL window. Defaults to 3. */
|
|
982
|
+
maxRequestsPerEmail?: number;
|
|
983
|
+
/**
|
|
984
|
+
* Callback invoked when a reset is requested.
|
|
985
|
+
* The developer must implement email sending.
|
|
986
|
+
* If not provided, the token is returned in the route response (development mode).
|
|
987
|
+
*/
|
|
988
|
+
onResetRequested?: (email: string, token: string, expiresAt: number) => void | Promise<void>;
|
|
989
|
+
}
|
|
990
|
+
declare class PasswordResetError extends KoraError {
|
|
991
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
992
|
+
}
|
|
993
|
+
declare class ResetTokenExpiredError extends PasswordResetError {
|
|
994
|
+
constructor();
|
|
995
|
+
}
|
|
996
|
+
declare class ResetTokenNotFoundError extends PasswordResetError {
|
|
997
|
+
constructor();
|
|
998
|
+
}
|
|
999
|
+
declare class ResetRateLimitedError extends PasswordResetError {
|
|
1000
|
+
constructor();
|
|
1001
|
+
}
|
|
1002
|
+
declare class InMemoryPasswordResetStore implements PasswordResetStore {
|
|
1003
|
+
private tokens;
|
|
1004
|
+
store(token: PasswordResetToken): Promise<void>;
|
|
1005
|
+
get(token: string): Promise<PasswordResetToken | null>;
|
|
1006
|
+
consume(token: string): Promise<void>;
|
|
1007
|
+
countActiveForEmail(email: string): Promise<number>;
|
|
1008
|
+
cleanExpired(): Promise<number>;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Manages the password reset flow.
|
|
1012
|
+
*
|
|
1013
|
+
* @example
|
|
1014
|
+
* ```typescript
|
|
1015
|
+
* const resetManager = new PasswordResetManager({
|
|
1016
|
+
* userStore,
|
|
1017
|
+
* onResetRequested: async (email, token, expiresAt) => {
|
|
1018
|
+
* await sendEmail(email, `Reset link: https://app.com/reset?token=${token}`)
|
|
1019
|
+
* },
|
|
1020
|
+
* })
|
|
1021
|
+
*
|
|
1022
|
+
* // Request reset (always returns 200 to prevent email enumeration)
|
|
1023
|
+
* const response = await resetManager.requestReset('user@example.com')
|
|
1024
|
+
*
|
|
1025
|
+
* // Consume token and set new password
|
|
1026
|
+
* const response = await resetManager.resetPassword(token, 'newPassword123')
|
|
1027
|
+
* ```
|
|
1028
|
+
*/
|
|
1029
|
+
declare class PasswordResetManager {
|
|
1030
|
+
private readonly userStore;
|
|
1031
|
+
private readonly resetStore;
|
|
1032
|
+
private readonly tokenTtlMs;
|
|
1033
|
+
private readonly maxRequestsPerEmail;
|
|
1034
|
+
private readonly onResetRequested?;
|
|
1035
|
+
constructor(config: PasswordResetConfig);
|
|
1036
|
+
/**
|
|
1037
|
+
* Request a password reset for an email.
|
|
1038
|
+
* Always returns success to prevent email enumeration.
|
|
1039
|
+
*
|
|
1040
|
+
* If a callback is configured, invokes it with the token.
|
|
1041
|
+
* In development mode (no callback), returns the token in the response.
|
|
1042
|
+
*/
|
|
1043
|
+
requestReset(email: string): Promise<{
|
|
1044
|
+
status: number;
|
|
1045
|
+
body: {
|
|
1046
|
+
data: {
|
|
1047
|
+
message: string;
|
|
1048
|
+
token?: string;
|
|
1049
|
+
};
|
|
1050
|
+
} | {
|
|
1051
|
+
error: string;
|
|
1052
|
+
};
|
|
1053
|
+
}>;
|
|
1054
|
+
/**
|
|
1055
|
+
* Consume a reset token and set a new password.
|
|
1056
|
+
*/
|
|
1057
|
+
resetPassword(token: string, newPassword: string): Promise<{
|
|
1058
|
+
status: number;
|
|
1059
|
+
body: {
|
|
1060
|
+
data: {
|
|
1061
|
+
message: string;
|
|
1062
|
+
};
|
|
1063
|
+
} | {
|
|
1064
|
+
error: string;
|
|
1065
|
+
};
|
|
1066
|
+
}>;
|
|
1067
|
+
/**
|
|
1068
|
+
* Change password for an authenticated user (requires current password verification).
|
|
1069
|
+
*/
|
|
1070
|
+
changePassword(userId: string, currentPassword: string, newPassword: string): Promise<{
|
|
1071
|
+
status: number;
|
|
1072
|
+
body: {
|
|
1073
|
+
data: {
|
|
1074
|
+
message: string;
|
|
1075
|
+
};
|
|
1076
|
+
} | {
|
|
1077
|
+
error: string;
|
|
1078
|
+
};
|
|
1079
|
+
}>;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* A pending email verification token.
|
|
1084
|
+
*/
|
|
1085
|
+
interface EmailVerificationToken {
|
|
1086
|
+
/** Cryptographically random single-use token */
|
|
1087
|
+
token: string;
|
|
1088
|
+
/** User ID the token was generated for */
|
|
1089
|
+
userId: string;
|
|
1090
|
+
/** Email being verified */
|
|
1091
|
+
email: string;
|
|
1092
|
+
/** When the token was created (ms since epoch) */
|
|
1093
|
+
createdAt: number;
|
|
1094
|
+
/** When the token expires (ms since epoch) */
|
|
1095
|
+
expiresAt: number;
|
|
1096
|
+
/** Whether the token has been consumed */
|
|
1097
|
+
consumed: boolean;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Persistence interface for email verification tokens.
|
|
1101
|
+
*/
|
|
1102
|
+
interface EmailVerificationStore {
|
|
1103
|
+
/** Store a verification token. */
|
|
1104
|
+
store(token: EmailVerificationToken): Promise<void>;
|
|
1105
|
+
/** Look up a token. Returns null if not found. */
|
|
1106
|
+
get(token: string): Promise<EmailVerificationToken | null>;
|
|
1107
|
+
/** Mark a token as consumed. */
|
|
1108
|
+
consume(token: string): Promise<void>;
|
|
1109
|
+
/** Count active (non-consumed, non-expired) tokens for a user. */
|
|
1110
|
+
countActiveForUser(userId: string): Promise<number>;
|
|
1111
|
+
/** Remove expired tokens. */
|
|
1112
|
+
cleanExpired(): Promise<number>;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Configuration for the email verification flow.
|
|
1116
|
+
*/
|
|
1117
|
+
interface EmailVerificationConfig {
|
|
1118
|
+
/** User store for looking up users */
|
|
1119
|
+
userStore: InMemoryUserStore;
|
|
1120
|
+
/** Store for verification tokens. Defaults to InMemoryEmailVerificationStore. */
|
|
1121
|
+
verificationStore?: EmailVerificationStore;
|
|
1122
|
+
/** Token TTL in milliseconds. Defaults to 24 hours. */
|
|
1123
|
+
tokenTtlMs?: number;
|
|
1124
|
+
/** Max verification requests per user per TTL window. Defaults to 3. */
|
|
1125
|
+
maxRequestsPerUser?: number;
|
|
1126
|
+
/**
|
|
1127
|
+
* Callback invoked when a verification email should be sent.
|
|
1128
|
+
* The developer must implement email sending.
|
|
1129
|
+
* If not provided, the token is returned in the route response (development mode).
|
|
1130
|
+
*/
|
|
1131
|
+
onVerificationRequired?: (email: string, token: string, expiresAt: number) => void | Promise<void>;
|
|
1132
|
+
}
|
|
1133
|
+
declare class EmailVerificationError extends KoraError {
|
|
1134
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
1135
|
+
}
|
|
1136
|
+
declare class VerificationTokenExpiredError extends EmailVerificationError {
|
|
1137
|
+
constructor();
|
|
1138
|
+
}
|
|
1139
|
+
declare class VerificationTokenNotFoundError extends EmailVerificationError {
|
|
1140
|
+
constructor();
|
|
1141
|
+
}
|
|
1142
|
+
declare class InMemoryEmailVerificationStore implements EmailVerificationStore {
|
|
1143
|
+
private tokens;
|
|
1144
|
+
store(token: EmailVerificationToken): Promise<void>;
|
|
1145
|
+
get(token: string): Promise<EmailVerificationToken | null>;
|
|
1146
|
+
consume(token: string): Promise<void>;
|
|
1147
|
+
countActiveForUser(userId: string): Promise<number>;
|
|
1148
|
+
cleanExpired(): Promise<number>;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Manages the email verification flow.
|
|
1152
|
+
*
|
|
1153
|
+
* @example
|
|
1154
|
+
* ```typescript
|
|
1155
|
+
* const verifier = new EmailVerificationManager({
|
|
1156
|
+
* userStore,
|
|
1157
|
+
* onVerificationRequired: async (email, token, expiresAt) => {
|
|
1158
|
+
* await sendEmail(email, `Verify: https://app.com/verify?token=${token}`)
|
|
1159
|
+
* },
|
|
1160
|
+
* })
|
|
1161
|
+
*
|
|
1162
|
+
* // Send verification email
|
|
1163
|
+
* await verifier.sendVerification('user-1', 'user@example.com')
|
|
1164
|
+
*
|
|
1165
|
+
* // Verify email
|
|
1166
|
+
* await verifier.verifyEmail(token)
|
|
1167
|
+
* ```
|
|
1168
|
+
*/
|
|
1169
|
+
declare class EmailVerificationManager {
|
|
1170
|
+
private readonly userStore;
|
|
1171
|
+
private readonly verificationStore;
|
|
1172
|
+
private readonly tokenTtlMs;
|
|
1173
|
+
private readonly maxRequestsPerUser;
|
|
1174
|
+
private readonly onVerificationRequired?;
|
|
1175
|
+
constructor(config: EmailVerificationConfig);
|
|
1176
|
+
/**
|
|
1177
|
+
* Send a verification email for a user.
|
|
1178
|
+
* Rate-limited to prevent abuse.
|
|
1179
|
+
*/
|
|
1180
|
+
sendVerification(userId: string, email: string): Promise<{
|
|
1181
|
+
status: number;
|
|
1182
|
+
body: {
|
|
1183
|
+
data: {
|
|
1184
|
+
message: string;
|
|
1185
|
+
token?: string;
|
|
1186
|
+
};
|
|
1187
|
+
} | {
|
|
1188
|
+
error: string;
|
|
1189
|
+
};
|
|
1190
|
+
}>;
|
|
1191
|
+
/**
|
|
1192
|
+
* Verify an email using a verification token.
|
|
1193
|
+
*/
|
|
1194
|
+
verifyEmail(token: string): Promise<{
|
|
1195
|
+
status: number;
|
|
1196
|
+
body: {
|
|
1197
|
+
data: {
|
|
1198
|
+
message: string;
|
|
1199
|
+
userId: string;
|
|
1200
|
+
email: string;
|
|
1201
|
+
};
|
|
1202
|
+
} | {
|
|
1203
|
+
error: string;
|
|
1204
|
+
};
|
|
1205
|
+
}>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Resend verification email for a user.
|
|
1208
|
+
* Delegates to sendVerification with rate limiting.
|
|
1209
|
+
*/
|
|
1210
|
+
resendVerification(userId: string): Promise<{
|
|
1211
|
+
status: number;
|
|
1212
|
+
body: {
|
|
1213
|
+
data: {
|
|
1214
|
+
message: string;
|
|
1215
|
+
token?: string;
|
|
1216
|
+
};
|
|
1217
|
+
} | {
|
|
1218
|
+
error: string;
|
|
1219
|
+
};
|
|
1220
|
+
}>;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
599
1223
|
/**
|
|
600
1224
|
* Parameters for signing up a new user.
|
|
601
1225
|
*/
|
|
@@ -777,4 +1401,2390 @@ declare class BuiltInProvider implements AuthProviderAdapter {
|
|
|
777
1401
|
listDevices(accessToken: string): Promise<AuthDevice[]>;
|
|
778
1402
|
}
|
|
779
1403
|
|
|
780
|
-
|
|
1404
|
+
/**
|
|
1405
|
+
* Thrown when an operation is not supported by external auth providers.
|
|
1406
|
+
*
|
|
1407
|
+
* External providers delegate user management to the third-party service
|
|
1408
|
+
* (Clerk, Auth0, Supabase, etc.). Operations like sign-up and sign-in must
|
|
1409
|
+
* be performed through the external provider's SDK or UI, not through Kora.
|
|
1410
|
+
*/
|
|
1411
|
+
declare class ExternalAuthOperationNotSupportedError extends KoraError {
|
|
1412
|
+
constructor(operation: string, provider: string);
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Thrown when an external JWT token fails validation.
|
|
1416
|
+
*/
|
|
1417
|
+
declare class ExternalTokenValidationError extends KoraError {
|
|
1418
|
+
constructor(reason: string, context?: Record<string, unknown>);
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* User information extracted from an external JWT token.
|
|
1422
|
+
* Returned by the claims mapping function.
|
|
1423
|
+
*/
|
|
1424
|
+
interface ExternalUserInfo {
|
|
1425
|
+
/** Unique user identifier from the external provider */
|
|
1426
|
+
userId: string;
|
|
1427
|
+
/** User's email address, if available in the token claims */
|
|
1428
|
+
email?: string;
|
|
1429
|
+
/** User's display name, if available in the token claims */
|
|
1430
|
+
name?: string;
|
|
1431
|
+
/** Additional metadata from the token claims */
|
|
1432
|
+
metadata?: Record<string, unknown>;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Configuration for the external JWT authentication provider.
|
|
1436
|
+
*
|
|
1437
|
+
* Supports two validation modes:
|
|
1438
|
+
* 1. **HMAC secret** (`jwtSecret`): For providers that sign tokens with a shared
|
|
1439
|
+
* secret (e.g., Supabase). Uses HS256 verification via the existing `verifyJwt` utility.
|
|
1440
|
+
* 2. **Custom validator** (`validateToken`): For providers that use asymmetric keys
|
|
1441
|
+
* (RS256, ES256) or require custom validation logic (e.g., Clerk with JWKS rotation).
|
|
1442
|
+
* The developer provides their own validation function.
|
|
1443
|
+
*
|
|
1444
|
+
* At least one of `jwtSecret` or `validateToken` must be provided.
|
|
1445
|
+
*
|
|
1446
|
+
* @example
|
|
1447
|
+
* ```typescript
|
|
1448
|
+
* // With a shared secret (e.g., Supabase)
|
|
1449
|
+
* const provider = new ExternalJwtProvider({
|
|
1450
|
+
* providerName: 'supabase',
|
|
1451
|
+
* jwtSecret: process.env.SUPABASE_JWT_SECRET,
|
|
1452
|
+
* })
|
|
1453
|
+
*
|
|
1454
|
+
* // With a custom validator (e.g., Clerk JWKS)
|
|
1455
|
+
* const provider = new ExternalJwtProvider({
|
|
1456
|
+
* providerName: 'clerk',
|
|
1457
|
+
* validateToken: async (token) => {
|
|
1458
|
+
* const claims = await clerkClient.verifyToken(token)
|
|
1459
|
+
* return claims ? { sub: claims.sub, ...claims } : null
|
|
1460
|
+
* },
|
|
1461
|
+
* })
|
|
1462
|
+
* ```
|
|
1463
|
+
*/
|
|
1464
|
+
interface ExternalJwtProviderConfig {
|
|
1465
|
+
/**
|
|
1466
|
+
* Human-readable name of the external auth provider.
|
|
1467
|
+
* Used in error messages and DevTools for identification.
|
|
1468
|
+
* @example 'clerk', 'auth0', 'supabase', 'firebase'
|
|
1469
|
+
*/
|
|
1470
|
+
providerName: string;
|
|
1471
|
+
/**
|
|
1472
|
+
* Shared secret for HS256 JWT verification.
|
|
1473
|
+
* Used when the external provider signs tokens with HMAC-SHA256.
|
|
1474
|
+
* Mutually exclusive with `validateToken` (if both are provided,
|
|
1475
|
+
* `validateToken` takes precedence).
|
|
1476
|
+
*/
|
|
1477
|
+
jwtSecret?: string;
|
|
1478
|
+
/**
|
|
1479
|
+
* Custom token validator function.
|
|
1480
|
+
* When provided, this is used instead of HS256 HMAC verification.
|
|
1481
|
+
* Receives the raw JWT string and must return the decoded claims
|
|
1482
|
+
* (with at least a `sub` field) or null if the token is invalid.
|
|
1483
|
+
*
|
|
1484
|
+
* Use this for providers that use asymmetric signing (RS256, ES256)
|
|
1485
|
+
* or require JWKS-based key rotation.
|
|
1486
|
+
*
|
|
1487
|
+
* @param token - The raw JWT string to validate
|
|
1488
|
+
* @returns The decoded claims object with at least a `sub` field, or null if invalid
|
|
1489
|
+
*/
|
|
1490
|
+
validateToken?: (token: string) => Promise<{
|
|
1491
|
+
sub: string;
|
|
1492
|
+
[key: string]: unknown;
|
|
1493
|
+
} | null>;
|
|
1494
|
+
/**
|
|
1495
|
+
* Map external JWT claims to Kora's expected user format.
|
|
1496
|
+
*
|
|
1497
|
+
* The default mapping extracts:
|
|
1498
|
+
* - `sub` -> `userId` (required)
|
|
1499
|
+
* - `email` -> `email` (optional)
|
|
1500
|
+
* - `name` -> `name` (optional)
|
|
1501
|
+
*
|
|
1502
|
+
* Override this to extract custom claims from your provider's tokens.
|
|
1503
|
+
*
|
|
1504
|
+
* @param claims - The decoded JWT claims object
|
|
1505
|
+
* @returns Kora-compatible user information
|
|
1506
|
+
*
|
|
1507
|
+
* @example
|
|
1508
|
+
* ```typescript
|
|
1509
|
+
* mapClaims: (claims) => ({
|
|
1510
|
+
* userId: claims.sub as string,
|
|
1511
|
+
* email: claims.email_address as string,
|
|
1512
|
+
* name: `${claims.first_name} ${claims.last_name}`,
|
|
1513
|
+
* metadata: { org: claims.org_id },
|
|
1514
|
+
* })
|
|
1515
|
+
* ```
|
|
1516
|
+
*/
|
|
1517
|
+
mapClaims?: (claims: Record<string, unknown>) => ExternalUserInfo;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Authentication provider adapter for external JWT issuers.
|
|
1521
|
+
*
|
|
1522
|
+
* This adapter validates JWTs issued by third-party auth services (Clerk, Auth0,
|
|
1523
|
+
* Supabase, Firebase, or any custom JWT issuer) and maps their claims to Kora's
|
|
1524
|
+
* internal auth context. It bridges external identity providers with Kora's
|
|
1525
|
+
* sync authentication layer.
|
|
1526
|
+
*
|
|
1527
|
+
* **How it works:**
|
|
1528
|
+
* 1. The client authenticates with the external provider and receives a JWT
|
|
1529
|
+
* 2. The client passes this JWT to Kora's sync server
|
|
1530
|
+
* 3. This adapter validates the JWT and extracts user identity
|
|
1531
|
+
* 4. Kora uses the extracted identity for sync authorization
|
|
1532
|
+
*
|
|
1533
|
+
* **What it does NOT do:**
|
|
1534
|
+
* - Sign up or sign in users (that happens through the external provider)
|
|
1535
|
+
* - Issue or refresh tokens (that is the external provider's responsibility)
|
|
1536
|
+
* - Manage devices (external providers handle their own device tracking)
|
|
1537
|
+
*
|
|
1538
|
+
* @example
|
|
1539
|
+
* ```typescript
|
|
1540
|
+
* import { ExternalJwtProvider } from '@korajs/auth/server'
|
|
1541
|
+
*
|
|
1542
|
+
* const auth = new ExternalJwtProvider({
|
|
1543
|
+
* providerName: 'clerk',
|
|
1544
|
+
* validateToken: async (token) => {
|
|
1545
|
+
* // Use Clerk's SDK or JWKS endpoint to verify
|
|
1546
|
+
* return verifiedClaims
|
|
1547
|
+
* },
|
|
1548
|
+
* })
|
|
1549
|
+
*
|
|
1550
|
+
* // Use with Kora sync server
|
|
1551
|
+
* const result = await auth.validateAccessToken('eyJhbG...')
|
|
1552
|
+
* if (result) {
|
|
1553
|
+
* console.log('User:', result.userId)
|
|
1554
|
+
* }
|
|
1555
|
+
* ```
|
|
1556
|
+
*/
|
|
1557
|
+
declare class ExternalJwtProvider implements AuthProviderAdapter {
|
|
1558
|
+
private readonly providerName;
|
|
1559
|
+
private readonly jwtSecret;
|
|
1560
|
+
private readonly customValidateToken;
|
|
1561
|
+
private readonly mapClaims;
|
|
1562
|
+
constructor(config: ExternalJwtProviderConfig);
|
|
1563
|
+
/**
|
|
1564
|
+
* Not supported for external providers.
|
|
1565
|
+
*
|
|
1566
|
+
* User registration must be performed through the external auth provider's
|
|
1567
|
+
* SDK or UI. Kora does not manage user accounts for external providers.
|
|
1568
|
+
*
|
|
1569
|
+
* @throws {ExternalAuthOperationNotSupportedError} Always
|
|
1570
|
+
*/
|
|
1571
|
+
signUp(_params: SignUpParams): Promise<{
|
|
1572
|
+
user: AuthUser;
|
|
1573
|
+
tokens: AuthTokens;
|
|
1574
|
+
}>;
|
|
1575
|
+
/**
|
|
1576
|
+
* Not supported for external providers.
|
|
1577
|
+
*
|
|
1578
|
+
* User authentication must be performed through the external auth provider's
|
|
1579
|
+
* SDK or UI. Kora does not manage credentials for external providers.
|
|
1580
|
+
*
|
|
1581
|
+
* @throws {ExternalAuthOperationNotSupportedError} Always
|
|
1582
|
+
*/
|
|
1583
|
+
signIn(_params: SignInParams): Promise<{
|
|
1584
|
+
user: AuthUser;
|
|
1585
|
+
tokens: AuthTokens;
|
|
1586
|
+
}>;
|
|
1587
|
+
/**
|
|
1588
|
+
* Not supported for external providers.
|
|
1589
|
+
*
|
|
1590
|
+
* Token refresh must be performed through the external auth provider's SDK.
|
|
1591
|
+
* Kora does not manage token lifecycle for external providers.
|
|
1592
|
+
*
|
|
1593
|
+
* @throws {ExternalAuthOperationNotSupportedError} Always
|
|
1594
|
+
*/
|
|
1595
|
+
refreshTokens(_refreshToken: string): Promise<AuthTokens>;
|
|
1596
|
+
/**
|
|
1597
|
+
* Validate an access token from the external auth provider.
|
|
1598
|
+
*
|
|
1599
|
+
* Uses either the custom `validateToken` function or HS256 HMAC verification
|
|
1600
|
+
* (depending on configuration) to validate the JWT. On success, maps the claims
|
|
1601
|
+
* to Kora's expected format and returns the user ID and device ID.
|
|
1602
|
+
*
|
|
1603
|
+
* The device ID for external providers is derived from the user ID with a
|
|
1604
|
+
* "external-" prefix, since external providers typically don't use Kora's
|
|
1605
|
+
* device identity system.
|
|
1606
|
+
*
|
|
1607
|
+
* @param token - The JWT access token issued by the external provider
|
|
1608
|
+
* @returns User ID and device ID if the token is valid, or null if invalid/expired
|
|
1609
|
+
*/
|
|
1610
|
+
validateAccessToken(token: string): Promise<{
|
|
1611
|
+
userId: string;
|
|
1612
|
+
deviceId: string;
|
|
1613
|
+
} | null>;
|
|
1614
|
+
/**
|
|
1615
|
+
* Not supported for external providers.
|
|
1616
|
+
*
|
|
1617
|
+
* User lookup must be performed through the external auth provider's API.
|
|
1618
|
+
*
|
|
1619
|
+
* @throws {ExternalAuthOperationNotSupportedError} Always
|
|
1620
|
+
*/
|
|
1621
|
+
getUser(_userId: string): Promise<AuthUser | null>;
|
|
1622
|
+
/**
|
|
1623
|
+
* Not supported for external providers.
|
|
1624
|
+
*
|
|
1625
|
+
* Device revocation must be managed through the external auth provider
|
|
1626
|
+
* or by revoking the user's tokens at the provider level.
|
|
1627
|
+
*
|
|
1628
|
+
* @throws {ExternalAuthOperationNotSupportedError} Always
|
|
1629
|
+
*/
|
|
1630
|
+
revokeDevice(_accessToken: string, _deviceId: string): Promise<void>;
|
|
1631
|
+
/**
|
|
1632
|
+
* Not supported for external providers.
|
|
1633
|
+
*
|
|
1634
|
+
* Device listing must be performed through the external auth provider's API.
|
|
1635
|
+
*
|
|
1636
|
+
* @throws {ExternalAuthOperationNotSupportedError} Always
|
|
1637
|
+
*/
|
|
1638
|
+
listDevices(_accessToken: string): Promise<AuthDevice[]>;
|
|
1639
|
+
/**
|
|
1640
|
+
* Creates a sync server auth provider compatible with `@korajs/server`.
|
|
1641
|
+
*
|
|
1642
|
+
* Returns an object with an `authenticate` method that validates the external
|
|
1643
|
+
* JWT and returns a Kora-compatible auth context. Use this to wire the external
|
|
1644
|
+
* auth provider into KoraSyncServer.
|
|
1645
|
+
*
|
|
1646
|
+
* @returns An object with an `authenticate` method for KoraSyncServer's `auth` config
|
|
1647
|
+
*
|
|
1648
|
+
* @example
|
|
1649
|
+
* ```typescript
|
|
1650
|
+
* const externalAuth = new ExternalJwtProvider({ ... })
|
|
1651
|
+
* const syncServer = new KoraSyncServer({
|
|
1652
|
+
* store,
|
|
1653
|
+
* auth: externalAuth.toSyncAuthProvider(),
|
|
1654
|
+
* })
|
|
1655
|
+
* ```
|
|
1656
|
+
*/
|
|
1657
|
+
toSyncAuthProvider(): {
|
|
1658
|
+
authenticate(token: string): Promise<{
|
|
1659
|
+
userId: string;
|
|
1660
|
+
scopes?: Record<string, Record<string, unknown>>;
|
|
1661
|
+
metadata?: Record<string, unknown>;
|
|
1662
|
+
} | null>;
|
|
1663
|
+
};
|
|
1664
|
+
/**
|
|
1665
|
+
* Extract and validate claims from a JWT token.
|
|
1666
|
+
*
|
|
1667
|
+
* Uses the custom validator if configured, otherwise falls back to
|
|
1668
|
+
* HS256 HMAC verification with the configured secret.
|
|
1669
|
+
*
|
|
1670
|
+
* @param token - The raw JWT string
|
|
1671
|
+
* @returns The decoded claims object, or null if the token is invalid
|
|
1672
|
+
*/
|
|
1673
|
+
private extractClaims;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Configuration for the Clerk authentication adapter.
|
|
1678
|
+
*
|
|
1679
|
+
* Clerk uses asymmetric signing (RS256) with JWKS key rotation, so this adapter
|
|
1680
|
+
* requires a custom `validateToken` function that handles JWKS-based verification.
|
|
1681
|
+
* The adapter provides sensible defaults for mapping Clerk's JWT claims to Kora's
|
|
1682
|
+
* expected format.
|
|
1683
|
+
*
|
|
1684
|
+
* Clerk JWTs typically contain:
|
|
1685
|
+
* - `sub`: User ID (e.g., "user_2abc123...")
|
|
1686
|
+
* - `email`: Primary email address (if configured in session claims)
|
|
1687
|
+
* - `first_name`, `last_name`: Name fields (if configured in session claims)
|
|
1688
|
+
* - `azp`: Authorized party (your frontend origin)
|
|
1689
|
+
* - `org_id`, `org_slug`, `org_role`: Organization claims (if using Clerk orgs)
|
|
1690
|
+
*
|
|
1691
|
+
* @example
|
|
1692
|
+
* ```typescript
|
|
1693
|
+
* import { createClerkAdapter } from '@korajs/auth/server'
|
|
1694
|
+
*
|
|
1695
|
+
* const clerkAuth = createClerkAdapter({
|
|
1696
|
+
* validateToken: async (token) => {
|
|
1697
|
+
* // Use Clerk's backend SDK or your own JWKS verification
|
|
1698
|
+
* const result = await clerkClient.verifyToken(token)
|
|
1699
|
+
* return result ? { sub: result.sub, ...result } : null
|
|
1700
|
+
* },
|
|
1701
|
+
* })
|
|
1702
|
+
*
|
|
1703
|
+
* // Use with Kora sync server
|
|
1704
|
+
* const syncServer = new KoraSyncServer({
|
|
1705
|
+
* store,
|
|
1706
|
+
* auth: clerkAuth.toSyncAuthProvider(),
|
|
1707
|
+
* })
|
|
1708
|
+
* ```
|
|
1709
|
+
*/
|
|
1710
|
+
interface ClerkAdapterConfig {
|
|
1711
|
+
/**
|
|
1712
|
+
* Custom token validator for Clerk JWTs.
|
|
1713
|
+
*
|
|
1714
|
+
* Clerk uses RS256 signing with JWKS key rotation, which requires either
|
|
1715
|
+
* Clerk's backend SDK or a JWKS-based verifier. This function receives
|
|
1716
|
+
* the raw JWT and should return the decoded claims or null.
|
|
1717
|
+
*
|
|
1718
|
+
* @param token - The raw JWT string from the Clerk session
|
|
1719
|
+
* @returns Decoded claims with at least a `sub` field, or null if invalid
|
|
1720
|
+
*/
|
|
1721
|
+
validateToken: (token: string) => Promise<{
|
|
1722
|
+
sub: string;
|
|
1723
|
+
[key: string]: unknown;
|
|
1724
|
+
} | null>;
|
|
1725
|
+
/**
|
|
1726
|
+
* Custom claim mapping override.
|
|
1727
|
+
*
|
|
1728
|
+
* By default, the Clerk adapter maps:
|
|
1729
|
+
* - `sub` -> `userId`
|
|
1730
|
+
* - `email` -> `email` (if present)
|
|
1731
|
+
* - `first_name` + `last_name` -> `name` (concatenated, if present)
|
|
1732
|
+
* - `org_id`, `org_slug`, `org_role` -> `metadata` (if present)
|
|
1733
|
+
*
|
|
1734
|
+
* Override this to customize how Clerk claims are mapped to Kora's format.
|
|
1735
|
+
*/
|
|
1736
|
+
mapClaims?: ExternalJwtProviderConfig['mapClaims'];
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Creates an ExternalJwtProvider configured for Clerk authentication.
|
|
1740
|
+
*
|
|
1741
|
+
* Clerk uses RS256 signing with JWKS key rotation, so a custom `validateToken`
|
|
1742
|
+
* function must be provided. This function should use Clerk's backend SDK or
|
|
1743
|
+
* a JWKS-based JWT verifier to validate tokens.
|
|
1744
|
+
*
|
|
1745
|
+
* This adapter does NOT depend on `@clerk/backend` or any Clerk SDK. It only
|
|
1746
|
+
* provides sensible defaults for mapping Clerk's JWT claims to Kora's format.
|
|
1747
|
+
* The actual token verification is delegated to the provided `validateToken` function.
|
|
1748
|
+
*
|
|
1749
|
+
* @param config - Clerk adapter configuration
|
|
1750
|
+
* @returns An ExternalJwtProvider instance configured for Clerk
|
|
1751
|
+
*
|
|
1752
|
+
* @example
|
|
1753
|
+
* ```typescript
|
|
1754
|
+
* import { createClerkAdapter } from '@korajs/auth/server'
|
|
1755
|
+
*
|
|
1756
|
+
* const clerkAuth = createClerkAdapter({
|
|
1757
|
+
* validateToken: async (token) => {
|
|
1758
|
+
* // Your JWKS verification logic here
|
|
1759
|
+
* const payload = await verifyWithJwks(token, CLERK_JWKS_URL)
|
|
1760
|
+
* return payload
|
|
1761
|
+
* },
|
|
1762
|
+
* })
|
|
1763
|
+
*
|
|
1764
|
+
* const result = await clerkAuth.validateAccessToken(sessionToken)
|
|
1765
|
+
* ```
|
|
1766
|
+
*/
|
|
1767
|
+
declare function createClerkAdapter(config: ClerkAdapterConfig): ExternalJwtProvider;
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Configuration for the Supabase authentication adapter.
|
|
1771
|
+
*
|
|
1772
|
+
* Supabase Auth signs JWTs with HS256 using the project's JWT secret,
|
|
1773
|
+
* which is available in your Supabase project settings under
|
|
1774
|
+
* Settings -> API -> JWT Secret.
|
|
1775
|
+
*
|
|
1776
|
+
* Supabase JWTs typically contain:
|
|
1777
|
+
* - `sub`: User UUID
|
|
1778
|
+
* - `email`: User's email address
|
|
1779
|
+
* - `role`: The database role (e.g., "authenticated", "anon")
|
|
1780
|
+
* - `aud`: Audience (usually "authenticated")
|
|
1781
|
+
* - `app_metadata`: Provider info, roles, etc.
|
|
1782
|
+
* - `user_metadata`: Custom user data (name, avatar, etc.)
|
|
1783
|
+
*
|
|
1784
|
+
* @example
|
|
1785
|
+
* ```typescript
|
|
1786
|
+
* import { createSupabaseAdapter } from '@korajs/auth/server'
|
|
1787
|
+
*
|
|
1788
|
+
* const supabaseAuth = createSupabaseAdapter({
|
|
1789
|
+
* jwtSecret: process.env.SUPABASE_JWT_SECRET,
|
|
1790
|
+
* })
|
|
1791
|
+
*
|
|
1792
|
+
* // Use with Kora sync server
|
|
1793
|
+
* const syncServer = new KoraSyncServer({
|
|
1794
|
+
* store,
|
|
1795
|
+
* auth: supabaseAuth.toSyncAuthProvider(),
|
|
1796
|
+
* })
|
|
1797
|
+
* ```
|
|
1798
|
+
*/
|
|
1799
|
+
interface SupabaseAdapterConfig {
|
|
1800
|
+
/**
|
|
1801
|
+
* Supabase JWT secret from your project settings.
|
|
1802
|
+
*
|
|
1803
|
+
* Found in: Supabase Dashboard -> Settings -> API -> JWT Secret
|
|
1804
|
+
*
|
|
1805
|
+
* This is the HS256 HMAC secret used to sign and verify Supabase Auth JWTs.
|
|
1806
|
+
* Keep this secret secure and never expose it in client-side code.
|
|
1807
|
+
*/
|
|
1808
|
+
jwtSecret: string;
|
|
1809
|
+
/**
|
|
1810
|
+
* Custom claim mapping override.
|
|
1811
|
+
*
|
|
1812
|
+
* By default, the Supabase adapter maps:
|
|
1813
|
+
* - `sub` -> `userId`
|
|
1814
|
+
* - `email` -> `email`
|
|
1815
|
+
* - `user_metadata.full_name` or `user_metadata.name` -> `name`
|
|
1816
|
+
* - `role`, `aud`, `app_metadata` -> `metadata`
|
|
1817
|
+
*
|
|
1818
|
+
* Override this to customize how Supabase claims are mapped to Kora's format.
|
|
1819
|
+
*/
|
|
1820
|
+
mapClaims?: ExternalJwtProviderConfig['mapClaims'];
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Creates an ExternalJwtProvider configured for Supabase Auth.
|
|
1824
|
+
*
|
|
1825
|
+
* Supabase Auth uses HS256 signing with the project's JWT secret, making it
|
|
1826
|
+
* compatible with Kora's built-in `verifyJwt` utility. No external SDK is needed.
|
|
1827
|
+
*
|
|
1828
|
+
* This adapter validates the JWT signature and expiration, then maps Supabase's
|
|
1829
|
+
* standard claims (sub, email, role, user_metadata) to Kora's auth context format.
|
|
1830
|
+
*
|
|
1831
|
+
* @param config - Supabase adapter configuration
|
|
1832
|
+
* @returns An ExternalJwtProvider instance configured for Supabase
|
|
1833
|
+
*
|
|
1834
|
+
* @example
|
|
1835
|
+
* ```typescript
|
|
1836
|
+
* import { createSupabaseAdapter } from '@korajs/auth/server'
|
|
1837
|
+
*
|
|
1838
|
+
* const supabaseAuth = createSupabaseAdapter({
|
|
1839
|
+
* jwtSecret: process.env.SUPABASE_JWT_SECRET,
|
|
1840
|
+
* })
|
|
1841
|
+
*
|
|
1842
|
+
* // Validate a Supabase access token
|
|
1843
|
+
* const result = await supabaseAuth.validateAccessToken(supabaseAccessToken)
|
|
1844
|
+
* if (result) {
|
|
1845
|
+
* console.log('Supabase user:', result.userId)
|
|
1846
|
+
* }
|
|
1847
|
+
*
|
|
1848
|
+
* // Or use with the sync server
|
|
1849
|
+
* const syncServer = new KoraSyncServer({
|
|
1850
|
+
* store,
|
|
1851
|
+
* auth: supabaseAuth.toSyncAuthProvider(),
|
|
1852
|
+
* })
|
|
1853
|
+
* ```
|
|
1854
|
+
*/
|
|
1855
|
+
declare function createSupabaseAdapter(config: SupabaseAdapterConfig): ExternalJwtProvider;
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Thrown when server-side passkey verification fails.
|
|
1859
|
+
*/
|
|
1860
|
+
declare class PasskeyVerificationError extends KoraError {
|
|
1861
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
1862
|
+
}
|
|
1863
|
+
/** Options returned by generateRegistrationOptions for the client. */
|
|
1864
|
+
interface RegistrationOptions {
|
|
1865
|
+
/** Base64url-encoded random challenge (32 bytes) */
|
|
1866
|
+
challenge: string;
|
|
1867
|
+
/** Relying party ID (domain) */
|
|
1868
|
+
rpId: string;
|
|
1869
|
+
/** Relying party display name */
|
|
1870
|
+
rpName: string;
|
|
1871
|
+
/** Base64url-encoded user ID */
|
|
1872
|
+
userId: string;
|
|
1873
|
+
/** User's email or username */
|
|
1874
|
+
userName: string;
|
|
1875
|
+
/** Human-readable display name */
|
|
1876
|
+
userDisplayName: string;
|
|
1877
|
+
/** Credential IDs to exclude (prevents re-registration) */
|
|
1878
|
+
excludeCredentialIds: string[];
|
|
1879
|
+
/** Authenticator selection criteria */
|
|
1880
|
+
authenticatorSelection: {
|
|
1881
|
+
authenticatorAttachment: 'platform';
|
|
1882
|
+
residentKey: 'preferred';
|
|
1883
|
+
userVerification: 'required';
|
|
1884
|
+
};
|
|
1885
|
+
/** Timeout in milliseconds */
|
|
1886
|
+
timeout: number;
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Generate registration options for creating a new passkey.
|
|
1890
|
+
*
|
|
1891
|
+
* Creates a cryptographically random challenge and assembles the options
|
|
1892
|
+
* object that should be sent to the client for `createPasskeyCredential()`.
|
|
1893
|
+
*
|
|
1894
|
+
* The server must store the challenge (keyed by user session or similar)
|
|
1895
|
+
* for later verification when the client responds.
|
|
1896
|
+
*
|
|
1897
|
+
* @param params - Registration parameters
|
|
1898
|
+
* @param params.rpId - Relying party ID (your domain, e.g. "example.com")
|
|
1899
|
+
* @param params.rpName - Relying party display name
|
|
1900
|
+
* @param params.userId - Unique user identifier
|
|
1901
|
+
* @param params.userName - User's email or username
|
|
1902
|
+
* @param params.userDisplayName - Human-readable display name
|
|
1903
|
+
* @param params.existingCredentialIds - Base64url credential IDs to exclude
|
|
1904
|
+
* @returns Registration options to send to the client, including the challenge
|
|
1905
|
+
*
|
|
1906
|
+
* @example
|
|
1907
|
+
* ```typescript
|
|
1908
|
+
* const options = generateRegistrationOptions({
|
|
1909
|
+
* rpId: 'example.com',
|
|
1910
|
+
* rpName: 'My App',
|
|
1911
|
+
* userId: user.id,
|
|
1912
|
+
* userName: user.email,
|
|
1913
|
+
* userDisplayName: user.name,
|
|
1914
|
+
* })
|
|
1915
|
+
* // Store options.challenge in session for later verification
|
|
1916
|
+
* // Send options to client
|
|
1917
|
+
* ```
|
|
1918
|
+
*/
|
|
1919
|
+
declare function generateRegistrationOptions(params: {
|
|
1920
|
+
rpId: string;
|
|
1921
|
+
rpName: string;
|
|
1922
|
+
userId: string;
|
|
1923
|
+
userName: string;
|
|
1924
|
+
userDisplayName: string;
|
|
1925
|
+
existingCredentialIds?: string[];
|
|
1926
|
+
}): RegistrationOptions;
|
|
1927
|
+
/** Result of verifying a registration response. */
|
|
1928
|
+
interface RegistrationVerificationResult {
|
|
1929
|
+
/** Whether the registration response was verified successfully */
|
|
1930
|
+
verified: boolean;
|
|
1931
|
+
/** Base64url-encoded credential ID */
|
|
1932
|
+
credentialId: string;
|
|
1933
|
+
/** Base64url-encoded COSE public key (store this for future authentication) */
|
|
1934
|
+
publicKey: string;
|
|
1935
|
+
/** Initial signature counter from the authenticator */
|
|
1936
|
+
signCount: number;
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Verify a registration response from the client.
|
|
1940
|
+
*
|
|
1941
|
+
* Validates the attestation object and clientDataJSON returned by the browser's
|
|
1942
|
+
* `navigator.credentials.create()` call. Extracts and returns the public key
|
|
1943
|
+
* and credential ID to store in your database.
|
|
1944
|
+
*
|
|
1945
|
+
* This implementation supports the "none" attestation format, which is the most
|
|
1946
|
+
* common and does not require trust in any attestation CA. For higher assurance
|
|
1947
|
+
* scenarios, extend this to verify packed/tpm/android attestation formats.
|
|
1948
|
+
*
|
|
1949
|
+
* @param params - Verification parameters
|
|
1950
|
+
* @param params.credential - The credential response from the client
|
|
1951
|
+
* @param params.expectedChallenge - The challenge that was sent to the client (base64url)
|
|
1952
|
+
* @param params.expectedOrigin - The expected origin (e.g. "https://example.com")
|
|
1953
|
+
* @param params.expectedRpId - The expected relying party ID (e.g. "example.com")
|
|
1954
|
+
* @returns Verification result with the credential ID and public key to store
|
|
1955
|
+
* @throws {PasskeyVerificationError} If the response is invalid or tampered with
|
|
1956
|
+
*
|
|
1957
|
+
* @example
|
|
1958
|
+
* ```typescript
|
|
1959
|
+
* const result = await verifyRegistrationResponse({
|
|
1960
|
+
* credential: clientResponse,
|
|
1961
|
+
* expectedChallenge: storedChallenge,
|
|
1962
|
+
* expectedOrigin: 'https://example.com',
|
|
1963
|
+
* expectedRpId: 'example.com',
|
|
1964
|
+
* })
|
|
1965
|
+
* if (result.verified) {
|
|
1966
|
+
* // Store result.credentialId, result.publicKey, result.signCount
|
|
1967
|
+
* }
|
|
1968
|
+
* ```
|
|
1969
|
+
*/
|
|
1970
|
+
declare function verifyRegistrationResponse(params: {
|
|
1971
|
+
credential: {
|
|
1972
|
+
credentialId: string;
|
|
1973
|
+
publicKey: string;
|
|
1974
|
+
clientDataJSON: string;
|
|
1975
|
+
attestationObject: string;
|
|
1976
|
+
};
|
|
1977
|
+
expectedChallenge: string;
|
|
1978
|
+
expectedOrigin: string;
|
|
1979
|
+
expectedRpId: string;
|
|
1980
|
+
}): Promise<RegistrationVerificationResult>;
|
|
1981
|
+
/** Options returned by generateAuthenticationOptions for the client. */
|
|
1982
|
+
interface AuthenticationOptions {
|
|
1983
|
+
/** Base64url-encoded random challenge (32 bytes) */
|
|
1984
|
+
challenge: string;
|
|
1985
|
+
/** Relying party ID */
|
|
1986
|
+
rpId: string;
|
|
1987
|
+
/** Credential IDs to allow (limit to specific credentials) */
|
|
1988
|
+
allowCredentialIds?: string[];
|
|
1989
|
+
/** User verification requirement */
|
|
1990
|
+
userVerification: 'preferred';
|
|
1991
|
+
/** Timeout in milliseconds */
|
|
1992
|
+
timeout: number;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Generate authentication options for signing in with a passkey.
|
|
1996
|
+
*
|
|
1997
|
+
* Creates a cryptographically random challenge and assembles the options
|
|
1998
|
+
* object that should be sent to the client for `authenticateWithPasskey()`.
|
|
1999
|
+
*
|
|
2000
|
+
* The server must store the challenge for later verification.
|
|
2001
|
+
*
|
|
2002
|
+
* @param params - Authentication parameters
|
|
2003
|
+
* @param params.rpId - Relying party ID (your domain)
|
|
2004
|
+
* @param params.allowCredentialIds - Base64url credential IDs to allow (optional)
|
|
2005
|
+
* @returns Authentication options to send to the client
|
|
2006
|
+
*
|
|
2007
|
+
* @example
|
|
2008
|
+
* ```typescript
|
|
2009
|
+
* const options = generateAuthenticationOptions({
|
|
2010
|
+
* rpId: 'example.com',
|
|
2011
|
+
* allowCredentialIds: user.credentialIds,
|
|
2012
|
+
* })
|
|
2013
|
+
* // Store options.challenge in session
|
|
2014
|
+
* // Send options to client
|
|
2015
|
+
* ```
|
|
2016
|
+
*/
|
|
2017
|
+
declare function generateAuthenticationOptions(params: {
|
|
2018
|
+
rpId: string;
|
|
2019
|
+
allowCredentialIds?: string[];
|
|
2020
|
+
}): AuthenticationOptions;
|
|
2021
|
+
/** Result of verifying an authentication response. */
|
|
2022
|
+
interface AuthenticationVerificationResult {
|
|
2023
|
+
/** Whether the authentication response was verified successfully */
|
|
2024
|
+
verified: boolean;
|
|
2025
|
+
/** Updated signature counter (store this to detect cloned authenticators) */
|
|
2026
|
+
newSignCount: number;
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Verify an authentication response from the client.
|
|
2030
|
+
*
|
|
2031
|
+
* Validates the signed assertion returned by the browser's
|
|
2032
|
+
* `navigator.credentials.get()` call. Checks the signature against the
|
|
2033
|
+
* stored public key, verifies the challenge and origin, and validates
|
|
2034
|
+
* the signature counter to detect cloned authenticators.
|
|
2035
|
+
*
|
|
2036
|
+
* This implementation supports ECDSA P-256 (ES256, COSE algorithm -7)
|
|
2037
|
+
* signatures, which is the most common algorithm used by platform
|
|
2038
|
+
* authenticators (Touch ID, Face ID, Windows Hello).
|
|
2039
|
+
*
|
|
2040
|
+
* @param params - Verification parameters
|
|
2041
|
+
* @param params.assertion - The assertion response from the client
|
|
2042
|
+
* @param params.expectedChallenge - The challenge that was sent to the client (base64url)
|
|
2043
|
+
* @param params.expectedOrigin - The expected origin (e.g. "https://example.com")
|
|
2044
|
+
* @param params.expectedRpId - The expected relying party ID
|
|
2045
|
+
* @param params.publicKey - The stored COSE public key (base64url, from registration)
|
|
2046
|
+
* @param params.previousSignCount - The previously stored signature counter
|
|
2047
|
+
* @returns Verification result with the new signature counter
|
|
2048
|
+
* @throws {PasskeyVerificationError} If the assertion is invalid
|
|
2049
|
+
*
|
|
2050
|
+
* @example
|
|
2051
|
+
* ```typescript
|
|
2052
|
+
* const result = await verifyAuthenticationResponse({
|
|
2053
|
+
* assertion: clientAssertion,
|
|
2054
|
+
* expectedChallenge: storedChallenge,
|
|
2055
|
+
* expectedOrigin: 'https://example.com',
|
|
2056
|
+
* expectedRpId: 'example.com',
|
|
2057
|
+
* publicKey: storedCredential.publicKey,
|
|
2058
|
+
* previousSignCount: storedCredential.signCount,
|
|
2059
|
+
* })
|
|
2060
|
+
* if (result.verified) {
|
|
2061
|
+
* // Update stored sign count: storedCredential.signCount = result.newSignCount
|
|
2062
|
+
* // Issue session tokens
|
|
2063
|
+
* }
|
|
2064
|
+
* ```
|
|
2065
|
+
*/
|
|
2066
|
+
declare function verifyAuthenticationResponse(params: {
|
|
2067
|
+
assertion: {
|
|
2068
|
+
credentialId: string;
|
|
2069
|
+
authenticatorData: string;
|
|
2070
|
+
clientDataJSON: string;
|
|
2071
|
+
signature: string;
|
|
2072
|
+
userHandle: string | null;
|
|
2073
|
+
};
|
|
2074
|
+
expectedChallenge: string;
|
|
2075
|
+
expectedOrigin: string;
|
|
2076
|
+
expectedRpId: string;
|
|
2077
|
+
publicKey: string;
|
|
2078
|
+
previousSignCount: number;
|
|
2079
|
+
}): Promise<AuthenticationVerificationResult>;
|
|
2080
|
+
|
|
2081
|
+
/**
|
|
2082
|
+
* An organization (workspace/team) that groups users together.
|
|
2083
|
+
*
|
|
2084
|
+
* Organizations are the fundamental unit of multi-tenancy in Kora.
|
|
2085
|
+
* Data is scoped to organizations, and users access data through
|
|
2086
|
+
* their organization memberships and roles.
|
|
2087
|
+
*/
|
|
2088
|
+
interface Organization {
|
|
2089
|
+
/** Unique identifier (UUID v7) */
|
|
2090
|
+
id: string;
|
|
2091
|
+
/** Display name of the organization */
|
|
2092
|
+
name: string;
|
|
2093
|
+
/** URL-friendly identifier (unique, lowercase, alphanumeric + hyphens) */
|
|
2094
|
+
slug: string;
|
|
2095
|
+
/** User ID of the organization owner */
|
|
2096
|
+
ownerId: string;
|
|
2097
|
+
/** When the organization was created (ms since epoch) */
|
|
2098
|
+
createdAt: number;
|
|
2099
|
+
/** When the organization was last updated (ms since epoch) */
|
|
2100
|
+
updatedAt: number;
|
|
2101
|
+
/** Arbitrary metadata (plan, billing info, settings, etc.) */
|
|
2102
|
+
metadata: Record<string, unknown>;
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Parameters for creating a new organization.
|
|
2106
|
+
*/
|
|
2107
|
+
interface CreateOrgParams {
|
|
2108
|
+
/** Display name */
|
|
2109
|
+
name: string;
|
|
2110
|
+
/** URL-friendly slug (auto-generated from name if omitted) */
|
|
2111
|
+
slug?: string;
|
|
2112
|
+
/** Optional metadata to attach */
|
|
2113
|
+
metadata?: Record<string, unknown>;
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Parameters for updating an existing organization.
|
|
2117
|
+
*/
|
|
2118
|
+
interface UpdateOrgParams {
|
|
2119
|
+
/** New display name */
|
|
2120
|
+
name?: string;
|
|
2121
|
+
/** New slug */
|
|
2122
|
+
slug?: string;
|
|
2123
|
+
/** Metadata to merge (shallow merge) */
|
|
2124
|
+
metadata?: Record<string, unknown>;
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Built-in organization roles, ordered by decreasing privilege.
|
|
2128
|
+
*
|
|
2129
|
+
* - **owner**: Full control, can delete org, transfer ownership, manage billing
|
|
2130
|
+
* - **admin**: Manage members and settings, full data access
|
|
2131
|
+
* - **member**: Read + write own data, read shared data
|
|
2132
|
+
* - **viewer**: Read-only access to shared data
|
|
2133
|
+
* - **billing**: Billing management only, no data access
|
|
2134
|
+
*/
|
|
2135
|
+
declare const ORG_ROLES: readonly ["owner", "admin", "member", "viewer", "billing"];
|
|
2136
|
+
type OrgRole = (typeof ORG_ROLES)[number];
|
|
2137
|
+
/**
|
|
2138
|
+
* Role hierarchy for permission inheritance.
|
|
2139
|
+
* Higher number = more privilege.
|
|
2140
|
+
*/
|
|
2141
|
+
declare const ROLE_HIERARCHY: Record<OrgRole, number>;
|
|
2142
|
+
/**
|
|
2143
|
+
* Check if one role has at least the privilege level of another.
|
|
2144
|
+
*
|
|
2145
|
+
* @param userRole - The user's current role
|
|
2146
|
+
* @param requiredRole - The minimum role required
|
|
2147
|
+
* @returns true if userRole >= requiredRole in the hierarchy
|
|
2148
|
+
*/
|
|
2149
|
+
declare function hasRoleLevel(userRole: OrgRole, requiredRole: OrgRole): boolean;
|
|
2150
|
+
/**
|
|
2151
|
+
* A user's membership in an organization.
|
|
2152
|
+
*/
|
|
2153
|
+
interface Membership {
|
|
2154
|
+
/** Unique identifier for this membership record */
|
|
2155
|
+
id: string;
|
|
2156
|
+
/** Organization this membership belongs to */
|
|
2157
|
+
orgId: string;
|
|
2158
|
+
/** User who is a member */
|
|
2159
|
+
userId: string;
|
|
2160
|
+
/** Role within the organization */
|
|
2161
|
+
role: OrgRole;
|
|
2162
|
+
/** User who invited this member (null if founder) */
|
|
2163
|
+
invitedBy: string | null;
|
|
2164
|
+
/** When the user joined the organization (ms since epoch) */
|
|
2165
|
+
joinedAt: number;
|
|
2166
|
+
/** Arbitrary metadata (department, title, etc.) */
|
|
2167
|
+
metadata: Record<string, unknown>;
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Invitation status lifecycle.
|
|
2171
|
+
*/
|
|
2172
|
+
declare const INVITATION_STATUSES: readonly ["pending", "accepted", "revoked", "expired"];
|
|
2173
|
+
type InvitationStatus = (typeof INVITATION_STATUSES)[number];
|
|
2174
|
+
/**
|
|
2175
|
+
* An invitation to join an organization.
|
|
2176
|
+
*
|
|
2177
|
+
* Invitations are sent by email and include a single-use token.
|
|
2178
|
+
* They expire after a configurable duration (default: 7 days).
|
|
2179
|
+
*/
|
|
2180
|
+
interface OrgInvitation {
|
|
2181
|
+
/** Unique identifier */
|
|
2182
|
+
id: string;
|
|
2183
|
+
/** Organization the invitation is for */
|
|
2184
|
+
orgId: string;
|
|
2185
|
+
/** Email address of the invitee */
|
|
2186
|
+
email: string;
|
|
2187
|
+
/** Role the invitee will receive upon accepting */
|
|
2188
|
+
role: OrgRole;
|
|
2189
|
+
/** User who created the invitation */
|
|
2190
|
+
invitedBy: string;
|
|
2191
|
+
/** Cryptographically random single-use token */
|
|
2192
|
+
token: string;
|
|
2193
|
+
/** When the invitation was created (ms since epoch) */
|
|
2194
|
+
createdAt: number;
|
|
2195
|
+
/** When the invitation expires (ms since epoch) */
|
|
2196
|
+
expiresAt: number;
|
|
2197
|
+
/** Current status */
|
|
2198
|
+
status: InvitationStatus;
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Parameters for creating an invitation.
|
|
2202
|
+
*/
|
|
2203
|
+
interface CreateInvitationParams {
|
|
2204
|
+
/** Email address to invite */
|
|
2205
|
+
email: string;
|
|
2206
|
+
/** Role to assign when the invitation is accepted */
|
|
2207
|
+
role: OrgRole;
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Base error for organization-related operations.
|
|
2211
|
+
*/
|
|
2212
|
+
declare class OrgError extends KoraError {
|
|
2213
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
2214
|
+
}
|
|
2215
|
+
declare class OrgNotFoundError extends OrgError {
|
|
2216
|
+
constructor();
|
|
2217
|
+
}
|
|
2218
|
+
declare class OrgSlugTakenError extends OrgError {
|
|
2219
|
+
constructor();
|
|
2220
|
+
}
|
|
2221
|
+
declare class MembershipNotFoundError extends OrgError {
|
|
2222
|
+
constructor();
|
|
2223
|
+
}
|
|
2224
|
+
declare class MemberAlreadyExistsError extends OrgError {
|
|
2225
|
+
constructor();
|
|
2226
|
+
}
|
|
2227
|
+
declare class InsufficientRoleError extends OrgError {
|
|
2228
|
+
constructor(required: OrgRole);
|
|
2229
|
+
}
|
|
2230
|
+
declare class CannotRemoveOwnerError extends OrgError {
|
|
2231
|
+
constructor();
|
|
2232
|
+
}
|
|
2233
|
+
declare class InvitationNotFoundError extends OrgError {
|
|
2234
|
+
constructor();
|
|
2235
|
+
}
|
|
2236
|
+
declare class InvitationExpiredError extends OrgError {
|
|
2237
|
+
constructor();
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
/**
|
|
2241
|
+
* Persistence interface for organizations, memberships, and invitations.
|
|
2242
|
+
*
|
|
2243
|
+
* Implement this interface to back organizations with any storage:
|
|
2244
|
+
* - `InMemoryOrgStore` for development and testing
|
|
2245
|
+
* - PostgreSQL/MySQL via Drizzle for production
|
|
2246
|
+
* - SQLite for self-hosted or embedded scenarios
|
|
2247
|
+
*
|
|
2248
|
+
* All methods are async to support any storage backend.
|
|
2249
|
+
*/
|
|
2250
|
+
interface OrgStore {
|
|
2251
|
+
/** Create a new organization. The caller becomes the owner. */
|
|
2252
|
+
createOrg(ownerId: string, params: CreateOrgParams): Promise<Organization>;
|
|
2253
|
+
/** Get an organization by ID. Returns null if not found. */
|
|
2254
|
+
getOrg(orgId: string): Promise<Organization | null>;
|
|
2255
|
+
/** Get an organization by slug. Returns null if not found. */
|
|
2256
|
+
getOrgBySlug(slug: string): Promise<Organization | null>;
|
|
2257
|
+
/** Update an organization's mutable fields. */
|
|
2258
|
+
updateOrg(orgId: string, params: UpdateOrgParams): Promise<Organization>;
|
|
2259
|
+
/** Delete an organization and all its memberships and invitations. */
|
|
2260
|
+
deleteOrg(orgId: string): Promise<void>;
|
|
2261
|
+
/** List all organizations a user is a member of. */
|
|
2262
|
+
listUserOrgs(userId: string): Promise<Organization[]>;
|
|
2263
|
+
/** Add a user as a member of an organization. */
|
|
2264
|
+
addMember(orgId: string, userId: string, role: OrgRole, invitedBy: string | null): Promise<Membership>;
|
|
2265
|
+
/** Remove a user from an organization. Cannot remove the owner. */
|
|
2266
|
+
removeMember(orgId: string, userId: string): Promise<void>;
|
|
2267
|
+
/** Update a member's role within an organization. */
|
|
2268
|
+
updateMemberRole(orgId: string, userId: string, role: OrgRole): Promise<Membership>;
|
|
2269
|
+
/** List all members of an organization. */
|
|
2270
|
+
listMembers(orgId: string): Promise<Membership[]>;
|
|
2271
|
+
/** Get a specific user's membership in an organization. Returns null if not a member. */
|
|
2272
|
+
getMembership(orgId: string, userId: string): Promise<Membership | null>;
|
|
2273
|
+
/** Transfer ownership of an organization to another member. */
|
|
2274
|
+
transferOwnership(orgId: string, newOwnerId: string): Promise<void>;
|
|
2275
|
+
/** Create an invitation to join an organization. */
|
|
2276
|
+
createInvitation(orgId: string, invitedBy: string, params: CreateInvitationParams): Promise<OrgInvitation>;
|
|
2277
|
+
/** Look up an invitation by its single-use token. Returns null if not found or already consumed. */
|
|
2278
|
+
getInvitationByToken(token: string): Promise<OrgInvitation | null>;
|
|
2279
|
+
/** Consume an invitation (mark as accepted). Returns the invitation details. */
|
|
2280
|
+
consumeInvitation(token: string): Promise<OrgInvitation>;
|
|
2281
|
+
/** Revoke a pending invitation. */
|
|
2282
|
+
revokeInvitation(invitationId: string): Promise<void>;
|
|
2283
|
+
/** List pending invitations for an organization. */
|
|
2284
|
+
listPendingInvitations(orgId: string): Promise<OrgInvitation[]>;
|
|
2285
|
+
/** List pending invitations for a specific email address. */
|
|
2286
|
+
listInvitationsForEmail(email: string): Promise<OrgInvitation[]>;
|
|
2287
|
+
/** Remove expired invitations. Returns count of removed invitations. */
|
|
2288
|
+
cleanExpiredInvitations(): Promise<number>;
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* In-memory implementation of OrgStore for development and testing.
|
|
2292
|
+
*
|
|
2293
|
+
* Data is lost when the process exits. For production, implement OrgStore
|
|
2294
|
+
* with a persistent backend (PostgreSQL, MySQL, SQLite via Drizzle).
|
|
2295
|
+
*
|
|
2296
|
+
* @example
|
|
2297
|
+
* ```typescript
|
|
2298
|
+
* const orgStore = new InMemoryOrgStore()
|
|
2299
|
+
* const org = await orgStore.createOrg('user-1', { name: 'Acme Inc', slug: 'acme' })
|
|
2300
|
+
* ```
|
|
2301
|
+
*/
|
|
2302
|
+
declare class InMemoryOrgStore implements OrgStore {
|
|
2303
|
+
private orgs;
|
|
2304
|
+
private memberships;
|
|
2305
|
+
private invitations;
|
|
2306
|
+
createOrg(ownerId: string, params: CreateOrgParams): Promise<Organization>;
|
|
2307
|
+
getOrg(orgId: string): Promise<Organization | null>;
|
|
2308
|
+
getOrgBySlug(slug: string): Promise<Organization | null>;
|
|
2309
|
+
updateOrg(orgId: string, params: UpdateOrgParams): Promise<Organization>;
|
|
2310
|
+
deleteOrg(orgId: string): Promise<void>;
|
|
2311
|
+
listUserOrgs(userId: string): Promise<Organization[]>;
|
|
2312
|
+
addMember(orgId: string, userId: string, role: OrgRole, invitedBy: string | null): Promise<Membership>;
|
|
2313
|
+
removeMember(orgId: string, userId: string): Promise<void>;
|
|
2314
|
+
updateMemberRole(orgId: string, userId: string, role: OrgRole): Promise<Membership>;
|
|
2315
|
+
listMembers(orgId: string): Promise<Membership[]>;
|
|
2316
|
+
getMembership(orgId: string, userId: string): Promise<Membership | null>;
|
|
2317
|
+
transferOwnership(orgId: string, newOwnerId: string): Promise<void>;
|
|
2318
|
+
createInvitation(orgId: string, invitedBy: string, params: CreateInvitationParams): Promise<OrgInvitation>;
|
|
2319
|
+
getInvitationByToken(token: string): Promise<OrgInvitation | null>;
|
|
2320
|
+
consumeInvitation(token: string): Promise<OrgInvitation>;
|
|
2321
|
+
revokeInvitation(invitationId: string): Promise<void>;
|
|
2322
|
+
listPendingInvitations(orgId: string): Promise<OrgInvitation[]>;
|
|
2323
|
+
listInvitationsForEmail(email: string): Promise<OrgInvitation[]>;
|
|
2324
|
+
cleanExpiredInvitations(): Promise<number>;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* Response envelope returned by all org route handlers.
|
|
2329
|
+
* Mirrors AuthRouteResponse for consistency.
|
|
2330
|
+
*/
|
|
2331
|
+
interface OrgRouteResponse<T> {
|
|
2332
|
+
/** HTTP status code */
|
|
2333
|
+
status: number;
|
|
2334
|
+
/** Either the success payload or an error message */
|
|
2335
|
+
body: {
|
|
2336
|
+
data: T;
|
|
2337
|
+
} | {
|
|
2338
|
+
error: string;
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Configuration for the org route handlers.
|
|
2343
|
+
*/
|
|
2344
|
+
interface OrgRoutesConfig {
|
|
2345
|
+
/** The organization store backing all org operations */
|
|
2346
|
+
orgStore: OrgStore;
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Server-side route handlers for organization management.
|
|
2350
|
+
*
|
|
2351
|
+
* These handlers enforce authorization (role checks), input validation,
|
|
2352
|
+
* and produce transport-agnostic response objects. Wire them to your
|
|
2353
|
+
* HTTP framework (Express, Hono, Fastify, etc.):
|
|
2354
|
+
*
|
|
2355
|
+
* @example
|
|
2356
|
+
* ```typescript
|
|
2357
|
+
* const orgRoutes = new OrgRoutes({ orgStore: new InMemoryOrgStore() })
|
|
2358
|
+
*
|
|
2359
|
+
* app.post('/orgs', async (req, res) => {
|
|
2360
|
+
* const result = await orgRoutes.createOrg(req.userId, req.body)
|
|
2361
|
+
* res.status(result.status).json(result.body)
|
|
2362
|
+
* })
|
|
2363
|
+
* ```
|
|
2364
|
+
*/
|
|
2365
|
+
declare class OrgRoutes {
|
|
2366
|
+
private readonly store;
|
|
2367
|
+
constructor(config: OrgRoutesConfig);
|
|
2368
|
+
/**
|
|
2369
|
+
* Create a new organization. The authenticated user becomes the owner.
|
|
2370
|
+
*/
|
|
2371
|
+
createOrg(userId: string, params: {
|
|
2372
|
+
name?: unknown;
|
|
2373
|
+
slug?: unknown;
|
|
2374
|
+
metadata?: unknown;
|
|
2375
|
+
}): Promise<OrgRouteResponse<Organization>>;
|
|
2376
|
+
/**
|
|
2377
|
+
* Get an organization by ID. Requires membership.
|
|
2378
|
+
*/
|
|
2379
|
+
getOrg(userId: string, orgId: string): Promise<OrgRouteResponse<Organization>>;
|
|
2380
|
+
/**
|
|
2381
|
+
* Update an organization. Requires admin or higher.
|
|
2382
|
+
*/
|
|
2383
|
+
updateOrg(userId: string, orgId: string, params: {
|
|
2384
|
+
name?: unknown;
|
|
2385
|
+
slug?: unknown;
|
|
2386
|
+
metadata?: unknown;
|
|
2387
|
+
}): Promise<OrgRouteResponse<Organization>>;
|
|
2388
|
+
/**
|
|
2389
|
+
* Delete an organization. Requires owner.
|
|
2390
|
+
*/
|
|
2391
|
+
deleteOrg(userId: string, orgId: string): Promise<OrgRouteResponse<{
|
|
2392
|
+
deleted: true;
|
|
2393
|
+
}>>;
|
|
2394
|
+
/**
|
|
2395
|
+
* List all organizations the authenticated user belongs to.
|
|
2396
|
+
*/
|
|
2397
|
+
listUserOrgs(userId: string): Promise<OrgRouteResponse<Organization[]>>;
|
|
2398
|
+
/**
|
|
2399
|
+
* Add a member to an organization. Requires admin or higher.
|
|
2400
|
+
*/
|
|
2401
|
+
addMember(userId: string, orgId: string, params: {
|
|
2402
|
+
targetUserId?: unknown;
|
|
2403
|
+
role?: unknown;
|
|
2404
|
+
}): Promise<OrgRouteResponse<Membership>>;
|
|
2405
|
+
/**
|
|
2406
|
+
* Remove a member from an organization. Requires admin or higher.
|
|
2407
|
+
* Members can also remove themselves (leave).
|
|
2408
|
+
*/
|
|
2409
|
+
removeMember(userId: string, orgId: string, targetUserId: string): Promise<OrgRouteResponse<{
|
|
2410
|
+
removed: true;
|
|
2411
|
+
}>>;
|
|
2412
|
+
/**
|
|
2413
|
+
* Update a member's role. Requires admin or higher.
|
|
2414
|
+
*/
|
|
2415
|
+
updateMemberRole(userId: string, orgId: string, params: {
|
|
2416
|
+
targetUserId?: unknown;
|
|
2417
|
+
role?: unknown;
|
|
2418
|
+
}): Promise<OrgRouteResponse<Membership>>;
|
|
2419
|
+
/**
|
|
2420
|
+
* List all members of an organization. Requires membership.
|
|
2421
|
+
*/
|
|
2422
|
+
listMembers(userId: string, orgId: string): Promise<OrgRouteResponse<Membership[]>>;
|
|
2423
|
+
/**
|
|
2424
|
+
* Transfer ownership to another member. Requires owner.
|
|
2425
|
+
*/
|
|
2426
|
+
transferOwnership(userId: string, orgId: string, params: {
|
|
2427
|
+
newOwnerId?: unknown;
|
|
2428
|
+
}): Promise<OrgRouteResponse<{
|
|
2429
|
+
transferred: true;
|
|
2430
|
+
}>>;
|
|
2431
|
+
/**
|
|
2432
|
+
* Create an invitation to join the organization. Requires admin or higher.
|
|
2433
|
+
*/
|
|
2434
|
+
createInvitation(userId: string, orgId: string, params: {
|
|
2435
|
+
email?: unknown;
|
|
2436
|
+
role?: unknown;
|
|
2437
|
+
}): Promise<OrgRouteResponse<OrgInvitation>>;
|
|
2438
|
+
/**
|
|
2439
|
+
* Accept an invitation by its token. The authenticated user joins the org.
|
|
2440
|
+
*/
|
|
2441
|
+
acceptInvitation(userId: string, params: {
|
|
2442
|
+
token?: unknown;
|
|
2443
|
+
}): Promise<OrgRouteResponse<Membership>>;
|
|
2444
|
+
/**
|
|
2445
|
+
* Revoke a pending invitation. Requires admin or higher.
|
|
2446
|
+
*/
|
|
2447
|
+
revokeInvitation(userId: string, orgId: string, invitationId: string): Promise<OrgRouteResponse<{
|
|
2448
|
+
revoked: true;
|
|
2449
|
+
}>>;
|
|
2450
|
+
/**
|
|
2451
|
+
* List pending invitations for an organization. Requires admin or higher.
|
|
2452
|
+
*/
|
|
2453
|
+
listPendingInvitations(userId: string, orgId: string): Promise<OrgRouteResponse<OrgInvitation[]>>;
|
|
2454
|
+
/**
|
|
2455
|
+
* List pending invitations for the authenticated user's email.
|
|
2456
|
+
*/
|
|
2457
|
+
listMyInvitations(email: string): Promise<OrgRouteResponse<OrgInvitation[]>>;
|
|
2458
|
+
/**
|
|
2459
|
+
* Check if the caller has the required role in the org.
|
|
2460
|
+
* Returns an error response if not authorized, or null if authorized.
|
|
2461
|
+
*/
|
|
2462
|
+
private requireRole;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
/**
|
|
2466
|
+
* A permission is a `resource:action` string.
|
|
2467
|
+
*
|
|
2468
|
+
* Resources are typically collection names or system resources.
|
|
2469
|
+
* Actions are the operations allowed on that resource.
|
|
2470
|
+
*
|
|
2471
|
+
* @example
|
|
2472
|
+
* ```typescript
|
|
2473
|
+
* const permission: Permission = 'todos:write'
|
|
2474
|
+
* const adminPerm: Permission = 'org:manage-members'
|
|
2475
|
+
* const wildcard: Permission = 'todos:*' // all actions on todos
|
|
2476
|
+
* const superAdmin: Permission = '*:*' // all actions on all resources
|
|
2477
|
+
* ```
|
|
2478
|
+
*/
|
|
2479
|
+
type Permission = `${string}:${string}`;
|
|
2480
|
+
/**
|
|
2481
|
+
* Parse a permission string into its resource and action parts.
|
|
2482
|
+
*/
|
|
2483
|
+
declare function parsePermission(permission: Permission): {
|
|
2484
|
+
resource: string;
|
|
2485
|
+
action: string;
|
|
2486
|
+
};
|
|
2487
|
+
/**
|
|
2488
|
+
* Check if a granted permission covers a required permission.
|
|
2489
|
+
* Supports wildcards: `todos:*` covers `todos:read`, `*:*` covers everything.
|
|
2490
|
+
*/
|
|
2491
|
+
declare function permissionCovers(granted: Permission, required: Permission): boolean;
|
|
2492
|
+
/**
|
|
2493
|
+
* A role definition describes a named set of permissions with optional inheritance.
|
|
2494
|
+
*
|
|
2495
|
+
* Roles form a hierarchy via `inherits`. When evaluating permissions,
|
|
2496
|
+
* all inherited roles' permissions are included transitively.
|
|
2497
|
+
*
|
|
2498
|
+
* @example
|
|
2499
|
+
* ```typescript
|
|
2500
|
+
* const roles: RoleDefinition[] = [
|
|
2501
|
+
* { name: 'viewer', permissions: ['todos:read', 'projects:read'] },
|
|
2502
|
+
* { name: 'member', permissions: ['todos:write', 'projects:write'], inherits: ['viewer'] },
|
|
2503
|
+
* { name: 'admin', permissions: ['org:manage-members'], inherits: ['member'] },
|
|
2504
|
+
* ]
|
|
2505
|
+
* ```
|
|
2506
|
+
*/
|
|
2507
|
+
interface RoleDefinition {
|
|
2508
|
+
/** Unique name for this role */
|
|
2509
|
+
name: string;
|
|
2510
|
+
/** Permissions directly granted to this role */
|
|
2511
|
+
permissions: Permission[];
|
|
2512
|
+
/** Roles this role inherits from (all their permissions are included) */
|
|
2513
|
+
inherits?: string[];
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Default built-in roles for organizations.
|
|
2517
|
+
*
|
|
2518
|
+
* These can be overridden or extended using `defineRoles()`.
|
|
2519
|
+
*/
|
|
2520
|
+
declare const BUILT_IN_ROLES: readonly RoleDefinition[];
|
|
2521
|
+
/**
|
|
2522
|
+
* Configuration for the RBAC engine.
|
|
2523
|
+
*/
|
|
2524
|
+
interface RbacConfig {
|
|
2525
|
+
/** Role definitions. Defaults to BUILT_IN_ROLES if not provided. */
|
|
2526
|
+
roles?: RoleDefinition[];
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* A sync scope filter for a single collection.
|
|
2530
|
+
* The keys are field names and values are the required values.
|
|
2531
|
+
* A special `__readonly` key can be set to restrict to read-only access.
|
|
2532
|
+
*
|
|
2533
|
+
* @example
|
|
2534
|
+
* ```typescript
|
|
2535
|
+
* // Only sync todos belonging to the user within the org
|
|
2536
|
+
* const scope: ScopeFilter = { orgId: 'org-123', userId: 'user-456' }
|
|
2537
|
+
*
|
|
2538
|
+
* // Read-only scope for viewers
|
|
2539
|
+
* const readOnlyScope: ScopeFilter = { orgId: 'org-123', __readonly: true }
|
|
2540
|
+
* ```
|
|
2541
|
+
*/
|
|
2542
|
+
interface ScopeFilter {
|
|
2543
|
+
[field: string]: unknown;
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* A complete set of sync scopes, keyed by collection name.
|
|
2547
|
+
*
|
|
2548
|
+
* @example
|
|
2549
|
+
* ```typescript
|
|
2550
|
+
* const scopes: SyncScopes = {
|
|
2551
|
+
* todos: { orgId: 'org-123' },
|
|
2552
|
+
* projects: { orgId: 'org-123' },
|
|
2553
|
+
* }
|
|
2554
|
+
* ```
|
|
2555
|
+
*/
|
|
2556
|
+
type SyncScopes = Record<string, ScopeFilter>;
|
|
2557
|
+
/**
|
|
2558
|
+
* Custom scope resolver for a specific collection.
|
|
2559
|
+
* Given the user context, returns the scope filter for that collection.
|
|
2560
|
+
*/
|
|
2561
|
+
type CollectionScopeResolver = (ctx: ScopeContext) => ScopeFilter | null;
|
|
2562
|
+
/**
|
|
2563
|
+
* Context passed to scope resolvers.
|
|
2564
|
+
*/
|
|
2565
|
+
interface ScopeContext {
|
|
2566
|
+
userId: string;
|
|
2567
|
+
orgId: string;
|
|
2568
|
+
role: string;
|
|
2569
|
+
permissions: Permission[];
|
|
2570
|
+
}
|
|
2571
|
+
declare class RbacError extends KoraError {
|
|
2572
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
2573
|
+
}
|
|
2574
|
+
declare class InvalidPermissionError extends RbacError {
|
|
2575
|
+
constructor(permission: string);
|
|
2576
|
+
}
|
|
2577
|
+
declare class RoleNotFoundError extends RbacError {
|
|
2578
|
+
constructor(role: string);
|
|
2579
|
+
}
|
|
2580
|
+
declare class CircularInheritanceError extends RbacError {
|
|
2581
|
+
constructor(chain: string[]);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
/**
|
|
2585
|
+
* Permission evaluation engine for role-based access control.
|
|
2586
|
+
*
|
|
2587
|
+
* The engine resolves permissions through role inheritance, supports
|
|
2588
|
+
* wildcard matching, and integrates with the OrgStore for membership lookups.
|
|
2589
|
+
*
|
|
2590
|
+
* @example
|
|
2591
|
+
* ```typescript
|
|
2592
|
+
* const rbac = new RbacEngine({ orgStore })
|
|
2593
|
+
*
|
|
2594
|
+
* // Check a permission
|
|
2595
|
+
* const canWrite = await rbac.hasPermission('user-1', 'org-1', 'todos:write')
|
|
2596
|
+
*
|
|
2597
|
+
* // Get all permissions for a user in an org
|
|
2598
|
+
* const perms = await rbac.getUserPermissions('user-1', 'org-1')
|
|
2599
|
+
*
|
|
2600
|
+
* // Resolve sync scopes
|
|
2601
|
+
* const scopes = await rbac.resolveScopes('user-1', 'org-1')
|
|
2602
|
+
* ```
|
|
2603
|
+
*/
|
|
2604
|
+
declare class RbacEngine {
|
|
2605
|
+
private readonly orgStore;
|
|
2606
|
+
private readonly roleMap;
|
|
2607
|
+
private readonly resolvedPermissions;
|
|
2608
|
+
private readonly collectionResolvers;
|
|
2609
|
+
constructor(orgStore: OrgStore, config?: RbacConfig);
|
|
2610
|
+
/**
|
|
2611
|
+
* Check if a user has a specific permission in an organization.
|
|
2612
|
+
*
|
|
2613
|
+
* Resolves the user's role from the org membership, then evaluates
|
|
2614
|
+
* the role's permissions (including inherited ones) against the required permission.
|
|
2615
|
+
*
|
|
2616
|
+
* Returns false (not an error) if the user is not a member.
|
|
2617
|
+
*/
|
|
2618
|
+
hasPermission(userId: string, orgId: string, permission: Permission): Promise<boolean>;
|
|
2619
|
+
/**
|
|
2620
|
+
* Get all effective permissions for a user in an organization.
|
|
2621
|
+
*
|
|
2622
|
+
* Returns an empty array if the user is not a member.
|
|
2623
|
+
*/
|
|
2624
|
+
getUserPermissions(userId: string, orgId: string): Promise<Permission[]>;
|
|
2625
|
+
/**
|
|
2626
|
+
* Get all effective permissions for a role name.
|
|
2627
|
+
* Includes permissions from inherited roles.
|
|
2628
|
+
*
|
|
2629
|
+
* @throws {RoleNotFoundError} if the role is not defined
|
|
2630
|
+
*/
|
|
2631
|
+
getRolePermissions(roleName: string): Permission[];
|
|
2632
|
+
/**
|
|
2633
|
+
* Check if a role has a specific permission.
|
|
2634
|
+
*/
|
|
2635
|
+
roleHasPermission(roleName: string, permission: Permission): boolean;
|
|
2636
|
+
/**
|
|
2637
|
+
* Register a custom scope resolver for a collection.
|
|
2638
|
+
*
|
|
2639
|
+
* Custom resolvers override the default scope logic for that collection.
|
|
2640
|
+
*/
|
|
2641
|
+
registerScopeResolver(collection: string, resolver: CollectionScopeResolver): void;
|
|
2642
|
+
/**
|
|
2643
|
+
* Resolve sync scopes for a user in an organization.
|
|
2644
|
+
*
|
|
2645
|
+
* The scopes determine what data the user can see and modify during sync.
|
|
2646
|
+
* - Owner/Admin: all org data
|
|
2647
|
+
* - Member: all org data (read + write)
|
|
2648
|
+
* - Viewer: all org data (read-only)
|
|
2649
|
+
* - Billing: no data scopes (billing-only access)
|
|
2650
|
+
*
|
|
2651
|
+
* Custom collection resolvers can override the defaults.
|
|
2652
|
+
*
|
|
2653
|
+
* @param collections - List of collection names to resolve scopes for.
|
|
2654
|
+
* If not provided, only custom-registered collections are included.
|
|
2655
|
+
*/
|
|
2656
|
+
resolveScopes(userId: string, orgId: string, collections?: string[]): Promise<SyncScopes | null>;
|
|
2657
|
+
/**
|
|
2658
|
+
* Get all defined role names.
|
|
2659
|
+
*/
|
|
2660
|
+
getRoleNames(): string[];
|
|
2661
|
+
/**
|
|
2662
|
+
* Get a role definition by name.
|
|
2663
|
+
*/
|
|
2664
|
+
getRoleDefinition(roleName: string): RoleDefinition | null;
|
|
2665
|
+
/**
|
|
2666
|
+
* Validate all role definitions for circular inheritance and unknown references.
|
|
2667
|
+
*/
|
|
2668
|
+
private validateRoles;
|
|
2669
|
+
/**
|
|
2670
|
+
* Detect circular inheritance in role hierarchy.
|
|
2671
|
+
*/
|
|
2672
|
+
private detectCircularInheritance;
|
|
2673
|
+
/**
|
|
2674
|
+
* Recursively resolve all permissions for a role, including inherited permissions.
|
|
2675
|
+
*/
|
|
2676
|
+
private resolvePermissionsForRole;
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Builder for defining custom roles.
|
|
2680
|
+
*
|
|
2681
|
+
* @example
|
|
2682
|
+
* ```typescript
|
|
2683
|
+
* const roles = defineRoles()
|
|
2684
|
+
* .role('viewer', ['*:read'])
|
|
2685
|
+
* .role('editor', ['*:write'], { inherits: ['viewer'] })
|
|
2686
|
+
* .role('admin', ['org:manage-members'], { inherits: ['editor'] })
|
|
2687
|
+
* .build()
|
|
2688
|
+
* ```
|
|
2689
|
+
*/
|
|
2690
|
+
declare function defineRoles(): RoleBuilder;
|
|
2691
|
+
declare class RoleBuilder {
|
|
2692
|
+
private roles;
|
|
2693
|
+
/**
|
|
2694
|
+
* Add a role definition.
|
|
2695
|
+
*/
|
|
2696
|
+
role(name: string, permissions: Permission[], options?: {
|
|
2697
|
+
inherits?: string[];
|
|
2698
|
+
}): RoleBuilder;
|
|
2699
|
+
/**
|
|
2700
|
+
* Include the built-in roles as a base.
|
|
2701
|
+
*/
|
|
2702
|
+
withBuiltInRoles(): RoleBuilder;
|
|
2703
|
+
/**
|
|
2704
|
+
* Build and return the role definitions array.
|
|
2705
|
+
*/
|
|
2706
|
+
build(): RoleDefinition[];
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/**
|
|
2710
|
+
* Resolves sync scopes for org-aware data filtering.
|
|
2711
|
+
*
|
|
2712
|
+
* Given a user, an organization, and a set of collections, this resolver
|
|
2713
|
+
* determines what data the user should receive during sync:
|
|
2714
|
+
*
|
|
2715
|
+
* - **Owner/Admin**: All data in the org (no field-level filtering)
|
|
2716
|
+
* - **Member**: All org data with read/write access
|
|
2717
|
+
* - **Viewer**: All org data with read-only flag
|
|
2718
|
+
* - **Billing**: No data access (empty scopes)
|
|
2719
|
+
*
|
|
2720
|
+
* Developers can register per-collection scope resolvers for fine-grained control.
|
|
2721
|
+
*
|
|
2722
|
+
* @example
|
|
2723
|
+
* ```typescript
|
|
2724
|
+
* const resolver = new OrgScopeResolver(orgStore, rbacEngine)
|
|
2725
|
+
*
|
|
2726
|
+
* // Custom scope: members only see their own todos
|
|
2727
|
+
* resolver.registerCollectionScope('todos', (ctx) => {
|
|
2728
|
+
* if (ctx.role === 'member') {
|
|
2729
|
+
* return { orgId: ctx.orgId, userId: ctx.userId }
|
|
2730
|
+
* }
|
|
2731
|
+
* return { orgId: ctx.orgId } // admins/owners see all
|
|
2732
|
+
* })
|
|
2733
|
+
*
|
|
2734
|
+
* const scopes = await resolver.resolve('user-1', 'org-1', ['todos', 'projects'])
|
|
2735
|
+
* // { todos: { orgId: 'org-1', userId: 'user-1' }, projects: { orgId: 'org-1' } }
|
|
2736
|
+
* ```
|
|
2737
|
+
*/
|
|
2738
|
+
declare class OrgScopeResolver {
|
|
2739
|
+
private readonly orgStore;
|
|
2740
|
+
private readonly rbac;
|
|
2741
|
+
private readonly collectionScopes;
|
|
2742
|
+
constructor(orgStore: OrgStore, rbac: RbacEngine);
|
|
2743
|
+
/**
|
|
2744
|
+
* Register a custom scope resolver for a collection.
|
|
2745
|
+
* This overrides the default orgId-based filtering for that collection.
|
|
2746
|
+
*/
|
|
2747
|
+
registerCollectionScope(collection: string, resolver: CollectionScopeResolver): void;
|
|
2748
|
+
/**
|
|
2749
|
+
* Resolve sync scopes for all specified collections.
|
|
2750
|
+
*
|
|
2751
|
+
* Returns null if the user is not a member of the organization.
|
|
2752
|
+
* Returns an empty object if the user has no data access (e.g., billing role).
|
|
2753
|
+
*/
|
|
2754
|
+
resolve(userId: string, orgId: string, collections: string[]): Promise<SyncScopes | null>;
|
|
2755
|
+
/**
|
|
2756
|
+
* Check if a user can write to a specific collection in an org.
|
|
2757
|
+
*/
|
|
2758
|
+
canWrite(userId: string, orgId: string, collection: string): Promise<boolean>;
|
|
2759
|
+
/**
|
|
2760
|
+
* Check if a user can read from a specific collection in an org.
|
|
2761
|
+
*/
|
|
2762
|
+
canRead(userId: string, orgId: string, collection: string): Promise<boolean>;
|
|
2763
|
+
private resolveCollectionScope;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
/**
|
|
2767
|
+
* Configuration for an OAuth 2.0 provider.
|
|
2768
|
+
*/
|
|
2769
|
+
interface OAuthProviderConfig {
|
|
2770
|
+
/** Provider identifier (e.g., 'google', 'github', 'microsoft') */
|
|
2771
|
+
providerId: string;
|
|
2772
|
+
/** OAuth client ID */
|
|
2773
|
+
clientId: string;
|
|
2774
|
+
/** OAuth client secret */
|
|
2775
|
+
clientSecret: string;
|
|
2776
|
+
/** Authorization endpoint URL */
|
|
2777
|
+
authorizationUrl: string;
|
|
2778
|
+
/** Token exchange endpoint URL */
|
|
2779
|
+
tokenUrl: string;
|
|
2780
|
+
/** User info endpoint URL */
|
|
2781
|
+
userInfoUrl: string;
|
|
2782
|
+
/** OAuth scopes to request */
|
|
2783
|
+
scopes: string[];
|
|
2784
|
+
/** Redirect URI for the callback */
|
|
2785
|
+
redirectUri: string;
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Tokens returned by the OAuth provider after code exchange.
|
|
2789
|
+
*/
|
|
2790
|
+
interface OAuthTokens {
|
|
2791
|
+
/** OAuth access token */
|
|
2792
|
+
accessToken: string;
|
|
2793
|
+
/** Token type (usually 'Bearer') */
|
|
2794
|
+
tokenType: string;
|
|
2795
|
+
/** Access token expiry in seconds (if provided) */
|
|
2796
|
+
expiresIn?: number;
|
|
2797
|
+
/** Refresh token (if provided) */
|
|
2798
|
+
refreshToken?: string;
|
|
2799
|
+
/** ID token (if provided, e.g., OpenID Connect) */
|
|
2800
|
+
idToken?: string;
|
|
2801
|
+
/** Granted scopes (may differ from requested scopes) */
|
|
2802
|
+
scope?: string;
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* User information from the OAuth provider.
|
|
2806
|
+
*/
|
|
2807
|
+
interface OAuthUserInfo {
|
|
2808
|
+
/** Provider-specific user ID */
|
|
2809
|
+
providerId: string;
|
|
2810
|
+
/** Provider name (e.g., 'google', 'github') */
|
|
2811
|
+
provider: string;
|
|
2812
|
+
/** User's email address (may be null if not granted) */
|
|
2813
|
+
email: string | null;
|
|
2814
|
+
/** Whether the email is verified by the provider */
|
|
2815
|
+
emailVerified: boolean;
|
|
2816
|
+
/** User's display name */
|
|
2817
|
+
name: string | null;
|
|
2818
|
+
/** URL to the user's avatar/profile picture */
|
|
2819
|
+
avatarUrl: string | null;
|
|
2820
|
+
/** Raw profile data from the provider */
|
|
2821
|
+
rawProfile: Record<string, unknown>;
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* State stored during the OAuth flow for CSRF protection.
|
|
2825
|
+
*/
|
|
2826
|
+
interface OAuthState {
|
|
2827
|
+
/** Random state parameter for CSRF protection */
|
|
2828
|
+
state: string;
|
|
2829
|
+
/** Provider ID */
|
|
2830
|
+
provider: string;
|
|
2831
|
+
/** Redirect URI used for this flow */
|
|
2832
|
+
redirectUri: string;
|
|
2833
|
+
/** When this state was created (ms since epoch) */
|
|
2834
|
+
createdAt: number;
|
|
2835
|
+
/** When this state expires (ms since epoch) */
|
|
2836
|
+
expiresAt: number;
|
|
2837
|
+
/** Optional: user-defined data to pass through the flow */
|
|
2838
|
+
metadata?: Record<string, unknown>;
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Store for OAuth state parameters.
|
|
2842
|
+
*/
|
|
2843
|
+
interface OAuthStateStore {
|
|
2844
|
+
/** Store a state parameter for later validation. */
|
|
2845
|
+
store(state: OAuthState): Promise<void>;
|
|
2846
|
+
/** Consume a state parameter (single-use). Returns null if not found or expired. */
|
|
2847
|
+
consume(stateValue: string): Promise<OAuthState | null>;
|
|
2848
|
+
/** Clean up expired states. */
|
|
2849
|
+
cleanExpired(): Promise<number>;
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* A linked OAuth identity for a user.
|
|
2853
|
+
* Users can have multiple linked identities (e.g., Google + GitHub).
|
|
2854
|
+
*/
|
|
2855
|
+
interface LinkedIdentity {
|
|
2856
|
+
/** Unique ID of this link */
|
|
2857
|
+
id: string;
|
|
2858
|
+
/** Kora user ID */
|
|
2859
|
+
userId: string;
|
|
2860
|
+
/** OAuth provider name */
|
|
2861
|
+
provider: string;
|
|
2862
|
+
/** Provider-specific user ID */
|
|
2863
|
+
providerUserId: string;
|
|
2864
|
+
/** Provider email (at time of linking) */
|
|
2865
|
+
email: string | null;
|
|
2866
|
+
/** When this identity was linked */
|
|
2867
|
+
linkedAt: number;
|
|
2868
|
+
}
|
|
2869
|
+
declare class OAuthError extends KoraError {
|
|
2870
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
2871
|
+
}
|
|
2872
|
+
declare class OAuthStateMismatchError extends OAuthError {
|
|
2873
|
+
constructor();
|
|
2874
|
+
}
|
|
2875
|
+
declare class OAuthCodeExchangeError extends OAuthError {
|
|
2876
|
+
constructor(details?: string);
|
|
2877
|
+
}
|
|
2878
|
+
declare class OAuthUserInfoError extends OAuthError {
|
|
2879
|
+
constructor(details?: string);
|
|
2880
|
+
}
|
|
2881
|
+
declare class OAuthProviderNotFoundError extends OAuthError {
|
|
2882
|
+
constructor(provider: string);
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
/**
|
|
2886
|
+
* In-memory OAuth state store for development.
|
|
2887
|
+
* Use Redis or a database in production for multi-server deployments.
|
|
2888
|
+
*/
|
|
2889
|
+
declare class InMemoryOAuthStateStore implements OAuthStateStore {
|
|
2890
|
+
private readonly states;
|
|
2891
|
+
store(state: OAuthState): Promise<void>;
|
|
2892
|
+
consume(stateValue: string): Promise<OAuthState | null>;
|
|
2893
|
+
cleanExpired(): Promise<number>;
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Configuration for the OAuth manager.
|
|
2897
|
+
*/
|
|
2898
|
+
interface OAuthManagerConfig {
|
|
2899
|
+
/** Registered OAuth providers */
|
|
2900
|
+
providers: OAuthProviderConfig[];
|
|
2901
|
+
/** State store. Defaults to InMemoryOAuthStateStore. */
|
|
2902
|
+
stateStore?: OAuthStateStore;
|
|
2903
|
+
/** State TTL in milliseconds. Defaults to 10 minutes. */
|
|
2904
|
+
stateTtlMs?: number;
|
|
2905
|
+
/**
|
|
2906
|
+
* Custom fetch function. Defaults to global fetch.
|
|
2907
|
+
* Useful for testing or custom HTTP clients.
|
|
2908
|
+
*/
|
|
2909
|
+
fetch?: typeof globalThis.fetch;
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Manages the OAuth 2.0 authorization code flow.
|
|
2913
|
+
*
|
|
2914
|
+
* Supports any standard OAuth 2.0 / OpenID Connect provider.
|
|
2915
|
+
* Pre-built configurations available for Google, GitHub, and Microsoft.
|
|
2916
|
+
*
|
|
2917
|
+
* @example
|
|
2918
|
+
* ```typescript
|
|
2919
|
+
* const oauth = new OAuthManager({
|
|
2920
|
+
* providers: [
|
|
2921
|
+
* googleProvider({ clientId: '...', clientSecret: '...', redirectUri: '...' }),
|
|
2922
|
+
* githubProvider({ clientId: '...', clientSecret: '...', redirectUri: '...' }),
|
|
2923
|
+
* ],
|
|
2924
|
+
* })
|
|
2925
|
+
*
|
|
2926
|
+
* // Step 1: Generate authorization URL
|
|
2927
|
+
* const { url, state } = await oauth.getAuthorizationUrl('google')
|
|
2928
|
+
* // Redirect user to url...
|
|
2929
|
+
*
|
|
2930
|
+
* // Step 2: Handle callback
|
|
2931
|
+
* const { tokens, userInfo } = await oauth.handleCallback('google', code, stateParam)
|
|
2932
|
+
* ```
|
|
2933
|
+
*/
|
|
2934
|
+
declare class OAuthManager {
|
|
2935
|
+
private readonly providers;
|
|
2936
|
+
private readonly stateStore;
|
|
2937
|
+
private readonly stateTtlMs;
|
|
2938
|
+
private readonly fetchFn;
|
|
2939
|
+
constructor(config: OAuthManagerConfig);
|
|
2940
|
+
/**
|
|
2941
|
+
* Generate an authorization URL for the user to visit.
|
|
2942
|
+
* Returns the URL and the state parameter for CSRF validation.
|
|
2943
|
+
*/
|
|
2944
|
+
getAuthorizationUrl(providerId: string, metadata?: Record<string, unknown>): Promise<{
|
|
2945
|
+
url: string;
|
|
2946
|
+
state: string;
|
|
2947
|
+
}>;
|
|
2948
|
+
/**
|
|
2949
|
+
* Handle the OAuth callback after the user authorizes.
|
|
2950
|
+
* Validates the state parameter, exchanges the code for tokens,
|
|
2951
|
+
* and fetches user info.
|
|
2952
|
+
*
|
|
2953
|
+
* @param providerId - The OAuth provider
|
|
2954
|
+
* @param code - The authorization code from the callback
|
|
2955
|
+
* @param state - The state parameter from the callback
|
|
2956
|
+
* @returns Tokens and user info from the provider
|
|
2957
|
+
*/
|
|
2958
|
+
handleCallback(providerId: string, code: string, state: string): Promise<{
|
|
2959
|
+
tokens: OAuthTokens;
|
|
2960
|
+
userInfo: OAuthUserInfo;
|
|
2961
|
+
stateMetadata?: Record<string, unknown>;
|
|
2962
|
+
}>;
|
|
2963
|
+
/**
|
|
2964
|
+
* Get a registered provider by ID.
|
|
2965
|
+
*/
|
|
2966
|
+
getProvider(providerId: string): OAuthProviderConfig;
|
|
2967
|
+
/**
|
|
2968
|
+
* List all registered provider IDs.
|
|
2969
|
+
*/
|
|
2970
|
+
getProviderIds(): string[];
|
|
2971
|
+
private exchangeCodeForTokens;
|
|
2972
|
+
private fetchUserInfo;
|
|
2973
|
+
}
|
|
2974
|
+
interface ProviderFactoryConfig {
|
|
2975
|
+
clientId: string;
|
|
2976
|
+
clientSecret: string;
|
|
2977
|
+
redirectUri: string;
|
|
2978
|
+
scopes?: string[];
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* Create a Google OAuth provider configuration.
|
|
2982
|
+
*/
|
|
2983
|
+
declare function googleProvider(config: ProviderFactoryConfig): OAuthProviderConfig;
|
|
2984
|
+
/**
|
|
2985
|
+
* Create a GitHub OAuth provider configuration.
|
|
2986
|
+
*/
|
|
2987
|
+
declare function githubProvider(config: ProviderFactoryConfig): OAuthProviderConfig;
|
|
2988
|
+
/**
|
|
2989
|
+
* Create a Microsoft OAuth provider configuration.
|
|
2990
|
+
*/
|
|
2991
|
+
declare function microsoftProvider(config: ProviderFactoryConfig & {
|
|
2992
|
+
tenantId?: string;
|
|
2993
|
+
}): OAuthProviderConfig;
|
|
2994
|
+
|
|
2995
|
+
/**
|
|
2996
|
+
* A user session with metadata.
|
|
2997
|
+
*/
|
|
2998
|
+
interface Session {
|
|
2999
|
+
/** Unique session ID */
|
|
3000
|
+
id: string;
|
|
3001
|
+
/** User ID */
|
|
3002
|
+
userId: string;
|
|
3003
|
+
/** Device ID (if available) */
|
|
3004
|
+
deviceId: string | null;
|
|
3005
|
+
/** IP address of the client (for display, not for security decisions) */
|
|
3006
|
+
ipAddress: string | null;
|
|
3007
|
+
/** User agent string */
|
|
3008
|
+
userAgent: string | null;
|
|
3009
|
+
/** When the session was created */
|
|
3010
|
+
createdAt: number;
|
|
3011
|
+
/** When the session was last active */
|
|
3012
|
+
lastActiveAt: number;
|
|
3013
|
+
/** When the session expires (absolute expiry) */
|
|
3014
|
+
expiresAt: number;
|
|
3015
|
+
/** Whether MFA has been completed for this session */
|
|
3016
|
+
mfaVerified: boolean;
|
|
3017
|
+
/** Custom metadata */
|
|
3018
|
+
metadata?: Record<string, unknown>;
|
|
3019
|
+
}
|
|
3020
|
+
/**
|
|
3021
|
+
* Configuration for the session manager.
|
|
3022
|
+
*/
|
|
3023
|
+
interface SessionManagerConfig {
|
|
3024
|
+
/** Session store implementation */
|
|
3025
|
+
store: SessionStore;
|
|
3026
|
+
/** Session TTL in milliseconds. Default: 7 days */
|
|
3027
|
+
sessionTtlMs?: number;
|
|
3028
|
+
/** Idle timeout in milliseconds. Default: 30 minutes */
|
|
3029
|
+
idleTimeoutMs?: number;
|
|
3030
|
+
/** Maximum concurrent sessions per user. Default: 10 */
|
|
3031
|
+
maxSessionsPerUser?: number;
|
|
3032
|
+
/** Whether to extend session on activity. Default: true */
|
|
3033
|
+
slidingWindow?: boolean;
|
|
3034
|
+
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Parameters for creating a new session.
|
|
3037
|
+
*/
|
|
3038
|
+
interface CreateSessionParams {
|
|
3039
|
+
userId: string;
|
|
3040
|
+
deviceId?: string;
|
|
3041
|
+
ipAddress?: string;
|
|
3042
|
+
userAgent?: string;
|
|
3043
|
+
mfaVerified?: boolean;
|
|
3044
|
+
metadata?: Record<string, unknown>;
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Store for session data.
|
|
3048
|
+
*/
|
|
3049
|
+
interface SessionStore {
|
|
3050
|
+
/** Create a new session */
|
|
3051
|
+
create(session: Session): Promise<void>;
|
|
3052
|
+
/** Get a session by ID */
|
|
3053
|
+
getById(sessionId: string): Promise<Session | null>;
|
|
3054
|
+
/** Update a session */
|
|
3055
|
+
update(session: Session): Promise<void>;
|
|
3056
|
+
/** Delete a session */
|
|
3057
|
+
delete(sessionId: string): Promise<void>;
|
|
3058
|
+
/** Get all sessions for a user */
|
|
3059
|
+
listByUserId(userId: string): Promise<Session[]>;
|
|
3060
|
+
/** Delete all sessions for a user */
|
|
3061
|
+
deleteAllForUser(userId: string): Promise<number>;
|
|
3062
|
+
/** Delete all sessions for a user except one */
|
|
3063
|
+
deleteAllExcept(userId: string, keepSessionId: string): Promise<number>;
|
|
3064
|
+
/** Clean up expired sessions */
|
|
3065
|
+
cleanExpired(): Promise<number>;
|
|
3066
|
+
}
|
|
3067
|
+
declare class SessionError extends KoraError {
|
|
3068
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
3069
|
+
}
|
|
3070
|
+
declare class SessionNotFoundError extends SessionError {
|
|
3071
|
+
constructor(sessionId: string);
|
|
3072
|
+
}
|
|
3073
|
+
declare class SessionExpiredError extends SessionError {
|
|
3074
|
+
constructor(sessionId: string);
|
|
3075
|
+
}
|
|
3076
|
+
declare class SessionLimitExceededError extends SessionError {
|
|
3077
|
+
constructor(userId: string, limit: number);
|
|
3078
|
+
}
|
|
3079
|
+
declare class SessionMfaRequiredError extends SessionError {
|
|
3080
|
+
constructor(sessionId: string);
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* In-memory session store for development and testing.
|
|
3084
|
+
*/
|
|
3085
|
+
declare class InMemorySessionStore implements SessionStore {
|
|
3086
|
+
private readonly sessions;
|
|
3087
|
+
create(session: Session): Promise<void>;
|
|
3088
|
+
getById(sessionId: string): Promise<Session | null>;
|
|
3089
|
+
update(session: Session): Promise<void>;
|
|
3090
|
+
delete(sessionId: string): Promise<void>;
|
|
3091
|
+
listByUserId(userId: string): Promise<Session[]>;
|
|
3092
|
+
deleteAllForUser(userId: string): Promise<number>;
|
|
3093
|
+
deleteAllExcept(userId: string, keepSessionId: string): Promise<number>;
|
|
3094
|
+
cleanExpired(): Promise<number>;
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Manages user sessions with support for:
|
|
3098
|
+
* - Session creation, validation, and revocation
|
|
3099
|
+
* - Sliding window expiry (extends on activity)
|
|
3100
|
+
* - Idle timeout detection
|
|
3101
|
+
* - Max concurrent sessions per user
|
|
3102
|
+
* - MFA verification tracking
|
|
3103
|
+
* - "Sign out everywhere" support
|
|
3104
|
+
*
|
|
3105
|
+
* @example
|
|
3106
|
+
* ```typescript
|
|
3107
|
+
* const sessions = new SessionManager({
|
|
3108
|
+
* store: new InMemorySessionStore(),
|
|
3109
|
+
* sessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
3110
|
+
* idleTimeoutMs: 30 * 60 * 1000, // 30 minutes
|
|
3111
|
+
* maxSessionsPerUser: 5,
|
|
3112
|
+
* })
|
|
3113
|
+
*
|
|
3114
|
+
* // Create session on login
|
|
3115
|
+
* const session = await sessions.create({
|
|
3116
|
+
* userId: 'user-123',
|
|
3117
|
+
* ipAddress: '192.168.1.1',
|
|
3118
|
+
* userAgent: 'Mozilla/5.0...',
|
|
3119
|
+
* })
|
|
3120
|
+
*
|
|
3121
|
+
* // Validate session on each request
|
|
3122
|
+
* const valid = await sessions.validate(session.id)
|
|
3123
|
+
*
|
|
3124
|
+
* // Touch session to extend idle timeout
|
|
3125
|
+
* await sessions.touch(session.id)
|
|
3126
|
+
* ```
|
|
3127
|
+
*/
|
|
3128
|
+
declare class SessionManager {
|
|
3129
|
+
private readonly store;
|
|
3130
|
+
private readonly sessionTtlMs;
|
|
3131
|
+
private readonly idleTimeoutMs;
|
|
3132
|
+
private readonly maxSessionsPerUser;
|
|
3133
|
+
private readonly slidingWindow;
|
|
3134
|
+
constructor(config: SessionManagerConfig);
|
|
3135
|
+
/**
|
|
3136
|
+
* Create a new session for a user.
|
|
3137
|
+
* Enforces the maximum concurrent sessions limit.
|
|
3138
|
+
*/
|
|
3139
|
+
create(params: CreateSessionParams): Promise<Session>;
|
|
3140
|
+
/**
|
|
3141
|
+
* Validate a session.
|
|
3142
|
+
* Returns the session if valid, throws if not found or expired.
|
|
3143
|
+
*/
|
|
3144
|
+
validate(sessionId: string): Promise<Session>;
|
|
3145
|
+
/**
|
|
3146
|
+
* Touch a session to update its last activity time.
|
|
3147
|
+
* If sliding window is enabled, also extends the absolute expiry.
|
|
3148
|
+
*/
|
|
3149
|
+
touch(sessionId: string): Promise<Session>;
|
|
3150
|
+
/**
|
|
3151
|
+
* Mark a session as MFA-verified.
|
|
3152
|
+
*/
|
|
3153
|
+
markMfaVerified(sessionId: string): Promise<Session>;
|
|
3154
|
+
/**
|
|
3155
|
+
* Require MFA verification on a session.
|
|
3156
|
+
* Throws SessionMfaRequiredError if MFA is not verified.
|
|
3157
|
+
*/
|
|
3158
|
+
requireMfa(sessionId: string): Promise<Session>;
|
|
3159
|
+
/**
|
|
3160
|
+
* Revoke (delete) a session.
|
|
3161
|
+
*/
|
|
3162
|
+
revoke(sessionId: string): Promise<void>;
|
|
3163
|
+
/**
|
|
3164
|
+
* Revoke all sessions for a user (sign out everywhere).
|
|
3165
|
+
* Returns the number of sessions revoked.
|
|
3166
|
+
*/
|
|
3167
|
+
revokeAll(userId: string): Promise<number>;
|
|
3168
|
+
/**
|
|
3169
|
+
* Revoke all sessions for a user except the current one.
|
|
3170
|
+
* Returns the number of sessions revoked.
|
|
3171
|
+
*/
|
|
3172
|
+
revokeOthers(userId: string, currentSessionId: string): Promise<number>;
|
|
3173
|
+
/**
|
|
3174
|
+
* List all active sessions for a user.
|
|
3175
|
+
*/
|
|
3176
|
+
listSessions(userId: string): Promise<Session[]>;
|
|
3177
|
+
/**
|
|
3178
|
+
* Clean up expired sessions.
|
|
3179
|
+
* Returns the number of sessions cleaned.
|
|
3180
|
+
*/
|
|
3181
|
+
cleanExpired(): Promise<number>;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
/**
|
|
3185
|
+
* Configuration for TOTP MFA.
|
|
3186
|
+
*/
|
|
3187
|
+
interface TotpConfig {
|
|
3188
|
+
/** Issuer name shown in authenticator apps (e.g., "MyApp") */
|
|
3189
|
+
issuer: string;
|
|
3190
|
+
/** Number of digits in the TOTP code. Default: 6 */
|
|
3191
|
+
digits?: number;
|
|
3192
|
+
/** Time step in seconds. Default: 30 */
|
|
3193
|
+
period?: number;
|
|
3194
|
+
/** Hash algorithm. Default: 'SHA-1' (most compatible with authenticator apps) */
|
|
3195
|
+
algorithm?: 'SHA-1' | 'SHA-256' | 'SHA-512';
|
|
3196
|
+
/** Number of time windows to check before/after current. Default: 1 */
|
|
3197
|
+
window?: number;
|
|
3198
|
+
/** Number of recovery codes to generate. Default: 8 */
|
|
3199
|
+
recoveryCodes?: number;
|
|
3200
|
+
}
|
|
3201
|
+
/**
|
|
3202
|
+
* A TOTP secret with metadata for a user.
|
|
3203
|
+
*/
|
|
3204
|
+
interface TotpSecret {
|
|
3205
|
+
/** User ID */
|
|
3206
|
+
userId: string;
|
|
3207
|
+
/** Raw secret bytes (base32-encoded for display) */
|
|
3208
|
+
secret: string;
|
|
3209
|
+
/** Whether MFA is verified (user has confirmed setup with a valid code) */
|
|
3210
|
+
verified: boolean;
|
|
3211
|
+
/** Hashed recovery codes (unused ones only) */
|
|
3212
|
+
recoveryCodes: string[];
|
|
3213
|
+
/** When this secret was created */
|
|
3214
|
+
createdAt: number;
|
|
3215
|
+
/** When MFA was verified (confirmed with first valid code) */
|
|
3216
|
+
verifiedAt: number | null;
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Setup result returned when enabling TOTP MFA.
|
|
3220
|
+
*/
|
|
3221
|
+
interface TotpSetupResult {
|
|
3222
|
+
/** The raw secret in base32 encoding (for manual entry) */
|
|
3223
|
+
secret: string;
|
|
3224
|
+
/** otpauth:// URI for QR code generation */
|
|
3225
|
+
uri: string;
|
|
3226
|
+
/** Plaintext recovery codes (shown once, then discarded) */
|
|
3227
|
+
recoveryCodes: string[];
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Store for TOTP secrets.
|
|
3231
|
+
*/
|
|
3232
|
+
interface TotpStore {
|
|
3233
|
+
/** Save or update a TOTP secret for a user */
|
|
3234
|
+
save(secret: TotpSecret): Promise<void>;
|
|
3235
|
+
/** Get a TOTP secret by user ID */
|
|
3236
|
+
getByUserId(userId: string): Promise<TotpSecret | null>;
|
|
3237
|
+
/** Delete TOTP secret for a user (disable MFA) */
|
|
3238
|
+
delete(userId: string): Promise<void>;
|
|
3239
|
+
}
|
|
3240
|
+
declare class TotpError extends KoraError {
|
|
3241
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
3242
|
+
}
|
|
3243
|
+
declare class TotpInvalidCodeError extends TotpError {
|
|
3244
|
+
constructor();
|
|
3245
|
+
}
|
|
3246
|
+
declare class TotpNotEnabledError extends TotpError {
|
|
3247
|
+
constructor(userId: string);
|
|
3248
|
+
}
|
|
3249
|
+
declare class TotpAlreadyEnabledError extends TotpError {
|
|
3250
|
+
constructor(userId: string);
|
|
3251
|
+
}
|
|
3252
|
+
declare class TotpNotVerifiedError extends TotpError {
|
|
3253
|
+
constructor(userId: string);
|
|
3254
|
+
}
|
|
3255
|
+
declare class TotpRecoveryExhaustedError extends TotpError {
|
|
3256
|
+
constructor();
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* In-memory TOTP store for development and testing.
|
|
3260
|
+
*/
|
|
3261
|
+
declare class InMemoryTotpStore implements TotpStore {
|
|
3262
|
+
private readonly secrets;
|
|
3263
|
+
save(secret: TotpSecret): Promise<void>;
|
|
3264
|
+
getByUserId(userId: string): Promise<TotpSecret | null>;
|
|
3265
|
+
delete(userId: string): Promise<void>;
|
|
3266
|
+
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Manages TOTP-based Multi-Factor Authentication.
|
|
3269
|
+
*
|
|
3270
|
+
* Implements RFC 6238 (TOTP) and RFC 4226 (HOTP) with Web Crypto API.
|
|
3271
|
+
* Compatible with Google Authenticator, Authy, 1Password, and other
|
|
3272
|
+
* TOTP-compatible authenticator apps.
|
|
3273
|
+
*
|
|
3274
|
+
* @example
|
|
3275
|
+
* ```typescript
|
|
3276
|
+
* const totp = new TotpManager({
|
|
3277
|
+
* issuer: 'MyApp',
|
|
3278
|
+
* store: new InMemoryTotpStore(),
|
|
3279
|
+
* })
|
|
3280
|
+
*
|
|
3281
|
+
* // Step 1: Enable MFA (returns QR code URI and recovery codes)
|
|
3282
|
+
* const setup = await totp.enable('user-123', 'alice@example.com')
|
|
3283
|
+
* // Show setup.uri as QR code, show setup.recoveryCodes once
|
|
3284
|
+
*
|
|
3285
|
+
* // Step 2: Verify setup with a code from authenticator app
|
|
3286
|
+
* await totp.verifySetup('user-123', '123456')
|
|
3287
|
+
*
|
|
3288
|
+
* // Step 3: On login, verify TOTP code
|
|
3289
|
+
* const valid = await totp.verify('user-123', '654321')
|
|
3290
|
+
* ```
|
|
3291
|
+
*/
|
|
3292
|
+
declare class TotpManager {
|
|
3293
|
+
private readonly store;
|
|
3294
|
+
private readonly issuer;
|
|
3295
|
+
private readonly digits;
|
|
3296
|
+
private readonly period;
|
|
3297
|
+
private readonly algorithm;
|
|
3298
|
+
private readonly window;
|
|
3299
|
+
private readonly recoveryCodeCount;
|
|
3300
|
+
constructor(config: TotpConfig & {
|
|
3301
|
+
store: TotpStore;
|
|
3302
|
+
});
|
|
3303
|
+
/**
|
|
3304
|
+
* Enable TOTP MFA for a user.
|
|
3305
|
+
* Returns the secret URI (for QR code) and recovery codes.
|
|
3306
|
+
* The user must verify setup with a valid code before MFA is active.
|
|
3307
|
+
*/
|
|
3308
|
+
enable(userId: string, accountName: string): Promise<TotpSetupResult>;
|
|
3309
|
+
/**
|
|
3310
|
+
* Verify the TOTP setup by confirming the user can generate a valid code.
|
|
3311
|
+
* Must be called after `enable()` and before MFA is enforced.
|
|
3312
|
+
*/
|
|
3313
|
+
verifySetup(userId: string, code: string): Promise<boolean>;
|
|
3314
|
+
/**
|
|
3315
|
+
* Verify a TOTP code during login.
|
|
3316
|
+
* Returns true if the code is valid, false otherwise.
|
|
3317
|
+
*/
|
|
3318
|
+
verify(userId: string, code: string): Promise<boolean>;
|
|
3319
|
+
/**
|
|
3320
|
+
* Verify a recovery code as an alternative to TOTP.
|
|
3321
|
+
* Recovery codes are single-use.
|
|
3322
|
+
*/
|
|
3323
|
+
verifyRecoveryCode(userId: string, recoveryCode: string): Promise<boolean>;
|
|
3324
|
+
/**
|
|
3325
|
+
* Regenerate recovery codes. Requires a valid TOTP code for authorization.
|
|
3326
|
+
* Replaces all existing recovery codes.
|
|
3327
|
+
*/
|
|
3328
|
+
regenerateRecoveryCodes(userId: string, totpCode: string): Promise<string[]>;
|
|
3329
|
+
/**
|
|
3330
|
+
* Disable TOTP MFA for a user.
|
|
3331
|
+
* Requires a valid TOTP code or recovery code for authorization.
|
|
3332
|
+
*/
|
|
3333
|
+
disable(userId: string, code: string): Promise<void>;
|
|
3334
|
+
/**
|
|
3335
|
+
* Check if a user has TOTP MFA enabled and verified.
|
|
3336
|
+
*/
|
|
3337
|
+
isEnabled(userId: string): Promise<boolean>;
|
|
3338
|
+
/**
|
|
3339
|
+
* Get the number of remaining recovery codes for a user.
|
|
3340
|
+
*/
|
|
3341
|
+
remainingRecoveryCodes(userId: string): Promise<number>;
|
|
3342
|
+
private validateCode;
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Encode bytes to base32 string (RFC 4648, no padding).
|
|
3346
|
+
*/
|
|
3347
|
+
declare function base32Encode(data: Uint8Array): string;
|
|
3348
|
+
/**
|
|
3349
|
+
* Decode a base32 string to bytes (RFC 4648).
|
|
3350
|
+
*/
|
|
3351
|
+
declare function base32Decode(encoded: string): Uint8Array;
|
|
3352
|
+
|
|
3353
|
+
/**
|
|
3354
|
+
* Actions that can be audited.
|
|
3355
|
+
*/
|
|
3356
|
+
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"];
|
|
3357
|
+
type AuditAction = (typeof AUDIT_ACTIONS)[number];
|
|
3358
|
+
/**
|
|
3359
|
+
* An audit log entry.
|
|
3360
|
+
*/
|
|
3361
|
+
interface AuditEntry {
|
|
3362
|
+
/** Unique entry ID */
|
|
3363
|
+
id: string;
|
|
3364
|
+
/** When this event occurred */
|
|
3365
|
+
timestamp: number;
|
|
3366
|
+
/** The action that was performed */
|
|
3367
|
+
action: AuditAction;
|
|
3368
|
+
/** Who performed the action (user ID, "system", or "anonymous") */
|
|
3369
|
+
actorId: string;
|
|
3370
|
+
/** The type of actor */
|
|
3371
|
+
actorType: 'user' | 'admin' | 'system';
|
|
3372
|
+
/** The target of the action (e.g., user ID, org ID, session ID) */
|
|
3373
|
+
targetId: string | null;
|
|
3374
|
+
/** The type of target */
|
|
3375
|
+
targetType: string | null;
|
|
3376
|
+
/** IP address of the actor */
|
|
3377
|
+
ipAddress: string | null;
|
|
3378
|
+
/** User agent */
|
|
3379
|
+
userAgent: string | null;
|
|
3380
|
+
/** Whether the action succeeded */
|
|
3381
|
+
success: boolean;
|
|
3382
|
+
/** Error message if the action failed */
|
|
3383
|
+
errorMessage: string | null;
|
|
3384
|
+
/** Additional structured context */
|
|
3385
|
+
metadata?: Record<string, unknown>;
|
|
3386
|
+
}
|
|
3387
|
+
/**
|
|
3388
|
+
* Query parameters for searching audit logs.
|
|
3389
|
+
*/
|
|
3390
|
+
interface AuditLogQuery {
|
|
3391
|
+
/** Filter by actor ID */
|
|
3392
|
+
actorId?: string;
|
|
3393
|
+
/** Filter by target ID */
|
|
3394
|
+
targetId?: string;
|
|
3395
|
+
/** Filter by action(s) */
|
|
3396
|
+
actions?: AuditAction[];
|
|
3397
|
+
/** Filter by success/failure */
|
|
3398
|
+
success?: boolean;
|
|
3399
|
+
/** Start time (inclusive) */
|
|
3400
|
+
startTime?: number;
|
|
3401
|
+
/** End time (inclusive) */
|
|
3402
|
+
endTime?: number;
|
|
3403
|
+
/** Maximum number of entries to return */
|
|
3404
|
+
limit?: number;
|
|
3405
|
+
/** Offset for pagination */
|
|
3406
|
+
offset?: number;
|
|
3407
|
+
}
|
|
3408
|
+
/**
|
|
3409
|
+
* Store for audit log entries.
|
|
3410
|
+
*/
|
|
3411
|
+
interface AuditLogStore {
|
|
3412
|
+
/** Append an audit entry */
|
|
3413
|
+
append(entry: AuditEntry): Promise<void>;
|
|
3414
|
+
/** Query audit entries */
|
|
3415
|
+
query(params: AuditLogQuery): Promise<AuditEntry[]>;
|
|
3416
|
+
/** Count matching entries */
|
|
3417
|
+
count(params: AuditLogQuery): Promise<number>;
|
|
3418
|
+
/** Delete entries older than a given timestamp */
|
|
3419
|
+
purgeOlderThan(timestamp: number): Promise<number>;
|
|
3420
|
+
}
|
|
3421
|
+
declare class AuditLogError extends KoraError {
|
|
3422
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* In-memory audit log store for development and testing.
|
|
3426
|
+
*/
|
|
3427
|
+
declare class InMemoryAuditLogStore implements AuditLogStore {
|
|
3428
|
+
private readonly entries;
|
|
3429
|
+
append(entry: AuditEntry): Promise<void>;
|
|
3430
|
+
query(params: AuditLogQuery): Promise<AuditEntry[]>;
|
|
3431
|
+
count(params: AuditLogQuery): Promise<number>;
|
|
3432
|
+
purgeOlderThan(timestamp: number): Promise<number>;
|
|
3433
|
+
private filterEntries;
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Structured audit logger for authentication events.
|
|
3437
|
+
*
|
|
3438
|
+
* Records all security-relevant actions for compliance and debugging.
|
|
3439
|
+
* Designed to be plugged into the auth system at key decision points.
|
|
3440
|
+
*
|
|
3441
|
+
* @example
|
|
3442
|
+
* ```typescript
|
|
3443
|
+
* const auditLog = new AuditLogger({
|
|
3444
|
+
* store: new InMemoryAuditLogStore(),
|
|
3445
|
+
* })
|
|
3446
|
+
*
|
|
3447
|
+
* await auditLog.log({
|
|
3448
|
+
* action: 'user.signin',
|
|
3449
|
+
* actorId: 'user-123',
|
|
3450
|
+
* actorType: 'user',
|
|
3451
|
+
* success: true,
|
|
3452
|
+
* ipAddress: '192.168.1.1',
|
|
3453
|
+
* })
|
|
3454
|
+
*
|
|
3455
|
+
* const entries = await auditLog.query({ actorId: 'user-123', limit: 50 })
|
|
3456
|
+
* ```
|
|
3457
|
+
*/
|
|
3458
|
+
declare class AuditLogger {
|
|
3459
|
+
private readonly store;
|
|
3460
|
+
private readonly retentionMs;
|
|
3461
|
+
constructor(config: {
|
|
3462
|
+
store: AuditLogStore;
|
|
3463
|
+
retentionDays?: number;
|
|
3464
|
+
});
|
|
3465
|
+
/**
|
|
3466
|
+
* Log an audit event.
|
|
3467
|
+
*/
|
|
3468
|
+
log(params: {
|
|
3469
|
+
action: AuditAction;
|
|
3470
|
+
actorId: string;
|
|
3471
|
+
actorType?: 'user' | 'admin' | 'system';
|
|
3472
|
+
targetId?: string;
|
|
3473
|
+
targetType?: string;
|
|
3474
|
+
ipAddress?: string;
|
|
3475
|
+
userAgent?: string;
|
|
3476
|
+
success?: boolean;
|
|
3477
|
+
errorMessage?: string;
|
|
3478
|
+
metadata?: Record<string, unknown>;
|
|
3479
|
+
}): Promise<AuditEntry>;
|
|
3480
|
+
/**
|
|
3481
|
+
* Query audit log entries.
|
|
3482
|
+
*/
|
|
3483
|
+
query(params: AuditLogQuery): Promise<AuditEntry[]>;
|
|
3484
|
+
/**
|
|
3485
|
+
* Count matching audit entries.
|
|
3486
|
+
*/
|
|
3487
|
+
count(params: AuditLogQuery): Promise<number>;
|
|
3488
|
+
/**
|
|
3489
|
+
* Purge old entries based on retention policy.
|
|
3490
|
+
* Returns the number of entries purged.
|
|
3491
|
+
*/
|
|
3492
|
+
purge(): Promise<number>;
|
|
3493
|
+
/**
|
|
3494
|
+
* Get recent activity for a user.
|
|
3495
|
+
*/
|
|
3496
|
+
getUserActivity(userId: string, limit?: number): Promise<AuditEntry[]>;
|
|
3497
|
+
/**
|
|
3498
|
+
* Get failed login attempts for a user within a time window.
|
|
3499
|
+
*/
|
|
3500
|
+
getFailedLogins(userId: string, windowMs: number): Promise<AuditEntry[]>;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
/**
|
|
3504
|
+
* Configuration for the Admin API.
|
|
3505
|
+
*/
|
|
3506
|
+
interface AdminApiConfig {
|
|
3507
|
+
/** User store for managing users */
|
|
3508
|
+
userStore: InMemoryUserStore;
|
|
3509
|
+
/** Session store for managing sessions (optional) */
|
|
3510
|
+
sessionStore?: SessionStore;
|
|
3511
|
+
/** Audit logger (optional) */
|
|
3512
|
+
auditLogger?: AuditLogger;
|
|
3513
|
+
}
|
|
3514
|
+
/**
|
|
3515
|
+
* Paginated result set.
|
|
3516
|
+
*/
|
|
3517
|
+
interface PaginatedResult<T> {
|
|
3518
|
+
data: T[];
|
|
3519
|
+
total: number;
|
|
3520
|
+
limit: number;
|
|
3521
|
+
offset: number;
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* User list query parameters.
|
|
3525
|
+
*/
|
|
3526
|
+
interface UserListQuery {
|
|
3527
|
+
/** Search by email (substring match) */
|
|
3528
|
+
email?: string;
|
|
3529
|
+
/** Filter by email verified status */
|
|
3530
|
+
emailVerified?: boolean;
|
|
3531
|
+
/** Maximum number of results */
|
|
3532
|
+
limit?: number;
|
|
3533
|
+
/** Offset for pagination */
|
|
3534
|
+
offset?: number;
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Admin-level user update (different from self-update).
|
|
3538
|
+
*/
|
|
3539
|
+
interface AdminUserUpdate {
|
|
3540
|
+
/** Update display name */
|
|
3541
|
+
name?: string;
|
|
3542
|
+
/** Set email verified status */
|
|
3543
|
+
emailVerified?: boolean;
|
|
3544
|
+
/** Update email (admin override) */
|
|
3545
|
+
email?: string;
|
|
3546
|
+
}
|
|
3547
|
+
declare class AdminApiError extends KoraError {
|
|
3548
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
3549
|
+
}
|
|
3550
|
+
declare class AdminUserNotFoundError extends AdminApiError {
|
|
3551
|
+
constructor(userId: string);
|
|
3552
|
+
}
|
|
3553
|
+
declare class AdminUnauthorizedError extends AdminApiError {
|
|
3554
|
+
constructor();
|
|
3555
|
+
}
|
|
3556
|
+
/**
|
|
3557
|
+
* Administrative API for managing users, sessions, and system configuration.
|
|
3558
|
+
*
|
|
3559
|
+
* Provides elevated operations that should only be accessible to administrators.
|
|
3560
|
+
* All operations are audit-logged when an AuditLogger is configured.
|
|
3561
|
+
*
|
|
3562
|
+
* @example
|
|
3563
|
+
* ```typescript
|
|
3564
|
+
* const admin = new AdminApi({
|
|
3565
|
+
* userStore: myUserStore,
|
|
3566
|
+
* sessionStore: mySessionStore,
|
|
3567
|
+
* auditLogger: myAuditLogger,
|
|
3568
|
+
* })
|
|
3569
|
+
*
|
|
3570
|
+
* // List users
|
|
3571
|
+
* const { data, total } = await admin.listUsers({ limit: 20 })
|
|
3572
|
+
*
|
|
3573
|
+
* // Suspend a user (revokes all sessions)
|
|
3574
|
+
* await admin.suspendUser('admin-user-id', 'target-user-id', 'Policy violation')
|
|
3575
|
+
* ```
|
|
3576
|
+
*/
|
|
3577
|
+
declare class AdminApi {
|
|
3578
|
+
private readonly userStore;
|
|
3579
|
+
private readonly sessionStore;
|
|
3580
|
+
private readonly auditLogger;
|
|
3581
|
+
constructor(config: AdminApiConfig);
|
|
3582
|
+
/**
|
|
3583
|
+
* Get a user by ID with full details.
|
|
3584
|
+
*/
|
|
3585
|
+
getUser(adminId: string, userId: string): Promise<AuthUser>;
|
|
3586
|
+
/**
|
|
3587
|
+
* List users with optional filtering and pagination.
|
|
3588
|
+
*/
|
|
3589
|
+
listUsers(query?: UserListQuery): Promise<PaginatedResult<AuthUser>>;
|
|
3590
|
+
/**
|
|
3591
|
+
* Update a user's profile (admin-level).
|
|
3592
|
+
*/
|
|
3593
|
+
updateUser(adminId: string, userId: string, updates: AdminUserUpdate): Promise<AuthUser>;
|
|
3594
|
+
/**
|
|
3595
|
+
* Delete a user and all associated sessions.
|
|
3596
|
+
*/
|
|
3597
|
+
deleteUser(adminId: string, userId: string): Promise<void>;
|
|
3598
|
+
/**
|
|
3599
|
+
* Get all active sessions for a user.
|
|
3600
|
+
*/
|
|
3601
|
+
getUserSessions(userId: string): Promise<Session[]>;
|
|
3602
|
+
/**
|
|
3603
|
+
* Revoke all sessions for a user.
|
|
3604
|
+
*/
|
|
3605
|
+
revokeUserSessions(adminId: string, userId: string): Promise<number>;
|
|
3606
|
+
/**
|
|
3607
|
+
* Revoke a specific session.
|
|
3608
|
+
*/
|
|
3609
|
+
revokeSession(adminId: string, sessionId: string): Promise<void>;
|
|
3610
|
+
/**
|
|
3611
|
+
* Get system statistics.
|
|
3612
|
+
*/
|
|
3613
|
+
getStats(): Promise<{
|
|
3614
|
+
totalUsers: number;
|
|
3615
|
+
verifiedUsers: number;
|
|
3616
|
+
unverifiedUsers: number;
|
|
3617
|
+
}>;
|
|
3618
|
+
private audit;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
/**
|
|
3622
|
+
* Events that can trigger webhooks.
|
|
3623
|
+
*/
|
|
3624
|
+
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"];
|
|
3625
|
+
type WebhookEvent = (typeof WEBHOOK_EVENTS)[number];
|
|
3626
|
+
/**
|
|
3627
|
+
* A webhook endpoint configuration.
|
|
3628
|
+
*/
|
|
3629
|
+
interface WebhookEndpoint {
|
|
3630
|
+
/** Unique endpoint ID */
|
|
3631
|
+
id: string;
|
|
3632
|
+
/** Destination URL */
|
|
3633
|
+
url: string;
|
|
3634
|
+
/** Events this endpoint subscribes to */
|
|
3635
|
+
events: WebhookEvent[];
|
|
3636
|
+
/** Signing secret for HMAC verification */
|
|
3637
|
+
secret: string;
|
|
3638
|
+
/** Whether this endpoint is active */
|
|
3639
|
+
active: boolean;
|
|
3640
|
+
/** When this endpoint was created */
|
|
3641
|
+
createdAt: number;
|
|
3642
|
+
/** Custom metadata */
|
|
3643
|
+
metadata?: Record<string, unknown>;
|
|
3644
|
+
}
|
|
3645
|
+
/**
|
|
3646
|
+
* A webhook delivery attempt.
|
|
3647
|
+
*/
|
|
3648
|
+
interface WebhookDelivery {
|
|
3649
|
+
/** Unique delivery ID */
|
|
3650
|
+
id: string;
|
|
3651
|
+
/** Endpoint ID */
|
|
3652
|
+
endpointId: string;
|
|
3653
|
+
/** Event that triggered this delivery */
|
|
3654
|
+
event: WebhookEvent;
|
|
3655
|
+
/** Request payload (JSON) */
|
|
3656
|
+
payload: string;
|
|
3657
|
+
/** HTTP response status (null if network error) */
|
|
3658
|
+
responseStatus: number | null;
|
|
3659
|
+
/** Whether the delivery was successful (2xx response) */
|
|
3660
|
+
success: boolean;
|
|
3661
|
+
/** Error message if delivery failed */
|
|
3662
|
+
error: string | null;
|
|
3663
|
+
/** Number of attempts made */
|
|
3664
|
+
attempts: number;
|
|
3665
|
+
/** When the delivery was first attempted */
|
|
3666
|
+
createdAt: number;
|
|
3667
|
+
/** When the delivery was last attempted */
|
|
3668
|
+
lastAttemptAt: number;
|
|
3669
|
+
}
|
|
3670
|
+
/**
|
|
3671
|
+
* Payload sent to webhook endpoints.
|
|
3672
|
+
*/
|
|
3673
|
+
interface WebhookPayload {
|
|
3674
|
+
/** Unique event ID */
|
|
3675
|
+
id: string;
|
|
3676
|
+
/** Event type */
|
|
3677
|
+
event: WebhookEvent;
|
|
3678
|
+
/** When the event occurred */
|
|
3679
|
+
timestamp: number;
|
|
3680
|
+
/** Event data */
|
|
3681
|
+
data: Record<string, unknown>;
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* Store for webhook endpoints and deliveries.
|
|
3685
|
+
*/
|
|
3686
|
+
interface WebhookStore {
|
|
3687
|
+
/** Save a webhook endpoint */
|
|
3688
|
+
saveEndpoint(endpoint: WebhookEndpoint): Promise<void>;
|
|
3689
|
+
/** Get an endpoint by ID */
|
|
3690
|
+
getEndpoint(id: string): Promise<WebhookEndpoint | null>;
|
|
3691
|
+
/** List all endpoints */
|
|
3692
|
+
listEndpoints(): Promise<WebhookEndpoint[]>;
|
|
3693
|
+
/** Delete an endpoint */
|
|
3694
|
+
deleteEndpoint(id: string): Promise<void>;
|
|
3695
|
+
/** Save a delivery record */
|
|
3696
|
+
saveDelivery(delivery: WebhookDelivery): Promise<void>;
|
|
3697
|
+
/** List recent deliveries for an endpoint */
|
|
3698
|
+
listDeliveries(endpointId: string, limit?: number): Promise<WebhookDelivery[]>;
|
|
3699
|
+
}
|
|
3700
|
+
declare class WebhookError extends KoraError {
|
|
3701
|
+
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
3702
|
+
}
|
|
3703
|
+
declare class WebhookEndpointNotFoundError extends WebhookError {
|
|
3704
|
+
constructor(id: string);
|
|
3705
|
+
}
|
|
3706
|
+
declare class InMemoryWebhookStore implements WebhookStore {
|
|
3707
|
+
private readonly endpoints;
|
|
3708
|
+
private readonly deliveries;
|
|
3709
|
+
saveEndpoint(endpoint: WebhookEndpoint): Promise<void>;
|
|
3710
|
+
getEndpoint(id: string): Promise<WebhookEndpoint | null>;
|
|
3711
|
+
listEndpoints(): Promise<WebhookEndpoint[]>;
|
|
3712
|
+
deleteEndpoint(id: string): Promise<void>;
|
|
3713
|
+
saveDelivery(delivery: WebhookDelivery): Promise<void>;
|
|
3714
|
+
listDeliveries(endpointId: string, limit?: number): Promise<WebhookDelivery[]>;
|
|
3715
|
+
}
|
|
3716
|
+
/**
|
|
3717
|
+
* Manages webhook registrations and event delivery.
|
|
3718
|
+
*
|
|
3719
|
+
* Sends HTTP POST requests to registered endpoints when auth events occur.
|
|
3720
|
+
* Includes HMAC-SHA256 signature verification and retry logic.
|
|
3721
|
+
*
|
|
3722
|
+
* @example
|
|
3723
|
+
* ```typescript
|
|
3724
|
+
* const webhooks = new WebhookManager({
|
|
3725
|
+
* store: new InMemoryWebhookStore(),
|
|
3726
|
+
* })
|
|
3727
|
+
*
|
|
3728
|
+
* // Register an endpoint
|
|
3729
|
+
* const endpoint = await webhooks.register({
|
|
3730
|
+
* url: 'https://myapp.com/webhooks/auth',
|
|
3731
|
+
* events: ['user.created', 'user.signin'],
|
|
3732
|
+
* })
|
|
3733
|
+
*
|
|
3734
|
+
* // Dispatch an event (usually called internally by auth system)
|
|
3735
|
+
* await webhooks.dispatch('user.created', { userId: 'user-123', email: 'alice@example.com' })
|
|
3736
|
+
* ```
|
|
3737
|
+
*/
|
|
3738
|
+
declare class WebhookManager {
|
|
3739
|
+
private readonly store;
|
|
3740
|
+
private readonly fetchFn;
|
|
3741
|
+
constructor(config: {
|
|
3742
|
+
store: WebhookStore;
|
|
3743
|
+
fetch?: typeof globalThis.fetch;
|
|
3744
|
+
});
|
|
3745
|
+
/**
|
|
3746
|
+
* Register a new webhook endpoint.
|
|
3747
|
+
*/
|
|
3748
|
+
register(params: {
|
|
3749
|
+
url: string;
|
|
3750
|
+
events: WebhookEvent[];
|
|
3751
|
+
metadata?: Record<string, unknown>;
|
|
3752
|
+
}): Promise<WebhookEndpoint>;
|
|
3753
|
+
/**
|
|
3754
|
+
* Update a webhook endpoint.
|
|
3755
|
+
*/
|
|
3756
|
+
update(id: string, updates: {
|
|
3757
|
+
url?: string;
|
|
3758
|
+
events?: WebhookEvent[];
|
|
3759
|
+
active?: boolean;
|
|
3760
|
+
}): Promise<WebhookEndpoint>;
|
|
3761
|
+
/**
|
|
3762
|
+
* Delete a webhook endpoint.
|
|
3763
|
+
*/
|
|
3764
|
+
remove(id: string): Promise<void>;
|
|
3765
|
+
/**
|
|
3766
|
+
* List all webhook endpoints.
|
|
3767
|
+
*/
|
|
3768
|
+
list(): Promise<WebhookEndpoint[]>;
|
|
3769
|
+
/**
|
|
3770
|
+
* Get a specific endpoint.
|
|
3771
|
+
*/
|
|
3772
|
+
get(id: string): Promise<WebhookEndpoint>;
|
|
3773
|
+
/**
|
|
3774
|
+
* Get recent deliveries for an endpoint.
|
|
3775
|
+
*/
|
|
3776
|
+
getDeliveries(endpointId: string, limit?: number): Promise<WebhookDelivery[]>;
|
|
3777
|
+
/**
|
|
3778
|
+
* Dispatch an event to all matching webhook endpoints.
|
|
3779
|
+
* Delivery is best-effort with retries.
|
|
3780
|
+
*/
|
|
3781
|
+
dispatch(event: WebhookEvent, data: Record<string, unknown>): Promise<void>;
|
|
3782
|
+
private deliverToEndpoint;
|
|
3783
|
+
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Verify a webhook payload signature.
|
|
3786
|
+
* Useful for consumers of webhooks to verify authenticity.
|
|
3787
|
+
*/
|
|
3788
|
+
declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
|
|
3789
|
+
|
|
3790
|
+
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, 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, 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, VerificationTokenExpiredError, VerificationTokenNotFoundError, type WebhookDelivery, type WebhookEndpoint, WebhookEndpointNotFoundError, WebhookError, type WebhookEvent, WebhookManager, type WebhookPayload, type WebhookStore, base32Decode, base32Encode, createClerkAdapter, createSupabaseAdapter, decodeJwt, defineRoles, encodeJwt, generateAuthenticationOptions, generateRegistrationOptions, githubProvider, googleProvider, hasRoleLevel, hashPassword, isExpired, microsoftProvider, parsePermission, permissionCovers, verifyAuthenticationResponse, verifyJwt, verifyPassword, verifyRegistrationResponse, verifyWebhookSignature };
|