@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/dist/server.js ADDED
@@ -0,0 +1,890 @@
1
+ import {
2
+ computePublicKeyThumbprint,
3
+ verifyChallenge
4
+ } from "./chunk-L554ZDPY.js";
5
+
6
+ // src/types.ts
7
+ import { KoraError } from "@korajs/core";
8
+ var DEFAULT_ACCESS_TOKEN_LIFETIME = 15 * 60 * 1e3;
9
+ var DEFAULT_REFRESH_TOKEN_LIFETIME = 90 * 24 * 60 * 60 * 1e3;
10
+ var DEFAULT_DEVICE_CREDENTIAL_LIFETIME = 90 * 24 * 60 * 60 * 1e3;
11
+ var DEFAULT_MAX_OFFLINE_DURATION = 30 * 24 * 60 * 60 * 1e3;
12
+ var DEFAULT_AUTO_LOCK_TIMEOUT = 15 * 60 * 1e3;
13
+
14
+ // src/tokens/jwt.ts
15
+ import { createHmac } from "crypto";
16
+ function base64urlEncode(input) {
17
+ return Buffer.from(input, "utf-8").toString("base64url");
18
+ }
19
+ function base64urlDecode(input) {
20
+ return Buffer.from(input, "base64url").toString("utf-8");
21
+ }
22
+ function hmacSha256Base64url(data, secret) {
23
+ return createHmac("sha256", secret).update(data).digest("base64url");
24
+ }
25
+ var ENCODED_HEADER = base64urlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
26
+ function encodeJwt(payload, secret) {
27
+ const encodedPayload = base64urlEncode(JSON.stringify(payload));
28
+ const signingInput = `${ENCODED_HEADER}.${encodedPayload}`;
29
+ const signature = hmacSha256Base64url(signingInput, secret);
30
+ return `${signingInput}.${signature}`;
31
+ }
32
+ function decodeJwt(token) {
33
+ const parts = token.split(".");
34
+ if (parts.length !== 3) {
35
+ return null;
36
+ }
37
+ const payloadSegment = parts[1];
38
+ if (payloadSegment === void 0) {
39
+ return null;
40
+ }
41
+ try {
42
+ const decoded = base64urlDecode(payloadSegment);
43
+ const parsed = JSON.parse(decoded);
44
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
45
+ return null;
46
+ }
47
+ return parsed;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function verifyJwt(token, secret) {
53
+ const parts = token.split(".");
54
+ if (parts.length !== 3) {
55
+ return null;
56
+ }
57
+ const headerSegment = parts[0];
58
+ const payloadSegment = parts[1];
59
+ const signatureSegment = parts[2];
60
+ if (headerSegment === void 0 || payloadSegment === void 0 || signatureSegment === void 0) {
61
+ return null;
62
+ }
63
+ const signingInput = `${headerSegment}.${payloadSegment}`;
64
+ const expectedSignature = hmacSha256Base64url(signingInput, secret);
65
+ if (expectedSignature.length !== signatureSegment.length) {
66
+ return null;
67
+ }
68
+ let mismatch = 0;
69
+ for (let i = 0; i < expectedSignature.length; i++) {
70
+ mismatch |= expectedSignature.charCodeAt(i) ^ signatureSegment.charCodeAt(i);
71
+ }
72
+ if (mismatch !== 0) {
73
+ return null;
74
+ }
75
+ try {
76
+ const decoded = base64urlDecode(payloadSegment);
77
+ const parsed = JSON.parse(decoded);
78
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
79
+ return null;
80
+ }
81
+ return parsed;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function isExpired(payload) {
87
+ if (typeof payload.exp !== "number") {
88
+ return false;
89
+ }
90
+ const nowSeconds = Math.floor(Date.now() / 1e3);
91
+ return nowSeconds >= payload.exp;
92
+ }
93
+
94
+ // src/tokens/token-manager.ts
95
+ var TokenManager = class {
96
+ secret;
97
+ accessTokenLifetime;
98
+ refreshTokenLifetime;
99
+ deviceCredentialLifetime;
100
+ constructor(config) {
101
+ this.secret = config.secret;
102
+ this.accessTokenLifetime = config.accessTokenLifetime ?? DEFAULT_ACCESS_TOKEN_LIFETIME;
103
+ this.refreshTokenLifetime = config.refreshTokenLifetime ?? DEFAULT_REFRESH_TOKEN_LIFETIME;
104
+ this.deviceCredentialLifetime = config.deviceCredentialLifetime ?? DEFAULT_DEVICE_CREDENTIAL_LIFETIME;
105
+ }
106
+ /**
107
+ * Issue a signed JWT access token.
108
+ *
109
+ * Access tokens are short-lived (default 15 minutes) and used to authorize
110
+ * API requests. When expired, use {@link refreshAccessToken} with a valid
111
+ * refresh token to obtain a new one.
112
+ *
113
+ * @param userId - The subject (user ID) to encode in the token
114
+ * @param deviceId - The device ID of the requesting device
115
+ * @returns A signed JWT string with type 'access'
116
+ */
117
+ issueAccessToken(userId, deviceId) {
118
+ const nowSeconds = Math.floor(Date.now() / 1e3);
119
+ const payload = {
120
+ sub: userId,
121
+ dev: deviceId,
122
+ type: "access",
123
+ iat: nowSeconds,
124
+ exp: nowSeconds + Math.floor(this.accessTokenLifetime / 1e3)
125
+ };
126
+ return encodeJwt(payload, this.secret);
127
+ }
128
+ /**
129
+ * Issue a signed JWT refresh token.
130
+ *
131
+ * Refresh tokens are longer-lived (default 90 days) and used exclusively
132
+ * to obtain new access tokens via {@link refreshAccessToken}. They should
133
+ * be stored securely and never sent to resource APIs.
134
+ *
135
+ * @param userId - The subject (user ID) to encode in the token
136
+ * @param deviceId - The device ID of the requesting device
137
+ * @returns A signed JWT string with type 'refresh'
138
+ */
139
+ issueRefreshToken(userId, deviceId) {
140
+ const nowSeconds = Math.floor(Date.now() / 1e3);
141
+ const payload = {
142
+ sub: userId,
143
+ dev: deviceId,
144
+ type: "refresh",
145
+ iat: nowSeconds,
146
+ exp: nowSeconds + Math.floor(this.refreshTokenLifetime / 1e3)
147
+ };
148
+ return encodeJwt(payload, this.secret);
149
+ }
150
+ /**
151
+ * Issue a signed device credential token.
152
+ *
153
+ * Device credentials are long-lived tokens bound to a device's public key.
154
+ * They include a `mustCheckinBy` deadline; if the device does not check in
155
+ * before this deadline, the credential should be treated as revoked.
156
+ *
157
+ * @param userId - The subject (user ID) to encode in the token
158
+ * @param deviceId - The device ID of the requesting device
159
+ * @param publicKeyThumbprint - SHA-256 thumbprint of the device's public key
160
+ * @returns A signed JWT string with type 'device_credential'
161
+ */
162
+ issueDeviceCredential(userId, deviceId, publicKeyThumbprint) {
163
+ const nowSeconds = Math.floor(Date.now() / 1e3);
164
+ const lifetimeSeconds = Math.floor(this.deviceCredentialLifetime / 1e3);
165
+ const payload = {
166
+ sub: userId,
167
+ dev: deviceId,
168
+ type: "device_credential",
169
+ iat: nowSeconds,
170
+ exp: nowSeconds + lifetimeSeconds,
171
+ dpk: publicKeyThumbprint,
172
+ mustCheckinBy: nowSeconds + lifetimeSeconds
173
+ };
174
+ return encodeJwt(payload, this.secret);
175
+ }
176
+ /**
177
+ * Issue a complete set of authentication tokens.
178
+ *
179
+ * Always issues an access token and refresh token. If a `publicKeyThumbprint`
180
+ * is provided, also issues a device credential.
181
+ *
182
+ * @param userId - The subject (user ID) to encode in the tokens
183
+ * @param deviceId - The device ID of the requesting device
184
+ * @param publicKeyThumbprint - Optional SHA-256 thumbprint of the device's public key.
185
+ * When provided, a device credential is included in the returned tokens.
186
+ * @returns An {@link AuthTokens} object containing the issued tokens
187
+ */
188
+ issueTokens(userId, deviceId, publicKeyThumbprint) {
189
+ const tokens = {
190
+ accessToken: this.issueAccessToken(userId, deviceId),
191
+ refreshToken: this.issueRefreshToken(userId, deviceId)
192
+ };
193
+ if (publicKeyThumbprint !== void 0) {
194
+ tokens.deviceCredential = this.issueDeviceCredential(
195
+ userId,
196
+ deviceId,
197
+ publicKeyThumbprint
198
+ );
199
+ }
200
+ return tokens;
201
+ }
202
+ /**
203
+ * Validate and decode a token.
204
+ *
205
+ * Verifies the HMAC-SHA256 signature and checks that the token has not expired.
206
+ * Returns null (rather than throwing) for invalid or expired tokens, so callers
207
+ * can handle authentication failure without try/catch.
208
+ *
209
+ * @param token - The JWT string to validate
210
+ * @returns The decoded {@link TokenPayload} if valid, or null if the token is
211
+ * invalid, expired, or missing required claims
212
+ */
213
+ validateToken(token) {
214
+ const decoded = verifyJwt(token, this.secret);
215
+ if (decoded === null) {
216
+ return null;
217
+ }
218
+ if (isExpired(decoded)) {
219
+ return null;
220
+ }
221
+ if (typeof decoded["sub"] !== "string" || typeof decoded["dev"] !== "string" || typeof decoded["type"] !== "string" || typeof decoded["iat"] !== "number" || typeof decoded["exp"] !== "number") {
222
+ return null;
223
+ }
224
+ const type = decoded["type"];
225
+ if (type !== "access" && type !== "refresh" && type !== "device_credential") {
226
+ return null;
227
+ }
228
+ return {
229
+ sub: decoded["sub"],
230
+ dev: decoded["dev"],
231
+ type,
232
+ iat: decoded["iat"],
233
+ exp: decoded["exp"]
234
+ };
235
+ }
236
+ /**
237
+ * Refresh an access token using a valid refresh token.
238
+ *
239
+ * Implements **refresh token rotation**: a new refresh token is issued alongside
240
+ * the new access token, and the old refresh token should be considered consumed.
241
+ * This limits the window of vulnerability if a refresh token is compromised.
242
+ *
243
+ * Returns null if the provided token is invalid, expired, or not a refresh token.
244
+ *
245
+ * @param refreshToken - The refresh token JWT string
246
+ * @returns A new access/refresh token pair, or null if the refresh token is invalid
247
+ */
248
+ refreshAccessToken(refreshToken) {
249
+ const payload = this.validateToken(refreshToken);
250
+ if (payload === null) {
251
+ return null;
252
+ }
253
+ if (payload.type !== "refresh") {
254
+ return null;
255
+ }
256
+ return {
257
+ accessToken: this.issueAccessToken(payload.sub, payload.dev),
258
+ refreshToken: this.issueRefreshToken(payload.sub, payload.dev)
259
+ };
260
+ }
261
+ };
262
+
263
+ // src/provider/built-in/password-hash.ts
264
+ import { randomBytes, pbkdf2, timingSafeEqual } from "crypto";
265
+ var PBKDF2_ITERATIONS = 6e5;
266
+ var PBKDF2_DIGEST = "sha512";
267
+ var KEY_LENGTH = 64;
268
+ var SALT_LENGTH = 32;
269
+ async function hashPassword(password) {
270
+ const salt = randomBytes(SALT_LENGTH);
271
+ const derivedKey = await pbkdf2Async(
272
+ password,
273
+ salt,
274
+ PBKDF2_ITERATIONS,
275
+ KEY_LENGTH,
276
+ PBKDF2_DIGEST
277
+ );
278
+ return {
279
+ hash: derivedKey.toString("hex"),
280
+ salt: salt.toString("hex")
281
+ };
282
+ }
283
+ async function verifyPassword(password, hash, salt) {
284
+ const saltBuffer = Buffer.from(salt, "hex");
285
+ const derivedKey = await pbkdf2Async(
286
+ password,
287
+ saltBuffer,
288
+ PBKDF2_ITERATIONS,
289
+ KEY_LENGTH,
290
+ PBKDF2_DIGEST
291
+ );
292
+ const hashBuffer = Buffer.from(hash, "hex");
293
+ if (derivedKey.length !== hashBuffer.length) {
294
+ return false;
295
+ }
296
+ return timingSafeEqual(derivedKey, hashBuffer);
297
+ }
298
+ function pbkdf2Async(password, salt, iterations, keyLength, digest) {
299
+ return new Promise((resolve, reject) => {
300
+ pbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {
301
+ if (err) {
302
+ reject(err);
303
+ } else {
304
+ resolve(derivedKey);
305
+ }
306
+ });
307
+ });
308
+ }
309
+
310
+ // src/provider/built-in/user-store.ts
311
+ import { KoraError as KoraError2 } from "@korajs/core";
312
+ import { randomUUID } from "crypto";
313
+ var DuplicateEmailError = class extends KoraError2 {
314
+ constructor(email) {
315
+ super(
316
+ `A user with email "${email}" already exists. Use a different email address or sign in to the existing account.`,
317
+ "DUPLICATE_EMAIL",
318
+ { email }
319
+ );
320
+ this.name = "DuplicateEmailError";
321
+ }
322
+ };
323
+ var InMemoryUserStore = class {
324
+ /** Users indexed by ID */
325
+ usersById = /* @__PURE__ */ new Map();
326
+ /** Users indexed by email (lowercase) for fast lookup */
327
+ usersByEmail = /* @__PURE__ */ new Map();
328
+ /** Devices indexed by device ID */
329
+ devicesById = /* @__PURE__ */ new Map();
330
+ /** Device IDs indexed by user ID for fast listing */
331
+ devicesByUserId = /* @__PURE__ */ new Map();
332
+ /**
333
+ * Create a new user account.
334
+ *
335
+ * @param params - User creation parameters
336
+ * @param params.email - The user's email address (must be unique, case-insensitive)
337
+ * @param params.passwordHash - Hex-encoded PBKDF2 derived key
338
+ * @param params.salt - Hex-encoded salt used during hashing
339
+ * @param params.name - The user's display name
340
+ * @returns The created user (without sensitive credential fields)
341
+ * @throws {DuplicateEmailError} If a user with the same email already exists
342
+ */
343
+ async createUser(params) {
344
+ const normalizedEmail = params.email.toLowerCase();
345
+ if (this.usersByEmail.has(normalizedEmail)) {
346
+ throw new DuplicateEmailError(params.email);
347
+ }
348
+ const now = Date.now();
349
+ const id = randomUUID();
350
+ const storedUser = {
351
+ id,
352
+ email: normalizedEmail,
353
+ name: params.name,
354
+ createdAt: now,
355
+ passwordHash: params.passwordHash,
356
+ salt: params.salt
357
+ };
358
+ this.usersById.set(id, storedUser);
359
+ this.usersByEmail.set(normalizedEmail, storedUser);
360
+ return toAuthUser(storedUser);
361
+ }
362
+ /**
363
+ * Find a user by email address.
364
+ *
365
+ * @param email - The email to search for (case-insensitive)
366
+ * @returns The stored user record including credentials, or null if not found
367
+ */
368
+ async findByEmail(email) {
369
+ return this.usersByEmail.get(email.toLowerCase()) ?? null;
370
+ }
371
+ /**
372
+ * Find a user by ID.
373
+ *
374
+ * @param id - The user ID to search for
375
+ * @returns The stored user record including credentials, or null if not found
376
+ */
377
+ async findById(id) {
378
+ return this.usersById.get(id) ?? null;
379
+ }
380
+ /**
381
+ * Register a device for a user.
382
+ *
383
+ * If a device with the same ID already exists and is not revoked, it is
384
+ * returned as-is (idempotent registration). If it was previously revoked,
385
+ * it is re-activated with updated details.
386
+ *
387
+ * @param params - Device registration parameters
388
+ * @param params.id - Unique device identifier
389
+ * @param params.userId - ID of the user who owns the device
390
+ * @param params.publicKey - Base64url-encoded device public key or thumbprint
391
+ * @param params.name - Human-readable device name
392
+ * @returns The registered device record
393
+ */
394
+ async registerDevice(params) {
395
+ const existing = this.devicesById.get(params.id);
396
+ if (existing !== void 0 && !existing.revoked) {
397
+ return existing;
398
+ }
399
+ const now = Date.now();
400
+ const device = {
401
+ id: params.id,
402
+ userId: params.userId,
403
+ publicKey: params.publicKey,
404
+ name: params.name,
405
+ revoked: false,
406
+ createdAt: now,
407
+ lastSeenAt: now
408
+ };
409
+ this.devicesById.set(params.id, device);
410
+ let userDevices = this.devicesByUserId.get(params.userId);
411
+ if (userDevices === void 0) {
412
+ userDevices = /* @__PURE__ */ new Set();
413
+ this.devicesByUserId.set(params.userId, userDevices);
414
+ }
415
+ userDevices.add(params.id);
416
+ return device;
417
+ }
418
+ /**
419
+ * Find a device by its ID.
420
+ *
421
+ * @param deviceId - The device ID to search for
422
+ * @returns The device record, or null if not found
423
+ */
424
+ async findDevice(deviceId) {
425
+ return this.devicesById.get(deviceId) ?? null;
426
+ }
427
+ /**
428
+ * List all devices registered for a user.
429
+ *
430
+ * @param userId - The user ID whose devices to list
431
+ * @returns Array of device records (includes revoked devices)
432
+ */
433
+ async listDevices(userId) {
434
+ const deviceIds = this.devicesByUserId.get(userId);
435
+ if (deviceIds === void 0) {
436
+ return [];
437
+ }
438
+ const devices = [];
439
+ for (const deviceId of deviceIds) {
440
+ const device = this.devicesById.get(deviceId);
441
+ if (device !== void 0) {
442
+ devices.push(device);
443
+ }
444
+ }
445
+ return devices;
446
+ }
447
+ /**
448
+ * Revoke a device, preventing it from being used for authentication.
449
+ *
450
+ * This is a soft revoke — the device record remains but is marked as revoked.
451
+ * If the device does not exist, this is a no-op.
452
+ *
453
+ * @param deviceId - The ID of the device to revoke
454
+ */
455
+ async revokeDevice(deviceId) {
456
+ const device = this.devicesById.get(deviceId);
457
+ if (device !== void 0) {
458
+ device.revoked = true;
459
+ }
460
+ }
461
+ /**
462
+ * Update the last-seen timestamp for a device.
463
+ *
464
+ * Called when a device authenticates or syncs to track activity.
465
+ * If the device does not exist, this is a no-op.
466
+ *
467
+ * @param deviceId - The ID of the device to update
468
+ */
469
+ async touchDevice(deviceId) {
470
+ const device = this.devicesById.get(deviceId);
471
+ if (device !== void 0) {
472
+ device.lastSeenAt = Date.now();
473
+ }
474
+ }
475
+ };
476
+ function toAuthUser(stored) {
477
+ return {
478
+ id: stored.id,
479
+ email: stored.email,
480
+ name: stored.name,
481
+ createdAt: stored.createdAt
482
+ };
483
+ }
484
+
485
+ // src/provider/built-in/auth-routes.ts
486
+ var MIN_PASSWORD_LENGTH = 8;
487
+ function isValidEmail(email) {
488
+ if (email.length === 0 || email.length > 254) {
489
+ return false;
490
+ }
491
+ const atIndex = email.indexOf("@");
492
+ if (atIndex < 1) {
493
+ return false;
494
+ }
495
+ const domain = email.slice(atIndex + 1);
496
+ if (domain.length === 0 || !domain.includes(".")) {
497
+ return false;
498
+ }
499
+ if (email.indexOf("@", atIndex + 1) !== -1) {
500
+ return false;
501
+ }
502
+ if (email.includes(" ")) {
503
+ return false;
504
+ }
505
+ return true;
506
+ }
507
+ var BuiltInAuthRoutes = class {
508
+ userStore;
509
+ tokenManager;
510
+ constructor(config) {
511
+ this.userStore = config.userStore;
512
+ this.tokenManager = config.tokenManager;
513
+ }
514
+ /**
515
+ * Handle user sign-up (POST /auth/signup).
516
+ *
517
+ * Validates email format and password length, hashes the password,
518
+ * creates the user, optionally registers a device, and issues tokens.
519
+ *
520
+ * @param body - Sign-up request body
521
+ * @param body.email - The user's email address
522
+ * @param body.password - The plaintext password (min 8 characters)
523
+ * @param body.name - Optional display name (defaults to email local part)
524
+ * @param body.deviceId - Optional device ID to register
525
+ * @param body.devicePublicKey - Optional device public key (base64url)
526
+ * @returns Auth response with the created user and tokens, or an error
527
+ */
528
+ async handleSignUp(body) {
529
+ if (!isValidEmail(body.email)) {
530
+ return {
531
+ status: 400,
532
+ body: {
533
+ error: "Invalid email address. Please provide a valid email in the format user@domain.com."
534
+ }
535
+ };
536
+ }
537
+ if (body.password.length < MIN_PASSWORD_LENGTH) {
538
+ return {
539
+ status: 400,
540
+ body: {
541
+ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters long.`
542
+ }
543
+ };
544
+ }
545
+ const { hash, salt } = await hashPassword(body.password);
546
+ let user;
547
+ try {
548
+ user = await this.userStore.createUser({
549
+ email: body.email,
550
+ passwordHash: hash,
551
+ salt,
552
+ name: body.name ?? body.email.split("@")[0] ?? body.email
553
+ });
554
+ } catch (err) {
555
+ if (err instanceof Error && err.name === "DuplicateEmailError") {
556
+ return {
557
+ status: 409,
558
+ body: { error: "An account with this email already exists." }
559
+ };
560
+ }
561
+ throw err;
562
+ }
563
+ const deviceId = body.deviceId ?? `device-${user.id}`;
564
+ if (body.deviceId !== void 0 && body.devicePublicKey !== void 0) {
565
+ await this.userStore.registerDevice({
566
+ id: body.deviceId,
567
+ userId: user.id,
568
+ publicKey: body.devicePublicKey,
569
+ name: "Primary Device"
570
+ });
571
+ }
572
+ const tokens = this.tokenManager.issueTokens(user.id, deviceId);
573
+ return {
574
+ status: 201,
575
+ body: { data: { user, tokens } }
576
+ };
577
+ }
578
+ /**
579
+ * Handle user sign-in (POST /auth/signin).
580
+ *
581
+ * Looks up the user by email, verifies the password, optionally registers
582
+ * a new device, and issues tokens.
583
+ *
584
+ * @param body - Sign-in request body
585
+ * @param body.email - The user's email address
586
+ * @param body.password - The plaintext password
587
+ * @param body.deviceId - Optional device ID to register
588
+ * @param body.devicePublicKey - Optional device public key (base64url)
589
+ * @returns Auth response with the user and tokens, or an error
590
+ */
591
+ async handleSignIn(body) {
592
+ const storedUser = await this.userStore.findByEmail(body.email);
593
+ if (storedUser === null) {
594
+ return {
595
+ status: 401,
596
+ body: { error: "Invalid email or password." }
597
+ };
598
+ }
599
+ const passwordValid = await verifyPassword(
600
+ body.password,
601
+ storedUser.passwordHash,
602
+ storedUser.salt
603
+ );
604
+ if (!passwordValid) {
605
+ return {
606
+ status: 401,
607
+ body: { error: "Invalid email or password." }
608
+ };
609
+ }
610
+ const deviceId = body.deviceId ?? `device-${storedUser.id}`;
611
+ if (body.deviceId !== void 0 && body.devicePublicKey !== void 0) {
612
+ await this.userStore.registerDevice({
613
+ id: body.deviceId,
614
+ userId: storedUser.id,
615
+ publicKey: body.devicePublicKey,
616
+ name: "Device"
617
+ });
618
+ }
619
+ const tokens = this.tokenManager.issueTokens(storedUser.id, deviceId);
620
+ const user = {
621
+ id: storedUser.id,
622
+ email: storedUser.email,
623
+ name: storedUser.name,
624
+ createdAt: storedUser.createdAt
625
+ };
626
+ return {
627
+ status: 200,
628
+ body: { data: { user, tokens } }
629
+ };
630
+ }
631
+ /**
632
+ * Handle token refresh (POST /auth/refresh).
633
+ *
634
+ * Validates the provided refresh token and issues a new token pair
635
+ * (refresh token rotation). The old refresh token should be considered
636
+ * consumed after this call.
637
+ *
638
+ * @param body - Refresh request body
639
+ * @param body.refreshToken - The current refresh token
640
+ * @returns Auth response with new tokens, or an error
641
+ */
642
+ async handleRefresh(body) {
643
+ const result = this.tokenManager.refreshAccessToken(body.refreshToken);
644
+ if (result === null) {
645
+ return {
646
+ status: 401,
647
+ body: { error: "Invalid or expired refresh token." }
648
+ };
649
+ }
650
+ return {
651
+ status: 200,
652
+ body: { data: result }
653
+ };
654
+ }
655
+ /**
656
+ * Handle get-current-user (GET /auth/me).
657
+ *
658
+ * Validates the access token and returns the authenticated user's profile.
659
+ *
660
+ * @param accessToken - The JWT access token (without "Bearer " prefix)
661
+ * @returns Auth response with the user profile, or an error
662
+ */
663
+ async handleGetMe(accessToken) {
664
+ const payload = this.tokenManager.validateToken(accessToken);
665
+ if (payload === null || payload.type !== "access") {
666
+ return {
667
+ status: 401,
668
+ body: { error: "Invalid or expired access token." }
669
+ };
670
+ }
671
+ const storedUser = await this.userStore.findById(payload.sub);
672
+ if (storedUser === null) {
673
+ return {
674
+ status: 404,
675
+ body: { error: "User not found." }
676
+ };
677
+ }
678
+ const user = {
679
+ id: storedUser.id,
680
+ email: storedUser.email,
681
+ name: storedUser.name,
682
+ createdAt: storedUser.createdAt
683
+ };
684
+ return {
685
+ status: 200,
686
+ body: { data: user }
687
+ };
688
+ }
689
+ /**
690
+ * Handle list-devices (GET /auth/devices).
691
+ *
692
+ * Validates the access token and returns all devices registered for the user.
693
+ *
694
+ * @param accessToken - The JWT access token (without "Bearer " prefix)
695
+ * @returns Auth response with the device list, or an error
696
+ */
697
+ async handleListDevices(accessToken) {
698
+ const payload = this.tokenManager.validateToken(accessToken);
699
+ if (payload === null || payload.type !== "access") {
700
+ return {
701
+ status: 401,
702
+ body: { error: "Invalid or expired access token." }
703
+ };
704
+ }
705
+ const devices = await this.userStore.listDevices(payload.sub);
706
+ return {
707
+ status: 200,
708
+ body: { data: devices }
709
+ };
710
+ }
711
+ /**
712
+ * Handle device revocation (DELETE /auth/device/:id).
713
+ *
714
+ * Validates the access token and revokes the specified device.
715
+ * Only the device's owner can revoke it.
716
+ *
717
+ * @param accessToken - The JWT access token (without "Bearer " prefix)
718
+ * @param deviceId - The ID of the device to revoke
719
+ * @returns Auth response with success flag, or an error
720
+ */
721
+ async handleRevokeDevice(accessToken, deviceId) {
722
+ const payload = this.tokenManager.validateToken(accessToken);
723
+ if (payload === null || payload.type !== "access") {
724
+ return {
725
+ status: 401,
726
+ body: { error: "Invalid or expired access token." }
727
+ };
728
+ }
729
+ const device = await this.userStore.findDevice(deviceId);
730
+ if (device === null) {
731
+ return {
732
+ status: 404,
733
+ body: { error: "Device not found." }
734
+ };
735
+ }
736
+ if (device.userId !== payload.sub) {
737
+ return {
738
+ status: 403,
739
+ body: { error: "You can only revoke your own devices." }
740
+ };
741
+ }
742
+ await this.userStore.revokeDevice(deviceId);
743
+ return {
744
+ status: 200,
745
+ body: { data: { success: true } }
746
+ };
747
+ }
748
+ /**
749
+ * Creates a sync server auth provider compatible with `@korajs/server`.
750
+ *
751
+ * The returned object implements the `AuthProvider` interface from
752
+ * `@korajs/server`, validating access tokens and returning an auth
753
+ * context containing the user ID and device metadata. This bridges
754
+ * the built-in auth system with the sync server's authentication layer.
755
+ *
756
+ * @returns An object with an `authenticate` method suitable for KoraSyncServer's `auth` config
757
+ *
758
+ * @example
759
+ * ```typescript
760
+ * const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
761
+ * const syncServer = new KoraSyncServer({
762
+ * store,
763
+ * auth: routes.toSyncAuthProvider(),
764
+ * })
765
+ * ```
766
+ */
767
+ toSyncAuthProvider() {
768
+ const tokenManager = this.tokenManager;
769
+ const userStore = this.userStore;
770
+ return {
771
+ async authenticate(token) {
772
+ const payload = tokenManager.validateToken(token);
773
+ if (payload === null || payload.type !== "access") {
774
+ return null;
775
+ }
776
+ const user = await userStore.findById(payload.sub);
777
+ if (user === null) {
778
+ return null;
779
+ }
780
+ await userStore.touchDevice(payload.dev);
781
+ return {
782
+ userId: payload.sub,
783
+ metadata: {
784
+ deviceId: payload.dev,
785
+ email: user.email,
786
+ name: user.name
787
+ }
788
+ };
789
+ }
790
+ };
791
+ }
792
+ };
793
+
794
+ // src/provider/adapter.ts
795
+ var AuthProviderError = class extends Error {
796
+ /** HTTP-style status code */
797
+ status;
798
+ constructor(message, status) {
799
+ super(message);
800
+ this.name = "AuthProviderError";
801
+ this.status = status;
802
+ }
803
+ };
804
+ var BuiltInProvider = class {
805
+ routes;
806
+ tokenManager;
807
+ userStore;
808
+ constructor(config) {
809
+ this.routes = new BuiltInAuthRoutes(config);
810
+ this.tokenManager = config.tokenManager;
811
+ this.userStore = config.userStore;
812
+ }
813
+ /** @inheritdoc */
814
+ async signUp(params) {
815
+ const result = await this.routes.handleSignUp(params);
816
+ if ("error" in result.body) {
817
+ throw new AuthProviderError(result.body.error, result.status);
818
+ }
819
+ return result.body.data;
820
+ }
821
+ /** @inheritdoc */
822
+ async signIn(params) {
823
+ const result = await this.routes.handleSignIn(params);
824
+ if ("error" in result.body) {
825
+ throw new AuthProviderError(result.body.error, result.status);
826
+ }
827
+ return result.body.data;
828
+ }
829
+ /** @inheritdoc */
830
+ async refreshTokens(refreshToken) {
831
+ const result = await this.routes.handleRefresh({ refreshToken });
832
+ if ("error" in result.body) {
833
+ throw new AuthProviderError(result.body.error, result.status);
834
+ }
835
+ return result.body.data;
836
+ }
837
+ /** @inheritdoc */
838
+ async validateAccessToken(token) {
839
+ const payload = this.tokenManager.validateToken(token);
840
+ if (payload === null || payload.type !== "access") {
841
+ return null;
842
+ }
843
+ return { userId: payload.sub, deviceId: payload.dev };
844
+ }
845
+ /** @inheritdoc */
846
+ async getUser(userId) {
847
+ const stored = await this.userStore.findById(userId);
848
+ if (stored === null) {
849
+ return null;
850
+ }
851
+ return {
852
+ id: stored.id,
853
+ email: stored.email,
854
+ name: stored.name,
855
+ createdAt: stored.createdAt
856
+ };
857
+ }
858
+ /** @inheritdoc */
859
+ async revokeDevice(accessToken, deviceId) {
860
+ const result = await this.routes.handleRevokeDevice(accessToken, deviceId);
861
+ if ("error" in result.body) {
862
+ throw new AuthProviderError(result.body.error, result.status);
863
+ }
864
+ }
865
+ /** @inheritdoc */
866
+ async listDevices(accessToken) {
867
+ const result = await this.routes.handleListDevices(accessToken);
868
+ if ("error" in result.body) {
869
+ throw new AuthProviderError(result.body.error, result.status);
870
+ }
871
+ return result.body.data;
872
+ }
873
+ };
874
+ export {
875
+ AuthProviderError,
876
+ BuiltInAuthRoutes,
877
+ BuiltInProvider,
878
+ DuplicateEmailError,
879
+ InMemoryUserStore,
880
+ TokenManager,
881
+ computePublicKeyThumbprint,
882
+ decodeJwt,
883
+ encodeJwt,
884
+ hashPassword,
885
+ isExpired,
886
+ verifyChallenge,
887
+ verifyJwt,
888
+ verifyPassword
889
+ };
890
+ //# sourceMappingURL=server.js.map