@korajs/auth 0.1.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 +156 -0
- package/dist/auth-client-CrDNuh10.d.cts +181 -0
- package/dist/auth-client-CrDNuh10.d.ts +181 -0
- package/dist/chunk-L554ZDPY.js +174 -0
- package/dist/chunk-L554ZDPY.js.map +1 -0
- package/dist/device-identity-DiwdLsUB.d.cts +247 -0
- package/dist/device-identity-DiwdLsUB.d.ts +247 -0
- package/dist/index.cjs +740 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +552 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +206 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +189 -0
- package/dist/react.d.ts +189 -0
- package/dist/react.js +175 -0
- package/dist/react.js.map +1 -0
- package/dist/server.cjs +1040 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +780 -0
- package/dist/server.d.ts +780 -0
- package/dist/server.js +890 -0
- package/dist/server.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import { A as AuthTokens, T as TokenPayload } from './device-identity-DiwdLsUB.cjs';
|
|
2
|
+
export { f as computePublicKeyThumbprint, v as verifyChallenge } from './device-identity-DiwdLsUB.cjs';
|
|
3
|
+
import { KoraError } from '@korajs/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the server-side TokenManager.
|
|
7
|
+
*/
|
|
8
|
+
interface TokenManagerConfig {
|
|
9
|
+
/** Secret key for signing JWTs (HMAC-SHA256) */
|
|
10
|
+
secret: string;
|
|
11
|
+
/** Access token lifetime in milliseconds (default: 15 minutes) */
|
|
12
|
+
accessTokenLifetime?: number;
|
|
13
|
+
/** Refresh token lifetime in milliseconds (default: 90 days) */
|
|
14
|
+
refreshTokenLifetime?: number;
|
|
15
|
+
/** Device credential lifetime in milliseconds (default: 90 days) */
|
|
16
|
+
deviceCredentialLifetime?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Server-side token manager responsible for issuing, refreshing, and validating
|
|
20
|
+
* Kora authentication tokens.
|
|
21
|
+
*
|
|
22
|
+
* Uses HMAC-SHA256 signed JWTs. Supports three token types:
|
|
23
|
+
* - **Access tokens**: Short-lived tokens for API authorization (default: 15 min)
|
|
24
|
+
* - **Refresh tokens**: Longer-lived tokens for obtaining new access tokens (default: 90 days)
|
|
25
|
+
* - **Device credentials**: Long-lived credentials bound to a device key pair (default: 90 days)
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const tokenManager = new TokenManager({ secret: 'my-signing-secret' })
|
|
30
|
+
*
|
|
31
|
+
* // Issue all tokens at once
|
|
32
|
+
* const tokens = tokenManager.issueTokens('user-123', 'device-456')
|
|
33
|
+
*
|
|
34
|
+
* // Validate an access token
|
|
35
|
+
* const payload = tokenManager.validateToken(tokens.accessToken)
|
|
36
|
+
*
|
|
37
|
+
* // Refresh when the access token expires
|
|
38
|
+
* const newTokens = tokenManager.refreshAccessToken(tokens.refreshToken)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare class TokenManager {
|
|
42
|
+
private readonly secret;
|
|
43
|
+
private readonly accessTokenLifetime;
|
|
44
|
+
private readonly refreshTokenLifetime;
|
|
45
|
+
private readonly deviceCredentialLifetime;
|
|
46
|
+
constructor(config: TokenManagerConfig);
|
|
47
|
+
/**
|
|
48
|
+
* Issue a signed JWT access token.
|
|
49
|
+
*
|
|
50
|
+
* Access tokens are short-lived (default 15 minutes) and used to authorize
|
|
51
|
+
* API requests. When expired, use {@link refreshAccessToken} with a valid
|
|
52
|
+
* refresh token to obtain a new one.
|
|
53
|
+
*
|
|
54
|
+
* @param userId - The subject (user ID) to encode in the token
|
|
55
|
+
* @param deviceId - The device ID of the requesting device
|
|
56
|
+
* @returns A signed JWT string with type 'access'
|
|
57
|
+
*/
|
|
58
|
+
issueAccessToken(userId: string, deviceId: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Issue a signed JWT refresh token.
|
|
61
|
+
*
|
|
62
|
+
* Refresh tokens are longer-lived (default 90 days) and used exclusively
|
|
63
|
+
* to obtain new access tokens via {@link refreshAccessToken}. They should
|
|
64
|
+
* be stored securely and never sent to resource APIs.
|
|
65
|
+
*
|
|
66
|
+
* @param userId - The subject (user ID) to encode in the token
|
|
67
|
+
* @param deviceId - The device ID of the requesting device
|
|
68
|
+
* @returns A signed JWT string with type 'refresh'
|
|
69
|
+
*/
|
|
70
|
+
issueRefreshToken(userId: string, deviceId: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Issue a signed device credential token.
|
|
73
|
+
*
|
|
74
|
+
* Device credentials are long-lived tokens bound to a device's public key.
|
|
75
|
+
* They include a `mustCheckinBy` deadline; if the device does not check in
|
|
76
|
+
* before this deadline, the credential should be treated as revoked.
|
|
77
|
+
*
|
|
78
|
+
* @param userId - The subject (user ID) to encode in the token
|
|
79
|
+
* @param deviceId - The device ID of the requesting device
|
|
80
|
+
* @param publicKeyThumbprint - SHA-256 thumbprint of the device's public key
|
|
81
|
+
* @returns A signed JWT string with type 'device_credential'
|
|
82
|
+
*/
|
|
83
|
+
issueDeviceCredential(userId: string, deviceId: string, publicKeyThumbprint: string): string;
|
|
84
|
+
/**
|
|
85
|
+
* Issue a complete set of authentication tokens.
|
|
86
|
+
*
|
|
87
|
+
* Always issues an access token and refresh token. If a `publicKeyThumbprint`
|
|
88
|
+
* is provided, also issues a device credential.
|
|
89
|
+
*
|
|
90
|
+
* @param userId - The subject (user ID) to encode in the tokens
|
|
91
|
+
* @param deviceId - The device ID of the requesting device
|
|
92
|
+
* @param publicKeyThumbprint - Optional SHA-256 thumbprint of the device's public key.
|
|
93
|
+
* When provided, a device credential is included in the returned tokens.
|
|
94
|
+
* @returns An {@link AuthTokens} object containing the issued tokens
|
|
95
|
+
*/
|
|
96
|
+
issueTokens(userId: string, deviceId: string, publicKeyThumbprint?: string): AuthTokens;
|
|
97
|
+
/**
|
|
98
|
+
* Validate and decode a token.
|
|
99
|
+
*
|
|
100
|
+
* Verifies the HMAC-SHA256 signature and checks that the token has not expired.
|
|
101
|
+
* Returns null (rather than throwing) for invalid or expired tokens, so callers
|
|
102
|
+
* can handle authentication failure without try/catch.
|
|
103
|
+
*
|
|
104
|
+
* @param token - The JWT string to validate
|
|
105
|
+
* @returns The decoded {@link TokenPayload} if valid, or null if the token is
|
|
106
|
+
* invalid, expired, or missing required claims
|
|
107
|
+
*/
|
|
108
|
+
validateToken(token: string): TokenPayload | null;
|
|
109
|
+
/**
|
|
110
|
+
* Refresh an access token using a valid refresh token.
|
|
111
|
+
*
|
|
112
|
+
* Implements **refresh token rotation**: a new refresh token is issued alongside
|
|
113
|
+
* the new access token, and the old refresh token should be considered consumed.
|
|
114
|
+
* This limits the window of vulnerability if a refresh token is compromised.
|
|
115
|
+
*
|
|
116
|
+
* Returns null if the provided token is invalid, expired, or not a refresh token.
|
|
117
|
+
*
|
|
118
|
+
* @param refreshToken - The refresh token JWT string
|
|
119
|
+
* @returns A new access/refresh token pair, or null if the refresh token is invalid
|
|
120
|
+
*/
|
|
121
|
+
refreshAccessToken(refreshToken: string): {
|
|
122
|
+
accessToken: string;
|
|
123
|
+
refreshToken: string;
|
|
124
|
+
} | null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A user as visible to the application layer.
|
|
129
|
+
* Does not include sensitive fields like password hash or salt.
|
|
130
|
+
*/
|
|
131
|
+
interface AuthUser {
|
|
132
|
+
/** Unique user identifier (UUID v7 or crypto.randomUUID) */
|
|
133
|
+
id: string;
|
|
134
|
+
/** User's email address */
|
|
135
|
+
email: string;
|
|
136
|
+
/** User's display name */
|
|
137
|
+
name: string;
|
|
138
|
+
/** Timestamp when the user was created (milliseconds since epoch) */
|
|
139
|
+
createdAt: number;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Internal user record that includes credentials.
|
|
143
|
+
* Extends AuthUser with password hash and salt for verification.
|
|
144
|
+
*/
|
|
145
|
+
interface StoredUser extends AuthUser {
|
|
146
|
+
/** Hex-encoded PBKDF2 derived key */
|
|
147
|
+
passwordHash: string;
|
|
148
|
+
/** Hex-encoded random salt used during hashing */
|
|
149
|
+
salt: string;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A device registered to a user.
|
|
153
|
+
*/
|
|
154
|
+
interface AuthDevice {
|
|
155
|
+
/** Unique device identifier */
|
|
156
|
+
id: string;
|
|
157
|
+
/** ID of the user who owns this device */
|
|
158
|
+
userId: string;
|
|
159
|
+
/** Base64url-encoded public key (or thumbprint) for the device */
|
|
160
|
+
publicKey: string;
|
|
161
|
+
/** Human-readable device name */
|
|
162
|
+
name: string;
|
|
163
|
+
/** Whether the device has been revoked */
|
|
164
|
+
revoked: boolean;
|
|
165
|
+
/** Timestamp when the device was first registered (milliseconds since epoch) */
|
|
166
|
+
createdAt: number;
|
|
167
|
+
/** Timestamp when the device was last seen (milliseconds since epoch) */
|
|
168
|
+
lastSeenAt: number;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Thrown when a user account already exists with the given email.
|
|
172
|
+
*/
|
|
173
|
+
declare class DuplicateEmailError extends KoraError {
|
|
174
|
+
constructor(email: string);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* In-memory user and device store for the built-in auth provider.
|
|
178
|
+
*
|
|
179
|
+
* This is a simple implementation suitable for development and testing.
|
|
180
|
+
* Production applications should replace this with a database-backed store
|
|
181
|
+
* implementing the same interface.
|
|
182
|
+
*
|
|
183
|
+
* All methods are async to match the interface that a real database store
|
|
184
|
+
* would expose, even though the in-memory operations are synchronous.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* const store = new InMemoryUserStore()
|
|
189
|
+
* const user = await store.createUser({
|
|
190
|
+
* email: 'alice@example.com',
|
|
191
|
+
* passwordHash: 'abc123...',
|
|
192
|
+
* salt: 'def456...',
|
|
193
|
+
* name: 'Alice',
|
|
194
|
+
* })
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
declare class InMemoryUserStore {
|
|
198
|
+
/** Users indexed by ID */
|
|
199
|
+
private readonly usersById;
|
|
200
|
+
/** Users indexed by email (lowercase) for fast lookup */
|
|
201
|
+
private readonly usersByEmail;
|
|
202
|
+
/** Devices indexed by device ID */
|
|
203
|
+
private readonly devicesById;
|
|
204
|
+
/** Device IDs indexed by user ID for fast listing */
|
|
205
|
+
private readonly devicesByUserId;
|
|
206
|
+
/**
|
|
207
|
+
* Create a new user account.
|
|
208
|
+
*
|
|
209
|
+
* @param params - User creation parameters
|
|
210
|
+
* @param params.email - The user's email address (must be unique, case-insensitive)
|
|
211
|
+
* @param params.passwordHash - Hex-encoded PBKDF2 derived key
|
|
212
|
+
* @param params.salt - Hex-encoded salt used during hashing
|
|
213
|
+
* @param params.name - The user's display name
|
|
214
|
+
* @returns The created user (without sensitive credential fields)
|
|
215
|
+
* @throws {DuplicateEmailError} If a user with the same email already exists
|
|
216
|
+
*/
|
|
217
|
+
createUser(params: {
|
|
218
|
+
email: string;
|
|
219
|
+
passwordHash: string;
|
|
220
|
+
salt: string;
|
|
221
|
+
name: string;
|
|
222
|
+
}): Promise<AuthUser>;
|
|
223
|
+
/**
|
|
224
|
+
* Find a user by email address.
|
|
225
|
+
*
|
|
226
|
+
* @param email - The email to search for (case-insensitive)
|
|
227
|
+
* @returns The stored user record including credentials, or null if not found
|
|
228
|
+
*/
|
|
229
|
+
findByEmail(email: string): Promise<StoredUser | null>;
|
|
230
|
+
/**
|
|
231
|
+
* Find a user by ID.
|
|
232
|
+
*
|
|
233
|
+
* @param id - The user ID to search for
|
|
234
|
+
* @returns The stored user record including credentials, or null if not found
|
|
235
|
+
*/
|
|
236
|
+
findById(id: string): Promise<StoredUser | null>;
|
|
237
|
+
/**
|
|
238
|
+
* Register a device for a user.
|
|
239
|
+
*
|
|
240
|
+
* If a device with the same ID already exists and is not revoked, it is
|
|
241
|
+
* returned as-is (idempotent registration). If it was previously revoked,
|
|
242
|
+
* it is re-activated with updated details.
|
|
243
|
+
*
|
|
244
|
+
* @param params - Device registration parameters
|
|
245
|
+
* @param params.id - Unique device identifier
|
|
246
|
+
* @param params.userId - ID of the user who owns the device
|
|
247
|
+
* @param params.publicKey - Base64url-encoded device public key or thumbprint
|
|
248
|
+
* @param params.name - Human-readable device name
|
|
249
|
+
* @returns The registered device record
|
|
250
|
+
*/
|
|
251
|
+
registerDevice(params: {
|
|
252
|
+
id: string;
|
|
253
|
+
userId: string;
|
|
254
|
+
publicKey: string;
|
|
255
|
+
name: string;
|
|
256
|
+
}): Promise<AuthDevice>;
|
|
257
|
+
/**
|
|
258
|
+
* Find a device by its ID.
|
|
259
|
+
*
|
|
260
|
+
* @param deviceId - The device ID to search for
|
|
261
|
+
* @returns The device record, or null if not found
|
|
262
|
+
*/
|
|
263
|
+
findDevice(deviceId: string): Promise<AuthDevice | null>;
|
|
264
|
+
/**
|
|
265
|
+
* List all devices registered for a user.
|
|
266
|
+
*
|
|
267
|
+
* @param userId - The user ID whose devices to list
|
|
268
|
+
* @returns Array of device records (includes revoked devices)
|
|
269
|
+
*/
|
|
270
|
+
listDevices(userId: string): Promise<AuthDevice[]>;
|
|
271
|
+
/**
|
|
272
|
+
* Revoke a device, preventing it from being used for authentication.
|
|
273
|
+
*
|
|
274
|
+
* This is a soft revoke — the device record remains but is marked as revoked.
|
|
275
|
+
* If the device does not exist, this is a no-op.
|
|
276
|
+
*
|
|
277
|
+
* @param deviceId - The ID of the device to revoke
|
|
278
|
+
*/
|
|
279
|
+
revokeDevice(deviceId: string): Promise<void>;
|
|
280
|
+
/**
|
|
281
|
+
* Update the last-seen timestamp for a device.
|
|
282
|
+
*
|
|
283
|
+
* Called when a device authenticates or syncs to track activity.
|
|
284
|
+
* If the device does not exist, this is a no-op.
|
|
285
|
+
*
|
|
286
|
+
* @param deviceId - The ID of the device to update
|
|
287
|
+
*/
|
|
288
|
+
touchDevice(deviceId: string): Promise<void>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Configuration for building the built-in auth routes.
|
|
293
|
+
*/
|
|
294
|
+
interface AuthRoutesConfig {
|
|
295
|
+
/** The user/device store backing the auth routes */
|
|
296
|
+
userStore: InMemoryUserStore;
|
|
297
|
+
/** The token manager for issuing and validating JWTs */
|
|
298
|
+
tokenManager: TokenManager;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Response envelope returned by all auth route handlers.
|
|
302
|
+
*
|
|
303
|
+
* Successful responses include a `data` field; failures include an `error` string.
|
|
304
|
+
* The `status` field maps directly to an HTTP status code.
|
|
305
|
+
*/
|
|
306
|
+
interface AuthRouteResponse<T> {
|
|
307
|
+
/** HTTP status code */
|
|
308
|
+
status: number;
|
|
309
|
+
/** Either the success payload or an error message */
|
|
310
|
+
body: {
|
|
311
|
+
data: T;
|
|
312
|
+
} | {
|
|
313
|
+
error: string;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* HTTP route handlers for the built-in Kora auth provider.
|
|
318
|
+
*
|
|
319
|
+
* These are framework-agnostic functions that accept parsed request bodies and return
|
|
320
|
+
* structured responses. The server package is responsible for wiring them into its
|
|
321
|
+
* HTTP server (e.g., mapping `POST /auth/signup` to `handleSignUp`).
|
|
322
|
+
*
|
|
323
|
+
* All handlers follow the same pattern:
|
|
324
|
+
* - Validate input
|
|
325
|
+
* - Perform the operation
|
|
326
|
+
* - Return `{ status, body: { data } }` on success
|
|
327
|
+
* - Return `{ status, body: { error } }` on failure
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* ```typescript
|
|
331
|
+
* const routes = new BuiltInAuthRoutes({
|
|
332
|
+
* userStore: new InMemoryUserStore(),
|
|
333
|
+
* tokenManager: new TokenManager({ secret: 'my-secret' }),
|
|
334
|
+
* })
|
|
335
|
+
*
|
|
336
|
+
* // Wire into an HTTP server:
|
|
337
|
+
* app.post('/auth/signup', async (req, res) => {
|
|
338
|
+
* const result = await routes.handleSignUp(req.body)
|
|
339
|
+
* res.status(result.status).json(result.body)
|
|
340
|
+
* })
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
declare class BuiltInAuthRoutes {
|
|
344
|
+
private readonly userStore;
|
|
345
|
+
private readonly tokenManager;
|
|
346
|
+
constructor(config: AuthRoutesConfig);
|
|
347
|
+
/**
|
|
348
|
+
* Handle user sign-up (POST /auth/signup).
|
|
349
|
+
*
|
|
350
|
+
* Validates email format and password length, hashes the password,
|
|
351
|
+
* creates the user, optionally registers a device, and issues tokens.
|
|
352
|
+
*
|
|
353
|
+
* @param body - Sign-up request body
|
|
354
|
+
* @param body.email - The user's email address
|
|
355
|
+
* @param body.password - The plaintext password (min 8 characters)
|
|
356
|
+
* @param body.name - Optional display name (defaults to email local part)
|
|
357
|
+
* @param body.deviceId - Optional device ID to register
|
|
358
|
+
* @param body.devicePublicKey - Optional device public key (base64url)
|
|
359
|
+
* @returns Auth response with the created user and tokens, or an error
|
|
360
|
+
*/
|
|
361
|
+
handleSignUp(body: {
|
|
362
|
+
email: string;
|
|
363
|
+
password: string;
|
|
364
|
+
name?: string;
|
|
365
|
+
deviceId?: string;
|
|
366
|
+
devicePublicKey?: string;
|
|
367
|
+
}): Promise<AuthRouteResponse<{
|
|
368
|
+
user: AuthUser;
|
|
369
|
+
tokens: AuthTokens;
|
|
370
|
+
}>>;
|
|
371
|
+
/**
|
|
372
|
+
* Handle user sign-in (POST /auth/signin).
|
|
373
|
+
*
|
|
374
|
+
* Looks up the user by email, verifies the password, optionally registers
|
|
375
|
+
* a new device, and issues tokens.
|
|
376
|
+
*
|
|
377
|
+
* @param body - Sign-in request body
|
|
378
|
+
* @param body.email - The user's email address
|
|
379
|
+
* @param body.password - The plaintext password
|
|
380
|
+
* @param body.deviceId - Optional device ID to register
|
|
381
|
+
* @param body.devicePublicKey - Optional device public key (base64url)
|
|
382
|
+
* @returns Auth response with the user and tokens, or an error
|
|
383
|
+
*/
|
|
384
|
+
handleSignIn(body: {
|
|
385
|
+
email: string;
|
|
386
|
+
password: string;
|
|
387
|
+
deviceId?: string;
|
|
388
|
+
devicePublicKey?: string;
|
|
389
|
+
}): Promise<AuthRouteResponse<{
|
|
390
|
+
user: AuthUser;
|
|
391
|
+
tokens: AuthTokens;
|
|
392
|
+
}>>;
|
|
393
|
+
/**
|
|
394
|
+
* Handle token refresh (POST /auth/refresh).
|
|
395
|
+
*
|
|
396
|
+
* Validates the provided refresh token and issues a new token pair
|
|
397
|
+
* (refresh token rotation). The old refresh token should be considered
|
|
398
|
+
* consumed after this call.
|
|
399
|
+
*
|
|
400
|
+
* @param body - Refresh request body
|
|
401
|
+
* @param body.refreshToken - The current refresh token
|
|
402
|
+
* @returns Auth response with new tokens, or an error
|
|
403
|
+
*/
|
|
404
|
+
handleRefresh(body: {
|
|
405
|
+
refreshToken: string;
|
|
406
|
+
}): Promise<AuthRouteResponse<AuthTokens>>;
|
|
407
|
+
/**
|
|
408
|
+
* Handle get-current-user (GET /auth/me).
|
|
409
|
+
*
|
|
410
|
+
* Validates the access token and returns the authenticated user's profile.
|
|
411
|
+
*
|
|
412
|
+
* @param accessToken - The JWT access token (without "Bearer " prefix)
|
|
413
|
+
* @returns Auth response with the user profile, or an error
|
|
414
|
+
*/
|
|
415
|
+
handleGetMe(accessToken: string): Promise<AuthRouteResponse<AuthUser>>;
|
|
416
|
+
/**
|
|
417
|
+
* Handle list-devices (GET /auth/devices).
|
|
418
|
+
*
|
|
419
|
+
* Validates the access token and returns all devices registered for the user.
|
|
420
|
+
*
|
|
421
|
+
* @param accessToken - The JWT access token (without "Bearer " prefix)
|
|
422
|
+
* @returns Auth response with the device list, or an error
|
|
423
|
+
*/
|
|
424
|
+
handleListDevices(accessToken: string): Promise<AuthRouteResponse<AuthDevice[]>>;
|
|
425
|
+
/**
|
|
426
|
+
* Handle device revocation (DELETE /auth/device/:id).
|
|
427
|
+
*
|
|
428
|
+
* Validates the access token and revokes the specified device.
|
|
429
|
+
* Only the device's owner can revoke it.
|
|
430
|
+
*
|
|
431
|
+
* @param accessToken - The JWT access token (without "Bearer " prefix)
|
|
432
|
+
* @param deviceId - The ID of the device to revoke
|
|
433
|
+
* @returns Auth response with success flag, or an error
|
|
434
|
+
*/
|
|
435
|
+
handleRevokeDevice(accessToken: string, deviceId: string): Promise<AuthRouteResponse<{
|
|
436
|
+
success: boolean;
|
|
437
|
+
}>>;
|
|
438
|
+
/**
|
|
439
|
+
* Creates a sync server auth provider compatible with `@korajs/server`.
|
|
440
|
+
*
|
|
441
|
+
* The returned object implements the `AuthProvider` interface from
|
|
442
|
+
* `@korajs/server`, validating access tokens and returning an auth
|
|
443
|
+
* context containing the user ID and device metadata. This bridges
|
|
444
|
+
* the built-in auth system with the sync server's authentication layer.
|
|
445
|
+
*
|
|
446
|
+
* @returns An object with an `authenticate` method suitable for KoraSyncServer's `auth` config
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```typescript
|
|
450
|
+
* const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
|
|
451
|
+
* const syncServer = new KoraSyncServer({
|
|
452
|
+
* store,
|
|
453
|
+
* auth: routes.toSyncAuthProvider(),
|
|
454
|
+
* })
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
toSyncAuthProvider(): {
|
|
458
|
+
authenticate(token: string): Promise<{
|
|
459
|
+
userId: string;
|
|
460
|
+
scopes?: Record<string, Record<string, unknown>>;
|
|
461
|
+
metadata?: Record<string, unknown>;
|
|
462
|
+
} | null>;
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Creates a signed JWT (HS256) from a payload and secret.
|
|
468
|
+
*
|
|
469
|
+
* The token is structured as `header.payload.signature` per RFC 7519.
|
|
470
|
+
* Only HMAC-SHA256 is supported. The header is always `{"alg":"HS256","typ":"JWT"}`.
|
|
471
|
+
*
|
|
472
|
+
* @param payload - The claims to include in the token. Must be JSON-serializable.
|
|
473
|
+
* @param secret - The HMAC-SHA256 secret key used for signing.
|
|
474
|
+
* @returns A signed JWT string in the format `header.payload.signature`
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* ```typescript
|
|
478
|
+
* const token = encodeJwt(
|
|
479
|
+
* { sub: 'user-123', exp: Math.floor(Date.now() / 1000) + 900 },
|
|
480
|
+
* 'my-secret'
|
|
481
|
+
* )
|
|
482
|
+
* ```
|
|
483
|
+
*/
|
|
484
|
+
declare function encodeJwt(payload: Record<string, unknown>, secret: string): string;
|
|
485
|
+
/**
|
|
486
|
+
* Decodes a JWT without verifying its signature.
|
|
487
|
+
*
|
|
488
|
+
* Use this only when you need to inspect claims (e.g., reading `exp` to decide
|
|
489
|
+
* whether to attempt a refresh) and do NOT need to trust the token's authenticity.
|
|
490
|
+
* For trusted reads, use {@link verifyJwt} instead.
|
|
491
|
+
*
|
|
492
|
+
* @param token - The JWT string to decode
|
|
493
|
+
* @returns The decoded payload as a record, or null if the token is malformed
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```typescript
|
|
497
|
+
* const claims = decodeJwt(token)
|
|
498
|
+
* if (claims && typeof claims.sub === 'string') {
|
|
499
|
+
* console.log('User:', claims.sub)
|
|
500
|
+
* }
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
declare function decodeJwt(token: string): Record<string, unknown> | null;
|
|
504
|
+
/**
|
|
505
|
+
* Decodes a JWT and verifies its HMAC-SHA256 signature.
|
|
506
|
+
*
|
|
507
|
+
* Returns the payload only if the signature is valid. Returns null if the token
|
|
508
|
+
* is malformed, has an invalid signature, or cannot be parsed.
|
|
509
|
+
*
|
|
510
|
+
* This function does NOT check expiration. Use {@link isExpired} separately
|
|
511
|
+
* to check the `exp` claim, allowing callers to distinguish between
|
|
512
|
+
* "invalid signature" (security issue) and "expired" (normal lifecycle).
|
|
513
|
+
*
|
|
514
|
+
* @param token - The JWT string to verify
|
|
515
|
+
* @param secret - The HMAC-SHA256 secret key that was used to sign the token
|
|
516
|
+
* @returns The decoded payload if the signature is valid, or null otherwise
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```typescript
|
|
520
|
+
* const claims = verifyJwt(token, 'my-secret')
|
|
521
|
+
* if (claims === null) {
|
|
522
|
+
* throw new Error('Invalid token signature')
|
|
523
|
+
* }
|
|
524
|
+
* if (isExpired(claims)) {
|
|
525
|
+
* throw new Error('Token has expired')
|
|
526
|
+
* }
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
declare function verifyJwt(token: string, secret: string): Record<string, unknown> | null;
|
|
530
|
+
/**
|
|
531
|
+
* Checks whether a token payload has expired based on its `exp` claim.
|
|
532
|
+
*
|
|
533
|
+
* The `exp` claim is expected to be in seconds since the Unix epoch (per JWT spec).
|
|
534
|
+
* If the `exp` claim is missing or is not a number, the token is considered
|
|
535
|
+
* non-expiring and this function returns false.
|
|
536
|
+
*
|
|
537
|
+
* @param payload - An object with an optional `exp` field (seconds since epoch)
|
|
538
|
+
* @returns true if the token has expired, false otherwise
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```typescript
|
|
542
|
+
* const claims = verifyJwt(token, secret)
|
|
543
|
+
* if (claims && isExpired(claims)) {
|
|
544
|
+
* // Token signature is valid but it has expired -- attempt refresh
|
|
545
|
+
* }
|
|
546
|
+
* ```
|
|
547
|
+
*/
|
|
548
|
+
declare function isExpired(payload: {
|
|
549
|
+
exp?: number;
|
|
550
|
+
}): boolean;
|
|
551
|
+
|
|
552
|
+
interface HashResult {
|
|
553
|
+
/** Hex-encoded PBKDF2 derived key. */
|
|
554
|
+
hash: string;
|
|
555
|
+
/** Hex-encoded random salt used during hashing. */
|
|
556
|
+
salt: string;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Hashes a password using PBKDF2 with a cryptographically random salt.
|
|
560
|
+
*
|
|
561
|
+
* Uses SHA-512 with 600,000 iterations and a 32-byte random salt,
|
|
562
|
+
* producing a 64-byte derived key. Both the hash and salt are returned
|
|
563
|
+
* as hex-encoded strings for storage.
|
|
564
|
+
*
|
|
565
|
+
* @param password - The plaintext password to hash
|
|
566
|
+
* @returns The hex-encoded hash and salt
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```typescript
|
|
570
|
+
* const { hash, salt } = await hashPassword('my-secret-password')
|
|
571
|
+
* // Store hash and salt in the database
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
declare function hashPassword(password: string): Promise<HashResult>;
|
|
575
|
+
/**
|
|
576
|
+
* Verifies a plaintext password against a stored hash and salt using
|
|
577
|
+
* timing-safe comparison to prevent timing attacks.
|
|
578
|
+
*
|
|
579
|
+
* Re-derives the key from the password and salt using the same PBKDF2
|
|
580
|
+
* parameters, then compares the result against the stored hash using
|
|
581
|
+
* `crypto.timingSafeEqual` to avoid leaking information through
|
|
582
|
+
* response timing.
|
|
583
|
+
*
|
|
584
|
+
* @param password - The plaintext password to verify
|
|
585
|
+
* @param hash - The hex-encoded hash to compare against
|
|
586
|
+
* @param salt - The hex-encoded salt that was used to produce the hash
|
|
587
|
+
* @returns `true` if the password matches, `false` otherwise
|
|
588
|
+
*
|
|
589
|
+
* @example
|
|
590
|
+
* ```typescript
|
|
591
|
+
* const isValid = await verifyPassword('my-secret-password', storedHash, storedSalt)
|
|
592
|
+
* if (isValid) {
|
|
593
|
+
* // Grant access
|
|
594
|
+
* }
|
|
595
|
+
* ```
|
|
596
|
+
*/
|
|
597
|
+
declare function verifyPassword(password: string, hash: string, salt: string): Promise<boolean>;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Parameters for signing up a new user.
|
|
601
|
+
*/
|
|
602
|
+
interface SignUpParams {
|
|
603
|
+
/** The user's email address */
|
|
604
|
+
email: string;
|
|
605
|
+
/** The plaintext password */
|
|
606
|
+
password: string;
|
|
607
|
+
/** Optional display name */
|
|
608
|
+
name?: string;
|
|
609
|
+
/** Optional device ID to register with the account */
|
|
610
|
+
deviceId?: string;
|
|
611
|
+
/** Optional device public key (base64url) */
|
|
612
|
+
devicePublicKey?: string;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Parameters for signing in an existing user.
|
|
616
|
+
*/
|
|
617
|
+
interface SignInParams {
|
|
618
|
+
/** The user's email address */
|
|
619
|
+
email: string;
|
|
620
|
+
/** The plaintext password */
|
|
621
|
+
password: string;
|
|
622
|
+
/** Optional device ID to register or associate */
|
|
623
|
+
deviceId?: string;
|
|
624
|
+
/** Optional device public key (base64url) */
|
|
625
|
+
devicePublicKey?: string;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Abstraction for authentication providers.
|
|
629
|
+
*
|
|
630
|
+
* This interface allows swapping between the built-in email/password provider
|
|
631
|
+
* and external providers (OAuth, SAML, custom) without changing application code.
|
|
632
|
+
* Every provider must support the same core operations: sign up, sign in,
|
|
633
|
+
* token refresh, token validation, user lookup, and device management.
|
|
634
|
+
*
|
|
635
|
+
* @example
|
|
636
|
+
* ```typescript
|
|
637
|
+
* // Use the built-in provider
|
|
638
|
+
* const provider: AuthProviderAdapter = new BuiltInProvider({
|
|
639
|
+
* userStore: new InMemoryUserStore(),
|
|
640
|
+
* tokenManager: new TokenManager({ secret: 'my-secret' }),
|
|
641
|
+
* })
|
|
642
|
+
*
|
|
643
|
+
* const { user, tokens } = await provider.signUp({
|
|
644
|
+
* email: 'alice@example.com',
|
|
645
|
+
* password: 'secure-password-123',
|
|
646
|
+
* })
|
|
647
|
+
* ```
|
|
648
|
+
*/
|
|
649
|
+
interface AuthProviderAdapter {
|
|
650
|
+
/**
|
|
651
|
+
* Create a new user account and issue authentication tokens.
|
|
652
|
+
*
|
|
653
|
+
* @param params - Sign-up parameters including email, password, and optional device info
|
|
654
|
+
* @returns The created user and tokens
|
|
655
|
+
* @throws If the email is already registered or input validation fails
|
|
656
|
+
*/
|
|
657
|
+
signUp(params: SignUpParams): Promise<{
|
|
658
|
+
user: AuthUser;
|
|
659
|
+
tokens: AuthTokens;
|
|
660
|
+
}>;
|
|
661
|
+
/**
|
|
662
|
+
* Authenticate an existing user and issue tokens.
|
|
663
|
+
*
|
|
664
|
+
* @param params - Sign-in parameters including email and password
|
|
665
|
+
* @returns The authenticated user and tokens
|
|
666
|
+
* @throws If the credentials are invalid
|
|
667
|
+
*/
|
|
668
|
+
signIn(params: SignInParams): Promise<{
|
|
669
|
+
user: AuthUser;
|
|
670
|
+
tokens: AuthTokens;
|
|
671
|
+
}>;
|
|
672
|
+
/**
|
|
673
|
+
* Exchange a refresh token for new tokens (rotation).
|
|
674
|
+
*
|
|
675
|
+
* @param refreshToken - The current refresh token
|
|
676
|
+
* @returns A new token pair
|
|
677
|
+
* @throws If the refresh token is invalid or expired
|
|
678
|
+
*/
|
|
679
|
+
refreshTokens(refreshToken: string): Promise<AuthTokens>;
|
|
680
|
+
/**
|
|
681
|
+
* Validate an access token and extract identity claims.
|
|
682
|
+
*
|
|
683
|
+
* @param token - The JWT access token
|
|
684
|
+
* @returns User ID and device ID if valid, or null if invalid/expired
|
|
685
|
+
*/
|
|
686
|
+
validateAccessToken(token: string): Promise<{
|
|
687
|
+
userId: string;
|
|
688
|
+
deviceId: string;
|
|
689
|
+
} | null>;
|
|
690
|
+
/**
|
|
691
|
+
* Look up a user by ID.
|
|
692
|
+
*
|
|
693
|
+
* @param userId - The user's ID
|
|
694
|
+
* @returns The user profile, or null if not found
|
|
695
|
+
*/
|
|
696
|
+
getUser(userId: string): Promise<AuthUser | null>;
|
|
697
|
+
/**
|
|
698
|
+
* Revoke a device. Requires a valid access token for authorization.
|
|
699
|
+
*
|
|
700
|
+
* @param accessToken - The caller's access token
|
|
701
|
+
* @param deviceId - The ID of the device to revoke
|
|
702
|
+
* @throws If the token is invalid or the device does not belong to the caller
|
|
703
|
+
*/
|
|
704
|
+
revokeDevice(accessToken: string, deviceId: string): Promise<void>;
|
|
705
|
+
/**
|
|
706
|
+
* List all devices for the authenticated user.
|
|
707
|
+
*
|
|
708
|
+
* @param accessToken - The caller's access token
|
|
709
|
+
* @returns Array of device records
|
|
710
|
+
* @throws If the token is invalid
|
|
711
|
+
*/
|
|
712
|
+
listDevices(accessToken: string): Promise<AuthDevice[]>;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Error thrown by provider adapter methods when an operation fails.
|
|
716
|
+
* Wraps the HTTP-style status and error message from route handlers
|
|
717
|
+
* into an exception for use in the adapter pattern.
|
|
718
|
+
*/
|
|
719
|
+
declare class AuthProviderError extends Error {
|
|
720
|
+
/** HTTP-style status code */
|
|
721
|
+
readonly status: number;
|
|
722
|
+
constructor(message: string, status: number);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Built-in authentication provider implementing email/password authentication.
|
|
726
|
+
*
|
|
727
|
+
* Wraps {@link BuiltInAuthRoutes} in the {@link AuthProviderAdapter} interface,
|
|
728
|
+
* converting HTTP-style responses into direct return values and exceptions.
|
|
729
|
+
* This is the default provider shipped with Kora and is suitable for
|
|
730
|
+
* applications that want simple email/password auth without external services.
|
|
731
|
+
*
|
|
732
|
+
* @example
|
|
733
|
+
* ```typescript
|
|
734
|
+
* import { BuiltInProvider } from '@korajs/auth'
|
|
735
|
+
* import { InMemoryUserStore } from '@korajs/auth'
|
|
736
|
+
* import { TokenManager } from '@korajs/auth'
|
|
737
|
+
*
|
|
738
|
+
* const provider = new BuiltInProvider({
|
|
739
|
+
* userStore: new InMemoryUserStore(),
|
|
740
|
+
* tokenManager: new TokenManager({ secret: process.env.AUTH_SECRET }),
|
|
741
|
+
* })
|
|
742
|
+
*
|
|
743
|
+
* const { user, tokens } = await provider.signUp({
|
|
744
|
+
* email: 'alice@example.com',
|
|
745
|
+
* password: 'strong-password-123',
|
|
746
|
+
* name: 'Alice',
|
|
747
|
+
* })
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
declare class BuiltInProvider implements AuthProviderAdapter {
|
|
751
|
+
private readonly routes;
|
|
752
|
+
private readonly tokenManager;
|
|
753
|
+
private readonly userStore;
|
|
754
|
+
constructor(config: AuthRoutesConfig);
|
|
755
|
+
/** @inheritdoc */
|
|
756
|
+
signUp(params: SignUpParams): Promise<{
|
|
757
|
+
user: AuthUser;
|
|
758
|
+
tokens: AuthTokens;
|
|
759
|
+
}>;
|
|
760
|
+
/** @inheritdoc */
|
|
761
|
+
signIn(params: SignInParams): Promise<{
|
|
762
|
+
user: AuthUser;
|
|
763
|
+
tokens: AuthTokens;
|
|
764
|
+
}>;
|
|
765
|
+
/** @inheritdoc */
|
|
766
|
+
refreshTokens(refreshToken: string): Promise<AuthTokens>;
|
|
767
|
+
/** @inheritdoc */
|
|
768
|
+
validateAccessToken(token: string): Promise<{
|
|
769
|
+
userId: string;
|
|
770
|
+
deviceId: string;
|
|
771
|
+
} | null>;
|
|
772
|
+
/** @inheritdoc */
|
|
773
|
+
getUser(userId: string): Promise<AuthUser | null>;
|
|
774
|
+
/** @inheritdoc */
|
|
775
|
+
revokeDevice(accessToken: string, deviceId: string): Promise<void>;
|
|
776
|
+
/** @inheritdoc */
|
|
777
|
+
listDevices(accessToken: string): Promise<AuthDevice[]>;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export { type AuthDevice, type AuthProviderAdapter, AuthProviderError, type AuthRouteResponse, type AuthRoutesConfig, type AuthUser, BuiltInAuthRoutes, BuiltInProvider, DuplicateEmailError, InMemoryUserStore, type SignInParams, type SignUpParams, type StoredUser, TokenManager, type TokenManagerConfig, decodeJwt, encodeJwt, hashPassword, isExpired, verifyJwt, verifyPassword };
|