@korajs/auth 0.4.0 → 0.5.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.cjs CHANGED
@@ -30,10 +30,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
- // ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
33
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
34
34
  var getImportMetaUrl, importMetaUrl;
35
35
  var init_cjs_shims = __esm({
36
- "../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js"() {
36
+ "../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js"() {
37
37
  "use strict";
38
38
  getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
39
39
  importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
@@ -109,6 +109,7 @@ __export(server_exports, {
109
109
  CannotRemoveOwnerError: () => CannotRemoveOwnerError,
110
110
  CircularInheritanceError: () => CircularInheritanceError,
111
111
  DuplicateEmailError: () => DuplicateEmailError,
112
+ DuplicateLinkedIdentityError: () => DuplicateLinkedIdentityError,
112
113
  EmailVerificationError: () => EmailVerificationError,
113
114
  EmailVerificationManager: () => EmailVerificationManager,
114
115
  ExternalAuthOperationNotSupportedError: () => ExternalAuthOperationNotSupportedError,
@@ -118,6 +119,7 @@ __export(server_exports, {
118
119
  InMemoryAuditLogStore: () => InMemoryAuditLogStore,
119
120
  InMemoryChallengeStore: () => InMemoryChallengeStore,
120
121
  InMemoryEmailVerificationStore: () => InMemoryEmailVerificationStore,
122
+ InMemoryLinkedIdentityStore: () => InMemoryLinkedIdentityStore,
121
123
  InMemoryOAuthStateStore: () => InMemoryOAuthStateStore,
122
124
  InMemoryOrgStore: () => InMemoryOrgStore,
123
125
  InMemoryPasswordResetStore: () => InMemoryPasswordResetStore,
@@ -150,6 +152,8 @@ __export(server_exports, {
150
152
  PasskeyVerificationError: () => PasskeyVerificationError,
151
153
  PasswordResetError: () => PasswordResetError,
152
154
  PasswordResetManager: () => PasswordResetManager,
155
+ PostgresLinkedIdentityStore: () => PostgresLinkedIdentityStore,
156
+ PostgresOAuthStateStore: () => PostgresOAuthStateStore,
153
157
  PostgresUserStore: () => PostgresUserStore,
154
158
  ROLE_HIERARCHY: () => ROLE_HIERARCHY,
155
159
  RbacEngine: () => RbacEngine,
@@ -164,6 +168,8 @@ __export(server_exports, {
164
168
  SessionManager: () => SessionManager,
165
169
  SessionMfaRequiredError: () => SessionMfaRequiredError,
166
170
  SessionNotFoundError: () => SessionNotFoundError,
171
+ SqliteLinkedIdentityStore: () => SqliteLinkedIdentityStore,
172
+ SqliteOAuthStateStore: () => SqliteOAuthStateStore,
167
173
  SqliteUserStore: () => SqliteUserStore,
168
174
  TokenManager: () => TokenManager,
169
175
  TotpAlreadyEnabledError: () => TotpAlreadyEnabledError,
@@ -182,7 +188,14 @@ __export(server_exports, {
182
188
  base32Encode: () => base32Encode,
183
189
  computePublicKeyThumbprint: () => computePublicKeyThumbprint,
184
190
  createClerkAdapter: () => createClerkAdapter,
191
+ createKoraAuthServer: () => createKoraAuthServer,
192
+ createPostgresLinkedIdentityStore: () => createPostgresLinkedIdentityStore,
193
+ createPostgresOAuthStateStore: () => createPostgresOAuthStateStore,
194
+ createPostgresOAuthStores: () => createPostgresOAuthStores,
185
195
  createPostgresUserStore: () => createPostgresUserStore,
196
+ createSqliteLinkedIdentityStore: () => createSqliteLinkedIdentityStore,
197
+ createSqliteOAuthStateStore: () => createSqliteOAuthStateStore,
198
+ createSqliteOAuthStores: () => createSqliteOAuthStores,
186
199
  createSqliteUserStore: () => createSqliteUserStore,
187
200
  createSupabaseAdapter: () => createSupabaseAdapter,
188
201
  decodeJwt: () => decodeJwt,
@@ -950,7 +963,7 @@ var BuiltInAuthRoutes = class {
950
963
  const userStore = this.userStore;
951
964
  return {
952
965
  async authenticate(token) {
953
- const payload = tokenManager.validateToken(token);
966
+ const payload = await tokenManager.validateTokenWithRevocation(token);
954
967
  if (payload === null || payload.type !== "access") {
955
968
  return null;
956
969
  }
@@ -976,6 +989,10 @@ var BuiltInAuthRoutes = class {
976
989
  }
977
990
  };
978
991
 
992
+ // src/provider/built-in/quickstart-server.ts
993
+ init_cjs_shims();
994
+ var import_node_crypto7 = require("crypto");
995
+
979
996
  // src/tokens/token-manager.ts
980
997
  init_cjs_shims();
981
998
  var import_node_crypto4 = require("crypto");
@@ -1091,7 +1108,7 @@ var InMemoryTokenRevocationStore = class {
1091
1108
  /**
1092
1109
  * Check if a device has been revoked.
1093
1110
  */
1094
- isDeviceRevoked(deviceId) {
1111
+ async isDeviceRevoked(deviceId) {
1095
1112
  return this.revokedDevices.has(deviceId);
1096
1113
  }
1097
1114
  /**
@@ -1284,7 +1301,7 @@ var TokenManager = class {
1284
1301
  * Validate a token and check it against the revocation store.
1285
1302
  *
1286
1303
  * Like {@link validateToken}, but also checks whether the token's `jti` has been
1287
- * revoked. Requires a revocation store to be configured.
1304
+ * revoked or belongs to a revoked device. Requires a revocation store to be configured.
1288
1305
  *
1289
1306
  * @param token - The JWT string to validate
1290
1307
  * @returns The decoded {@link TokenPayload} if valid and not revoked, or null otherwise
@@ -1299,6 +1316,10 @@ var TokenManager = class {
1299
1316
  if (revoked) {
1300
1317
  return null;
1301
1318
  }
1319
+ const deviceRevoked = await this.revocationStore.isDeviceRevoked(payload.dev);
1320
+ if (deviceRevoked) {
1321
+ return null;
1322
+ }
1302
1323
  }
1303
1324
  return payload;
1304
1325
  }
@@ -1365,223 +1386,1014 @@ var TokenManager = class {
1365
1386
  }
1366
1387
  };
1367
1388
 
1368
- // src/server.ts
1369
- init_password_hash();
1370
-
1371
- // src/provider/built-in/password-reset.ts
1389
+ // src/provider/oauth/linked-identity-store.ts
1372
1390
  init_cjs_shims();
1391
+ var import_node_crypto5 = require("crypto");
1373
1392
  var import_core3 = require("@korajs/core");
1374
- init_password_hash();
1375
- var PasswordResetError = class extends import_core3.KoraError {
1393
+ var DuplicateLinkedIdentityError = class extends import_core3.KoraError {
1394
+ constructor(provider) {
1395
+ super(`This ${provider} account is already linked.`, "DUPLICATE_LINKED_IDENTITY", {
1396
+ provider
1397
+ });
1398
+ this.name = "DuplicateLinkedIdentityError";
1399
+ }
1400
+ };
1401
+ var InMemoryLinkedIdentityStore = class {
1402
+ identitiesById = /* @__PURE__ */ new Map();
1403
+ identityIdByProvider = /* @__PURE__ */ new Map();
1404
+ identityIdsByUser = /* @__PURE__ */ new Map();
1405
+ identityIdByUserProvider = /* @__PURE__ */ new Map();
1406
+ async findByProvider(provider, providerUserId) {
1407
+ const id = this.identityIdByProvider.get(providerIdentityKey(provider, providerUserId));
1408
+ return id ? this.identitiesById.get(id) ?? null : null;
1409
+ }
1410
+ async findByUser(userId) {
1411
+ const ids = this.identityIdsByUser.get(userId);
1412
+ if (!ids) return [];
1413
+ return [...ids].map((id) => this.identitiesById.get(id)).filter((identity) => identity !== void 0);
1414
+ }
1415
+ async create(params) {
1416
+ const providerKey = providerIdentityKey(params.provider, params.providerUserId);
1417
+ const userProviderKeyValue = userProviderKey(params.userId, params.provider);
1418
+ if (this.identityIdByProvider.has(providerKey) || this.identityIdByUserProvider.has(userProviderKeyValue)) {
1419
+ throw new DuplicateLinkedIdentityError(params.provider);
1420
+ }
1421
+ const identity = {
1422
+ id: (0, import_node_crypto5.randomUUID)(),
1423
+ userId: params.userId,
1424
+ provider: params.provider,
1425
+ providerUserId: params.providerUserId,
1426
+ email: params.email,
1427
+ linkedAt: Date.now()
1428
+ };
1429
+ this.identitiesById.set(identity.id, identity);
1430
+ this.identityIdByProvider.set(providerKey, identity.id);
1431
+ this.identityIdByUserProvider.set(userProviderKeyValue, identity.id);
1432
+ const userIdentities = this.identityIdsByUser.get(params.userId) ?? /* @__PURE__ */ new Set();
1433
+ userIdentities.add(identity.id);
1434
+ this.identityIdsByUser.set(params.userId, userIdentities);
1435
+ return identity;
1436
+ }
1437
+ async delete(userId, provider) {
1438
+ const userProviderKeyValue = userProviderKey(userId, provider);
1439
+ const id = this.identityIdByUserProvider.get(userProviderKeyValue);
1440
+ if (!id) return;
1441
+ const identity = this.identitiesById.get(id);
1442
+ this.identitiesById.delete(id);
1443
+ this.identityIdByUserProvider.delete(userProviderKeyValue);
1444
+ if (identity) {
1445
+ this.identityIdByProvider.delete(
1446
+ providerIdentityKey(identity.provider, identity.providerUserId)
1447
+ );
1448
+ }
1449
+ const userIdentities = this.identityIdsByUser.get(userId);
1450
+ userIdentities?.delete(id);
1451
+ if (userIdentities?.size === 0) {
1452
+ this.identityIdsByUser.delete(userId);
1453
+ }
1454
+ }
1455
+ };
1456
+ function providerIdentityKey(provider, providerUserId) {
1457
+ return `${provider}:${providerUserId}`;
1458
+ }
1459
+ function userProviderKey(userId, provider) {
1460
+ return `${userId}:${provider}`;
1461
+ }
1462
+
1463
+ // src/provider/oauth/oauth-flow.ts
1464
+ init_cjs_shims();
1465
+
1466
+ // src/provider/oauth/oauth-types.ts
1467
+ init_cjs_shims();
1468
+ var import_core4 = require("@korajs/core");
1469
+ var OAuthError = class extends import_core4.KoraError {
1376
1470
  constructor(message, code, context) {
1377
1471
  super(message, code, context);
1378
- this.name = "PasswordResetError";
1472
+ this.name = "OAuthError";
1379
1473
  }
1380
1474
  };
1381
- var ResetTokenExpiredError = class extends PasswordResetError {
1475
+ var OAuthStateMismatchError = class extends OAuthError {
1382
1476
  constructor() {
1383
- super("Password reset token has expired.", "RESET_TOKEN_EXPIRED");
1477
+ super("OAuth state parameter does not match. Possible CSRF attack.", "OAUTH_STATE_MISMATCH");
1384
1478
  }
1385
1479
  };
1386
- var ResetTokenNotFoundError = class extends PasswordResetError {
1387
- constructor() {
1388
- super("Password reset token not found or already used.", "RESET_TOKEN_NOT_FOUND");
1480
+ var OAuthCodeExchangeError = class extends OAuthError {
1481
+ constructor(details) {
1482
+ super(
1483
+ `Failed to exchange authorization code for tokens.${details ? ` ${details}` : ""}`,
1484
+ "OAUTH_CODE_EXCHANGE_FAILED",
1485
+ details ? { details } : void 0
1486
+ );
1389
1487
  }
1390
1488
  };
1391
- var ResetRateLimitedError = class extends PasswordResetError {
1392
- constructor() {
1393
- super("Too many password reset requests. Please try again later.", "RESET_RATE_LIMITED");
1489
+ var OAuthUserInfoError = class extends OAuthError {
1490
+ constructor(details) {
1491
+ super(
1492
+ `Failed to fetch user info from OAuth provider.${details ? ` ${details}` : ""}`,
1493
+ "OAUTH_USER_INFO_FAILED",
1494
+ details ? { details } : void 0
1495
+ );
1394
1496
  }
1395
1497
  };
1396
- var InMemoryPasswordResetStore = class {
1397
- tokens = /* @__PURE__ */ new Map();
1398
- async store(token) {
1399
- this.tokens.set(token.token, token);
1400
- }
1401
- async get(token) {
1402
- return this.tokens.get(token) ?? null;
1498
+ var OAuthProviderNotFoundError = class extends OAuthError {
1499
+ constructor(provider) {
1500
+ super(`OAuth provider "${provider}" is not configured.`, "OAUTH_PROVIDER_NOT_FOUND", {
1501
+ provider
1502
+ });
1403
1503
  }
1404
- async consume(token) {
1405
- const entry = this.tokens.get(token);
1406
- if (entry) {
1407
- this.tokens.set(token, { ...entry, consumed: true });
1408
- }
1504
+ };
1505
+
1506
+ // src/provider/oauth/oauth-flow.ts
1507
+ var DEFAULT_STATE_TTL_MS = 10 * 60 * 1e3;
1508
+ var PKCE_VERIFIER_BYTES = 32;
1509
+ var InMemoryOAuthStateStore = class {
1510
+ states = /* @__PURE__ */ new Map();
1511
+ async store(state) {
1512
+ this.states.set(state.state, state);
1409
1513
  }
1410
- async countActiveForEmail(email) {
1411
- const now = Date.now();
1412
- let count = 0;
1413
- for (const token of this.tokens.values()) {
1414
- if (token.email === email && !token.consumed && now < token.expiresAt) {
1415
- count++;
1416
- }
1417
- }
1418
- return count;
1514
+ async consume(stateValue) {
1515
+ const state = this.states.get(stateValue);
1516
+ if (!state) return null;
1517
+ this.states.delete(stateValue);
1518
+ if (Date.now() > state.expiresAt) return null;
1519
+ return state;
1419
1520
  }
1420
1521
  async cleanExpired() {
1421
1522
  const now = Date.now();
1422
1523
  let count = 0;
1423
- for (const [key, token] of this.tokens) {
1424
- if (now > token.expiresAt) {
1425
- this.tokens.delete(key);
1524
+ for (const [key, state] of this.states) {
1525
+ if (now > state.expiresAt) {
1526
+ this.states.delete(key);
1426
1527
  count++;
1427
1528
  }
1428
1529
  }
1429
1530
  return count;
1430
1531
  }
1431
1532
  };
1432
- var DEFAULT_TOKEN_TTL_MS = 60 * 60 * 1e3;
1433
- var DEFAULT_MAX_REQUESTS = 3;
1434
- var MIN_PASSWORD_LENGTH2 = 8;
1435
- var MAX_PASSWORD_LENGTH2 = 128;
1436
- var PasswordResetManager = class {
1437
- userStore;
1438
- resetStore;
1439
- tokenTtlMs;
1440
- maxRequestsPerEmail;
1441
- onResetRequested;
1533
+ var OAuthManager = class {
1534
+ providers = /* @__PURE__ */ new Map();
1535
+ stateStore;
1536
+ stateTtlMs;
1537
+ fetchFn;
1442
1538
  constructor(config) {
1443
- this.userStore = config.userStore;
1444
- this.resetStore = config.resetStore ?? new InMemoryPasswordResetStore();
1445
- this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
1446
- this.maxRequestsPerEmail = config.maxRequestsPerEmail ?? DEFAULT_MAX_REQUESTS;
1447
- this.onResetRequested = config.onResetRequested;
1539
+ for (const provider of config.providers) {
1540
+ this.providers.set(provider.providerId, provider);
1541
+ }
1542
+ this.stateStore = config.stateStore ?? new InMemoryOAuthStateStore();
1543
+ this.stateTtlMs = config.stateTtlMs ?? DEFAULT_STATE_TTL_MS;
1544
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
1448
1545
  }
1449
1546
  /**
1450
- * Request a password reset for an email.
1451
- * Always returns success to prevent email enumeration.
1452
- *
1453
- * If a callback is configured, invokes it with the token.
1454
- * In development mode (no callback), returns the token in the response.
1547
+ * Generate an authorization URL for the user to visit.
1548
+ * Returns the URL and the state parameter for CSRF validation.
1455
1549
  */
1456
- async requestReset(email) {
1457
- const normalizedEmail = email.toLowerCase().trim();
1458
- const successResponse = {
1459
- status: 200,
1460
- body: {
1461
- data: {
1462
- message: "If an account with that email exists, a password reset link has been sent."
1463
- }
1464
- }
1465
- };
1466
- const user = await this.userStore.findByEmail(normalizedEmail);
1467
- if (!user) {
1468
- return successResponse;
1469
- }
1470
- const activeCount = await this.resetStore.countActiveForEmail(normalizedEmail);
1471
- if (activeCount >= this.maxRequestsPerEmail) {
1472
- return successResponse;
1473
- }
1474
- const token = generateSecureToken();
1550
+ async getAuthorizationUrl(providerId, metadata) {
1551
+ const provider = this.getProvider(providerId);
1552
+ const state = generateState();
1553
+ const codeVerifier = provider.pkce ? generateCodeVerifier() : void 0;
1554
+ const codeChallenge = codeVerifier ? await createCodeChallenge(codeVerifier) : void 0;
1475
1555
  const now = Date.now();
1476
- const resetToken = {
1477
- token,
1478
- userId: user.id,
1479
- email: normalizedEmail,
1556
+ const oauthState = {
1557
+ state,
1558
+ provider: providerId,
1559
+ redirectUri: provider.redirectUri,
1480
1560
  createdAt: now,
1481
- expiresAt: now + this.tokenTtlMs,
1482
- consumed: false
1561
+ expiresAt: now + this.stateTtlMs,
1562
+ metadata,
1563
+ codeVerifier
1483
1564
  };
1484
- await this.resetStore.store(resetToken);
1485
- if (this.onResetRequested) {
1486
- try {
1487
- await this.onResetRequested(normalizedEmail, token, resetToken.expiresAt);
1488
- } catch {
1489
- }
1490
- }
1491
- if (!this.onResetRequested) {
1492
- successResponse.body = { data: { message: "Password reset token generated.", token } };
1565
+ await this.stateStore.store(oauthState);
1566
+ const params = new URLSearchParams({
1567
+ client_id: provider.clientId,
1568
+ redirect_uri: provider.redirectUri,
1569
+ response_type: "code",
1570
+ scope: provider.scopes.join(" "),
1571
+ state
1572
+ });
1573
+ if (codeChallenge) {
1574
+ params.set("code_challenge", codeChallenge);
1575
+ params.set("code_challenge_method", "S256");
1493
1576
  }
1494
- return successResponse;
1577
+ const url = `${provider.authorizationUrl}?${params.toString()}`;
1578
+ return { url, state };
1495
1579
  }
1496
1580
  /**
1497
- * Consume a reset token and set a new password.
1581
+ * Handle the OAuth callback after the user authorizes.
1582
+ * Validates the state parameter, exchanges the code for tokens,
1583
+ * and fetches user info.
1584
+ *
1585
+ * @param providerId - The OAuth provider
1586
+ * @param code - The authorization code from the callback
1587
+ * @param state - The state parameter from the callback
1588
+ * @returns Tokens and user info from the provider
1498
1589
  */
1499
- async resetPassword(token, newPassword) {
1500
- if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
1501
- return {
1502
- status: 400,
1503
- body: { error: `Password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
1504
- };
1505
- }
1506
- if (newPassword.length > MAX_PASSWORD_LENGTH2) {
1507
- return {
1508
- status: 400,
1509
- body: { error: `Password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
1510
- };
1511
- }
1512
- const resetToken = await this.resetStore.get(token);
1513
- if (!resetToken || resetToken.consumed) {
1514
- return { status: 404, body: { error: "Password reset token not found or already used." } };
1515
- }
1516
- if (Date.now() > resetToken.expiresAt) {
1517
- await this.resetStore.consume(token);
1518
- return { status: 410, body: { error: "Password reset token has expired." } };
1590
+ async handleCallback(providerId, code, state) {
1591
+ const provider = this.getProvider(providerId);
1592
+ const oauthState = await this.stateStore.consume(state);
1593
+ if (!oauthState || oauthState.provider !== providerId) {
1594
+ throw new OAuthStateMismatchError();
1519
1595
  }
1520
- await this.resetStore.consume(token);
1521
- const hashed = await hashPassword(newPassword);
1522
- await this.userStore.updatePassword(resetToken.userId, hashed.hash, hashed.salt);
1523
- return { status: 200, body: { data: { message: "Password has been reset successfully." } } };
1596
+ const tokens = await this.exchangeCodeForTokens(provider, code, oauthState);
1597
+ const userInfo = await this.fetchUserInfo(provider, tokens.accessToken);
1598
+ return { tokens, userInfo, stateMetadata: oauthState.metadata };
1524
1599
  }
1525
1600
  /**
1526
- * Change password for an authenticated user (requires current password verification).
1601
+ * Get a registered provider by ID.
1527
1602
  */
1528
- async changePassword(userId, currentPassword, newPassword) {
1529
- if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
1530
- return {
1531
- status: 400,
1532
- body: { error: `New password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
1533
- };
1534
- }
1535
- if (newPassword.length > MAX_PASSWORD_LENGTH2) {
1536
- return {
1537
- status: 400,
1538
- body: { error: `New password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
1539
- };
1540
- }
1541
- const user = await this.userStore.findById(userId);
1542
- if (!user) {
1543
- return { status: 404, body: { error: "User not found." } };
1603
+ getProvider(providerId) {
1604
+ const provider = this.providers.get(providerId);
1605
+ if (!provider) {
1606
+ throw new OAuthProviderNotFoundError(providerId);
1544
1607
  }
1545
- const { verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password_hash(), password_hash_exports));
1546
- const isValid = await verifyPassword2(currentPassword, user.passwordHash, user.salt);
1547
- if (!isValid) {
1548
- return { status: 401, body: { error: "Current password is incorrect." } };
1608
+ return provider;
1609
+ }
1610
+ /**
1611
+ * List all registered provider IDs.
1612
+ */
1613
+ getProviderIds() {
1614
+ return [...this.providers.keys()];
1615
+ }
1616
+ // --- Private ---
1617
+ async exchangeCodeForTokens(provider, code, oauthState) {
1618
+ const body = new URLSearchParams({
1619
+ grant_type: "authorization_code",
1620
+ code,
1621
+ redirect_uri: provider.redirectUri,
1622
+ client_id: provider.clientId
1623
+ });
1624
+ if (provider.clientSecret) {
1625
+ body.set("client_secret", provider.clientSecret);
1626
+ }
1627
+ if (oauthState.codeVerifier) {
1628
+ body.set("code_verifier", oauthState.codeVerifier);
1629
+ }
1630
+ let response;
1631
+ try {
1632
+ response = await this.fetchFn(provider.tokenUrl, {
1633
+ method: "POST",
1634
+ headers: {
1635
+ "Content-Type": "application/x-www-form-urlencoded",
1636
+ Accept: "application/json"
1637
+ },
1638
+ body: body.toString()
1639
+ });
1640
+ } catch (err) {
1641
+ throw new OAuthCodeExchangeError(err instanceof Error ? err.message : "Network error");
1642
+ }
1643
+ if (!response.ok) {
1644
+ let details = `HTTP ${response.status}`;
1645
+ try {
1646
+ const errorBody = await response.text();
1647
+ details += `: ${errorBody}`;
1648
+ } catch {
1649
+ }
1650
+ throw new OAuthCodeExchangeError(details);
1651
+ }
1652
+ const data = await response.json();
1653
+ return {
1654
+ accessToken: data.access_token,
1655
+ tokenType: data.token_type ?? "Bearer",
1656
+ expiresIn: data.expires_in,
1657
+ refreshToken: data.refresh_token,
1658
+ idToken: data.id_token,
1659
+ scope: data.scope
1660
+ };
1661
+ }
1662
+ async fetchUserInfo(provider, accessToken) {
1663
+ let response;
1664
+ try {
1665
+ response = await this.fetchFn(provider.userInfoUrl, {
1666
+ headers: {
1667
+ Authorization: `Bearer ${accessToken}`,
1668
+ Accept: "application/json"
1669
+ }
1670
+ });
1671
+ } catch (err) {
1672
+ throw new OAuthUserInfoError(err instanceof Error ? err.message : "Network error");
1673
+ }
1674
+ if (!response.ok) {
1675
+ throw new OAuthUserInfoError(`HTTP ${response.status}`);
1676
+ }
1677
+ const profile = await response.json();
1678
+ return normalizeUserInfo(provider.providerId, profile);
1679
+ }
1680
+ };
1681
+ function normalizeUserInfo(providerId, profile) {
1682
+ switch (providerId) {
1683
+ case "google":
1684
+ return {
1685
+ providerId: profile.sub,
1686
+ provider: "google",
1687
+ email: profile.email ?? null,
1688
+ emailVerified: profile.email_verified ?? false,
1689
+ name: profile.name ?? null,
1690
+ avatarUrl: profile.picture ?? null,
1691
+ rawProfile: profile
1692
+ };
1693
+ case "github":
1694
+ return {
1695
+ providerId: String(profile.id),
1696
+ provider: "github",
1697
+ email: profile.email ?? null,
1698
+ emailVerified: false,
1699
+ // GitHub doesn't confirm in the profile response
1700
+ name: profile.name ?? profile.login ?? null,
1701
+ avatarUrl: profile.avatar_url ?? null,
1702
+ rawProfile: profile
1703
+ };
1704
+ case "microsoft":
1705
+ return {
1706
+ providerId: profile.id,
1707
+ provider: "microsoft",
1708
+ email: profile.mail ?? profile.userPrincipalName ?? null,
1709
+ emailVerified: false,
1710
+ name: profile.displayName ?? null,
1711
+ avatarUrl: null,
1712
+ rawProfile: profile
1713
+ };
1714
+ default:
1715
+ return {
1716
+ providerId: String(profile.id ?? profile.sub ?? ""),
1717
+ provider: providerId,
1718
+ email: profile.email ?? null,
1719
+ emailVerified: profile.email_verified ?? false,
1720
+ name: profile.name ?? null,
1721
+ avatarUrl: profile.picture ?? profile.avatar_url ?? null,
1722
+ rawProfile: profile
1723
+ };
1724
+ }
1725
+ }
1726
+ function googleProvider(config) {
1727
+ return {
1728
+ providerId: "google",
1729
+ clientId: config.clientId,
1730
+ clientSecret: config.clientSecret,
1731
+ authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1732
+ tokenUrl: "https://oauth2.googleapis.com/token",
1733
+ userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
1734
+ scopes: config.scopes ?? ["openid", "email", "profile"],
1735
+ redirectUri: config.redirectUri,
1736
+ pkce: config.pkce
1737
+ };
1738
+ }
1739
+ function githubProvider(config) {
1740
+ return {
1741
+ providerId: "github",
1742
+ clientId: config.clientId,
1743
+ clientSecret: config.clientSecret,
1744
+ authorizationUrl: "https://github.com/login/oauth/authorize",
1745
+ tokenUrl: "https://github.com/login/oauth/access_token",
1746
+ userInfoUrl: "https://api.github.com/user",
1747
+ scopes: config.scopes ?? ["read:user", "user:email"],
1748
+ redirectUri: config.redirectUri,
1749
+ pkce: config.pkce
1750
+ };
1751
+ }
1752
+ function microsoftProvider(config) {
1753
+ const tenant = config.tenantId ?? "common";
1754
+ return {
1755
+ providerId: "microsoft",
1756
+ clientId: config.clientId,
1757
+ clientSecret: config.clientSecret,
1758
+ authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
1759
+ tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
1760
+ userInfoUrl: "https://graph.microsoft.com/v1.0/me",
1761
+ scopes: config.scopes ?? ["openid", "email", "profile", "User.Read"],
1762
+ redirectUri: config.redirectUri,
1763
+ pkce: config.pkce
1764
+ };
1765
+ }
1766
+ function generateState() {
1767
+ const bytes = new Uint8Array(32);
1768
+ globalThis.crypto.getRandomValues(bytes);
1769
+ return toBase64Url2(bytes);
1770
+ }
1771
+ function generateCodeVerifier() {
1772
+ const bytes = new Uint8Array(PKCE_VERIFIER_BYTES);
1773
+ globalThis.crypto.getRandomValues(bytes);
1774
+ return toBase64Url2(bytes);
1775
+ }
1776
+ async function createCodeChallenge(codeVerifier) {
1777
+ const data = new TextEncoder().encode(codeVerifier);
1778
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
1779
+ return toBase64Url2(new Uint8Array(digest));
1780
+ }
1781
+ function toBase64Url2(bytes) {
1782
+ let binary = "";
1783
+ for (let i = 0; i < bytes.length; i++) {
1784
+ binary += String.fromCharCode(bytes[i]);
1785
+ }
1786
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1787
+ }
1788
+
1789
+ // src/provider/built-in/quickstart-server.ts
1790
+ init_password_hash();
1791
+
1792
+ // src/provider/built-in/user-store.ts
1793
+ init_cjs_shims();
1794
+ var import_node_crypto6 = require("crypto");
1795
+ var import_core5 = require("@korajs/core");
1796
+ var DuplicateEmailError = class extends import_core5.KoraError {
1797
+ constructor() {
1798
+ super("A user with this email already exists.", "DUPLICATE_EMAIL");
1799
+ this.name = "DuplicateEmailError";
1800
+ }
1801
+ };
1802
+ var InMemoryUserStore = class {
1803
+ /** Users indexed by ID */
1804
+ usersById = /* @__PURE__ */ new Map();
1805
+ /** Users indexed by email (lowercase) for fast lookup */
1806
+ usersByEmail = /* @__PURE__ */ new Map();
1807
+ /** Devices indexed by device ID */
1808
+ devicesById = /* @__PURE__ */ new Map();
1809
+ /** Device IDs indexed by user ID for fast listing */
1810
+ devicesByUserId = /* @__PURE__ */ new Map();
1811
+ /**
1812
+ * Create a new user account.
1813
+ *
1814
+ * @param params - User creation parameters
1815
+ * @param params.email - The user's email address (must be unique, case-insensitive)
1816
+ * @param params.passwordHash - Hex-encoded PBKDF2 derived key
1817
+ * @param params.salt - Hex-encoded salt used during hashing
1818
+ * @param params.name - The user's display name
1819
+ * @returns The created user (without sensitive credential fields)
1820
+ * @throws {DuplicateEmailError} If a user with the same email already exists
1821
+ */
1822
+ async createUser(params) {
1823
+ const normalizedEmail = params.email.toLowerCase();
1824
+ if (this.usersByEmail.has(normalizedEmail)) {
1825
+ throw new DuplicateEmailError();
1826
+ }
1827
+ const now = Date.now();
1828
+ const id = (0, import_node_crypto6.randomUUID)();
1829
+ const storedUser = {
1830
+ id,
1831
+ email: normalizedEmail,
1832
+ name: params.name,
1833
+ emailVerified: false,
1834
+ createdAt: now,
1835
+ passwordHash: params.passwordHash,
1836
+ salt: params.salt
1837
+ };
1838
+ this.usersById.set(id, storedUser);
1839
+ this.usersByEmail.set(normalizedEmail, storedUser);
1840
+ return toAuthUser(storedUser);
1841
+ }
1842
+ /**
1843
+ * Find a user by email address.
1844
+ *
1845
+ * @param email - The email to search for (case-insensitive)
1846
+ * @returns The stored user record including credentials, or null if not found
1847
+ */
1848
+ async findByEmail(email) {
1849
+ return this.usersByEmail.get(email.toLowerCase()) ?? null;
1850
+ }
1851
+ /**
1852
+ * Find a user by ID.
1853
+ *
1854
+ * @param id - The user ID to search for
1855
+ * @returns The stored user record including credentials, or null if not found
1856
+ */
1857
+ async findById(id) {
1858
+ return this.usersById.get(id) ?? null;
1859
+ }
1860
+ /**
1861
+ * Register a device for a user.
1862
+ *
1863
+ * If a device with the same ID already exists and is not revoked, it is
1864
+ * returned as-is (idempotent registration). If it was previously revoked,
1865
+ * it is re-activated with updated details.
1866
+ *
1867
+ * @param params - Device registration parameters
1868
+ * @param params.id - Unique device identifier
1869
+ * @param params.userId - ID of the user who owns the device
1870
+ * @param params.publicKey - Base64url-encoded device public key or thumbprint
1871
+ * @param params.name - Human-readable device name
1872
+ * @returns The registered device record
1873
+ */
1874
+ async registerDevice(params) {
1875
+ const existing = this.devicesById.get(params.id);
1876
+ if (existing !== void 0 && !existing.revoked) {
1877
+ return existing;
1878
+ }
1879
+ const now = Date.now();
1880
+ const device = {
1881
+ id: params.id,
1882
+ userId: params.userId,
1883
+ publicKey: params.publicKey,
1884
+ name: params.name,
1885
+ revoked: false,
1886
+ createdAt: now,
1887
+ lastSeenAt: now
1888
+ };
1889
+ this.devicesById.set(params.id, device);
1890
+ let userDevices = this.devicesByUserId.get(params.userId);
1891
+ if (userDevices === void 0) {
1892
+ userDevices = /* @__PURE__ */ new Set();
1893
+ this.devicesByUserId.set(params.userId, userDevices);
1894
+ }
1895
+ userDevices.add(params.id);
1896
+ return device;
1897
+ }
1898
+ /**
1899
+ * Find a device by its ID.
1900
+ *
1901
+ * @param deviceId - The device ID to search for
1902
+ * @returns The device record, or null if not found
1903
+ */
1904
+ async findDevice(deviceId) {
1905
+ return this.devicesById.get(deviceId) ?? null;
1906
+ }
1907
+ /**
1908
+ * List all devices registered for a user.
1909
+ *
1910
+ * @param userId - The user ID whose devices to list
1911
+ * @returns Array of device records (includes revoked devices)
1912
+ */
1913
+ async listDevices(userId) {
1914
+ const deviceIds = this.devicesByUserId.get(userId);
1915
+ if (deviceIds === void 0) {
1916
+ return [];
1917
+ }
1918
+ const devices = [];
1919
+ for (const deviceId of deviceIds) {
1920
+ const device = this.devicesById.get(deviceId);
1921
+ if (device !== void 0) {
1922
+ devices.push(device);
1923
+ }
1924
+ }
1925
+ return devices;
1926
+ }
1927
+ /**
1928
+ * Revoke a device, preventing it from being used for authentication.
1929
+ *
1930
+ * This is a soft revoke — the device record remains but is marked as revoked.
1931
+ * If the device does not exist, this is a no-op.
1932
+ *
1933
+ * @param deviceId - The ID of the device to revoke
1934
+ */
1935
+ async revokeDevice(deviceId) {
1936
+ const device = this.devicesById.get(deviceId);
1937
+ if (device !== void 0) {
1938
+ device.revoked = true;
1939
+ }
1940
+ }
1941
+ /**
1942
+ * Set a user's email verification status.
1943
+ *
1944
+ * @param userId - The user whose email to verify
1945
+ * @param verified - Whether the email is verified
1946
+ */
1947
+ async setEmailVerified(userId, verified) {
1948
+ const user = this.usersById.get(userId);
1949
+ if (!user) return;
1950
+ const updated = { ...user, emailVerified: verified };
1951
+ this.usersById.set(userId, updated);
1952
+ this.usersByEmail.set(user.email, updated);
1953
+ }
1954
+ /**
1955
+ * Update a user's password hash and salt.
1956
+ *
1957
+ * @param userId - The user whose password to update
1958
+ * @param passwordHash - New hex-encoded PBKDF2 derived key
1959
+ * @param salt - New hex-encoded salt
1960
+ */
1961
+ async updatePassword(userId, passwordHash, salt) {
1962
+ const user = this.usersById.get(userId);
1963
+ if (!user) return;
1964
+ const updated = { ...user, passwordHash, salt };
1965
+ this.usersById.set(userId, updated);
1966
+ this.usersByEmail.set(user.email, updated);
1967
+ }
1968
+ /**
1969
+ * List all users. For admin/development use.
1970
+ */
1971
+ async listAll() {
1972
+ return [...this.usersById.values()];
1973
+ }
1974
+ /**
1975
+ * Update a stored user record.
1976
+ */
1977
+ async update(user) {
1978
+ const existing = this.usersById.get(user.id);
1979
+ if (!existing) return;
1980
+ if (existing.email !== user.email) {
1981
+ this.usersByEmail.delete(existing.email);
1982
+ this.usersByEmail.set(user.email, user);
1983
+ } else {
1984
+ this.usersByEmail.set(user.email, user);
1985
+ }
1986
+ this.usersById.set(user.id, user);
1987
+ }
1988
+ /**
1989
+ * Delete a user and all associated devices.
1990
+ */
1991
+ async delete(userId) {
1992
+ const user = this.usersById.get(userId);
1993
+ if (!user) return;
1994
+ this.usersById.delete(userId);
1995
+ this.usersByEmail.delete(user.email);
1996
+ const deviceIds = this.devicesByUserId.get(userId);
1997
+ if (deviceIds) {
1998
+ for (const deviceId of deviceIds) {
1999
+ this.devicesById.delete(deviceId);
2000
+ }
2001
+ this.devicesByUserId.delete(userId);
2002
+ }
2003
+ }
2004
+ /**
2005
+ * Update the last-seen timestamp for a device.
2006
+ *
2007
+ * Called when a device authenticates or syncs to track activity.
2008
+ * If the device does not exist, this is a no-op.
2009
+ *
2010
+ * @param deviceId - The ID of the device to update
2011
+ */
2012
+ async touchDevice(deviceId) {
2013
+ const device = this.devicesById.get(deviceId);
2014
+ if (device !== void 0) {
2015
+ device.lastSeenAt = Date.now();
2016
+ }
2017
+ }
2018
+ };
2019
+ function toAuthUser(stored) {
2020
+ return {
2021
+ id: stored.id,
2022
+ email: stored.email,
2023
+ name: stored.name,
2024
+ emailVerified: stored.emailVerified,
2025
+ createdAt: stored.createdAt
2026
+ };
2027
+ }
2028
+
2029
+ // src/provider/built-in/quickstart-server.ts
2030
+ function createKoraAuthServer(options = {}) {
2031
+ const userStore = options.userStore ?? new InMemoryUserStore();
2032
+ const tokenManager = options.tokenManager ?? createDefaultTokenManager(options);
2033
+ const routes = new BuiltInAuthRoutes({
2034
+ userStore,
2035
+ tokenManager,
2036
+ challengeStore: options.challengeStore,
2037
+ rateLimiter: options.rateLimiter
2038
+ });
2039
+ const oauth = options.oauth ? createOAuthRuntime(options.oauth) : void 0;
2040
+ const path = normalizePath(options.path ?? "/auth");
2041
+ return {
2042
+ routes,
2043
+ userStore,
2044
+ tokenManager,
2045
+ oauth: oauth?.manager,
2046
+ linkedIdentityStore: oauth?.linkedIdentityStore,
2047
+ auth: routes.toSyncAuthProvider(),
2048
+ handleRequest(request) {
2049
+ return handleAuthRequest(routes, path, request, oauth, userStore, tokenManager);
2050
+ }
2051
+ };
2052
+ }
2053
+ function createDefaultTokenManager(options) {
2054
+ const secret = options.jwtSecret ?? readEnvSecret();
2055
+ if (!secret && isProduction()) {
2056
+ throw new Error(
2057
+ "createKoraAuthServer requires jwtSecret in production. Set KORA_AUTH_SECRET or pass jwtSecret."
2058
+ );
2059
+ }
2060
+ return new TokenManager({
2061
+ secret: secret ?? TokenManager.generateSecret(),
2062
+ revocationStore: new InMemoryTokenRevocationStore(),
2063
+ ...options.tokenManagerOptions
2064
+ });
2065
+ }
2066
+ function createOAuthRuntime(config) {
2067
+ return {
2068
+ manager: new OAuthManager(config),
2069
+ linkedIdentityStore: config.linkedIdentityStore ?? new InMemoryLinkedIdentityStore(),
2070
+ createNewUsers: config.createNewUsers ?? true,
2071
+ autoLinkVerifiedEmail: config.autoLinkVerifiedEmail ?? false,
2072
+ allowUnlinkLastIdentity: config.allowUnlinkLastIdentity ?? false
2073
+ };
2074
+ }
2075
+ async function handleAuthRequest(routes, pathPrefix, request, oauth, userStore, tokenManager) {
2076
+ const path = normalizePath(request.path);
2077
+ const relativePath = path === pathPrefix ? "/" : path.slice(pathPrefix.length);
2078
+ const method = request.method.toUpperCase();
2079
+ const body = isRecord(request.body) ? request.body : {};
2080
+ const token = extractBearerToken(request.headers);
2081
+ if (path !== pathPrefix && !path.startsWith(`${pathPrefix}/`)) {
2082
+ return notFound();
2083
+ }
2084
+ if (relativePath.startsWith("/oauth/")) {
2085
+ return handleOAuthRequest({
2086
+ oauth,
2087
+ userStore,
2088
+ tokenManager,
2089
+ relativePath,
2090
+ method,
2091
+ body,
2092
+ query: request.query,
2093
+ token
2094
+ });
2095
+ }
2096
+ if (method === "POST" && relativePath === "/signup") {
2097
+ return routes.handleSignUp(body, request.ip);
2098
+ }
2099
+ if (method === "POST" && relativePath === "/signin") {
2100
+ return routes.handleSignIn(body, request.ip);
2101
+ }
2102
+ if (method === "POST" && relativePath === "/refresh") {
2103
+ return routes.handleRefresh(body);
2104
+ }
2105
+ if (method === "POST" && relativePath === "/signout") {
2106
+ return routes.handleSignOut(token, body);
2107
+ }
2108
+ if (method === "GET" && relativePath === "/me") {
2109
+ return routes.handleGetMe(token);
2110
+ }
2111
+ if (method === "GET" && relativePath === "/devices") {
2112
+ return routes.handleListDevices(token);
2113
+ }
2114
+ if (method === "POST" && relativePath === "/device/register") {
2115
+ return routes.handleDeviceRegister(token, body);
2116
+ }
2117
+ if (method === "POST" && relativePath === "/device/challenge") {
2118
+ const deviceId = typeof body.deviceId === "string" ? body.deviceId : "";
2119
+ return routes.handleDeviceChallenge(token, deviceId);
2120
+ }
2121
+ if (method === "POST" && relativePath === "/device/verify") {
2122
+ return routes.handleDeviceVerify(body);
2123
+ }
2124
+ if (method === "DELETE" && relativePath.startsWith("/device/")) {
2125
+ return routes.handleRevokeDevice(token, relativePath.slice("/device/".length));
2126
+ }
2127
+ return notFound();
2128
+ }
2129
+ async function handleOAuthRequest(params) {
2130
+ const { oauth, userStore, tokenManager, relativePath, method, body, query, token } = params;
2131
+ if (!oauth) {
2132
+ return notFound();
2133
+ }
2134
+ try {
2135
+ if (method === "GET" && relativePath === "/oauth/links") {
2136
+ const authUser = await requireAuthUser(tokenManager, userStore, token);
2137
+ if ("status" in authUser) return authUser;
2138
+ const identities = await oauth.linkedIdentityStore.findByUser(authUser.id);
2139
+ return { status: 200, body: { data: identities } };
2140
+ }
2141
+ const match = /^\/oauth\/([^/]+)(?:\/(callback|link))?$/.exec(relativePath);
2142
+ if (!match) {
2143
+ return notFound();
2144
+ }
2145
+ const provider = decodeURIComponent(match[1]);
2146
+ const action = match[2];
2147
+ if (method === "GET" && !action) {
2148
+ const { url, state } = await oauth.manager.getAuthorizationUrl(
2149
+ provider,
2150
+ metadataFromQuery(query)
2151
+ );
2152
+ return { status: 200, body: { data: { url, state } } };
1549
2153
  }
1550
- const hashed = await hashPassword(newPassword);
1551
- await this.userStore.updatePassword(userId, hashed.hash, hashed.salt);
1552
- return { status: 200, body: { data: { message: "Password changed successfully." } } };
2154
+ if ((method === "GET" || method === "POST") && action === "callback") {
2155
+ const code = readString(method === "GET" ? queryValue(query, "code") : body.code);
2156
+ const state = readString(method === "GET" ? queryValue(query, "state") : body.state);
2157
+ if (!code || !state) {
2158
+ return { status: 400, body: { error: "OAuth callback requires code and state." } };
2159
+ }
2160
+ return completeOAuthSignIn({
2161
+ oauth,
2162
+ userStore,
2163
+ tokenManager,
2164
+ provider,
2165
+ code,
2166
+ state,
2167
+ deviceId: readString(body.deviceId),
2168
+ devicePublicKey: readString(body.devicePublicKey)
2169
+ });
2170
+ }
2171
+ if (method === "POST" && action === "link") {
2172
+ const authUser = await requireAuthUser(tokenManager, userStore, token);
2173
+ if ("status" in authUser) return authUser;
2174
+ const code = readString(body.code);
2175
+ const state = readString(body.state);
2176
+ if (!code || !state) {
2177
+ return { status: 400, body: { error: "OAuth linking requires code and state." } };
2178
+ }
2179
+ return linkOAuthIdentity(oauth, authUser.id, provider, code, state);
2180
+ }
2181
+ if (method === "DELETE" && action === "link") {
2182
+ const authUser = await requireAuthUser(tokenManager, userStore, token);
2183
+ if ("status" in authUser) return authUser;
2184
+ const identities = await oauth.linkedIdentityStore.findByUser(authUser.id);
2185
+ if (!oauth.allowUnlinkLastIdentity && identities.length <= 1) {
2186
+ return {
2187
+ status: 409,
2188
+ body: {
2189
+ error: "Cannot unlink the last OAuth identity unless allowUnlinkLastIdentity is enabled."
2190
+ }
2191
+ };
2192
+ }
2193
+ await oauth.linkedIdentityStore.delete(authUser.id, provider);
2194
+ return { status: 200, body: { data: { ok: true } } };
2195
+ }
2196
+ } catch (error) {
2197
+ return oauthErrorResponse(error);
1553
2198
  }
1554
- };
1555
- function generateSecureToken() {
1556
- const bytes = new Uint8Array(32);
1557
- globalThis.crypto.getRandomValues(bytes);
1558
- let binary = "";
1559
- for (let i = 0; i < bytes.length; i++) {
1560
- binary += String.fromCharCode(bytes[i]);
2199
+ return notFound();
2200
+ }
2201
+ async function completeOAuthSignIn(params) {
2202
+ const { oauth, userStore, tokenManager, provider, code, state, deviceId, devicePublicKey } = params;
2203
+ const { userInfo, stateMetadata } = await oauth.manager.handleCallback(provider, code, state);
2204
+ const linkedIdentity = await oauth.linkedIdentityStore.findByProvider(
2205
+ userInfo.provider,
2206
+ userInfo.providerId
2207
+ );
2208
+ let user;
2209
+ let identity;
2210
+ if (linkedIdentity) {
2211
+ const storedUser = await userStore.findById(linkedIdentity.userId);
2212
+ if (!storedUser) {
2213
+ return { status: 409, body: { error: "Linked OAuth account has no matching user." } };
2214
+ }
2215
+ user = toAuthUser2(storedUser);
2216
+ identity = linkedIdentity;
2217
+ } else {
2218
+ const resolved = await resolveOAuthUser(userStore, oauth, userInfo);
2219
+ if ("status" in resolved) return resolved;
2220
+ user = resolved;
2221
+ identity = await oauth.linkedIdentityStore.create({
2222
+ userId: user.id,
2223
+ provider: userInfo.provider,
2224
+ providerUserId: userInfo.providerId,
2225
+ email: userInfo.email
2226
+ });
1561
2227
  }
1562
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2228
+ const resolvedDeviceId = deviceId ?? readString(stateMetadata?.deviceId) ?? `device-${user.id}`;
2229
+ await userStore.registerDevice({
2230
+ id: resolvedDeviceId,
2231
+ userId: user.id,
2232
+ publicKey: devicePublicKey ?? readString(stateMetadata?.devicePublicKey) ?? "",
2233
+ name: deviceId ? "Device" : "Browser"
2234
+ });
2235
+ const tokens = tokenManager.issueTokens(user.id, resolvedDeviceId);
2236
+ return { status: 200, body: { data: { user, tokens, identity } } };
2237
+ }
2238
+ async function resolveOAuthUser(userStore, oauth, userInfo) {
2239
+ if (!userInfo.email) {
2240
+ return { status: 400, body: { error: "OAuth provider did not return an email address." } };
2241
+ }
2242
+ const existingUser = await userStore.findByEmail(userInfo.email);
2243
+ if (existingUser) {
2244
+ if (oauth.autoLinkVerifiedEmail && userInfo.emailVerified) {
2245
+ return toAuthUser2(existingUser);
2246
+ }
2247
+ return {
2248
+ status: 409,
2249
+ body: { error: "OAuth account is not linked. Sign in and link this provider first." }
2250
+ };
2251
+ }
2252
+ if (!oauth.createNewUsers) {
2253
+ return { status: 403, body: { error: "OAuth sign-up is disabled for this application." } };
2254
+ }
2255
+ const credential = await hashPassword((0, import_node_crypto7.randomUUID)());
2256
+ const user = await userStore.createUser({
2257
+ email: userInfo.email,
2258
+ passwordHash: credential.hash,
2259
+ salt: credential.salt,
2260
+ name: userInfo.name ?? userInfo.email.split("@")[0] ?? userInfo.email
2261
+ });
2262
+ if (userInfo.emailVerified) {
2263
+ await userStore.setEmailVerified(user.id, true);
2264
+ return { ...user, emailVerified: true };
2265
+ }
2266
+ return user;
2267
+ }
2268
+ async function linkOAuthIdentity(oauth, userId, provider, code, state) {
2269
+ const { userInfo } = await oauth.manager.handleCallback(provider, code, state);
2270
+ const existing = await oauth.linkedIdentityStore.findByProvider(
2271
+ userInfo.provider,
2272
+ userInfo.providerId
2273
+ );
2274
+ if (existing && existing.userId !== userId) {
2275
+ return { status: 409, body: { error: "This OAuth account is already linked to another user." } };
2276
+ }
2277
+ if (existing) {
2278
+ return { status: 200, body: { data: existing } };
2279
+ }
2280
+ const identity = await oauth.linkedIdentityStore.create({
2281
+ userId,
2282
+ provider: userInfo.provider,
2283
+ providerUserId: userInfo.providerId,
2284
+ email: userInfo.email
2285
+ });
2286
+ return { status: 201, body: { data: identity } };
2287
+ }
2288
+ async function requireAuthUser(tokenManager, userStore, token) {
2289
+ if (!token) {
2290
+ return { status: 401, body: { error: "Authorization token required." } };
2291
+ }
2292
+ const payload = await tokenManager.validateToken(token);
2293
+ if (!payload || payload.type !== "access") {
2294
+ return { status: 401, body: { error: "Invalid or expired token." } };
2295
+ }
2296
+ const user = await userStore.findById(payload.sub);
2297
+ if (!user) {
2298
+ return { status: 401, body: { error: "User not found." } };
2299
+ }
2300
+ return toAuthUser2(user);
2301
+ }
2302
+ function extractBearerToken(headers) {
2303
+ const authorization = headers?.authorization ?? headers?.Authorization;
2304
+ const value = Array.isArray(authorization) ? authorization[0] : authorization;
2305
+ if (!value?.startsWith("Bearer ")) {
2306
+ return "";
2307
+ }
2308
+ return value.slice("Bearer ".length).trim();
2309
+ }
2310
+ function normalizePath(path) {
2311
+ const withoutQuery = path.split("?")[0] || "/";
2312
+ const normalized = withoutQuery.startsWith("/") ? withoutQuery : `/${withoutQuery}`;
2313
+ return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
2314
+ }
2315
+ function metadataFromQuery(query) {
2316
+ if (!query) return void 0;
2317
+ const metadata = {};
2318
+ for (const [key, value] of Object.entries(query)) {
2319
+ if (key === "code" || key === "state") continue;
2320
+ if (value !== void 0) {
2321
+ metadata[key] = value;
2322
+ }
2323
+ }
2324
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
2325
+ }
2326
+ function queryValue(query, key) {
2327
+ return query?.[key];
2328
+ }
2329
+ function readString(value) {
2330
+ if (typeof value === "string" && value.length > 0) return value;
2331
+ if (Array.isArray(value) && typeof value[0] === "string" && value[0].length > 0) {
2332
+ return value[0];
2333
+ }
2334
+ return void 0;
2335
+ }
2336
+ function oauthErrorResponse(error) {
2337
+ if (error instanceof DuplicateLinkedIdentityError) {
2338
+ return { status: 409, body: { error: error.message } };
2339
+ }
2340
+ if (error instanceof OAuthError) {
2341
+ const status = error.code === "OAUTH_PROVIDER_NOT_FOUND" ? 404 : 400;
2342
+ return { status, body: { error: error.message } };
2343
+ }
2344
+ throw error;
2345
+ }
2346
+ function toAuthUser2(user) {
2347
+ return {
2348
+ id: user.id,
2349
+ email: user.email,
2350
+ name: user.name,
2351
+ emailVerified: user.emailVerified,
2352
+ createdAt: user.createdAt
2353
+ };
2354
+ }
2355
+ function isRecord(value) {
2356
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2357
+ }
2358
+ function notFound() {
2359
+ return { status: 404, body: { error: "Not found" } };
2360
+ }
2361
+ function readEnvSecret() {
2362
+ return typeof process !== "undefined" ? process.env.KORA_AUTH_SECRET : void 0;
2363
+ }
2364
+ function isProduction() {
2365
+ return typeof process !== "undefined" && process.env.NODE_ENV === "production";
1563
2366
  }
1564
2367
 
1565
- // src/provider/built-in/email-verification.ts
2368
+ // src/server.ts
2369
+ init_password_hash();
2370
+
2371
+ // src/provider/built-in/password-reset.ts
1566
2372
  init_cjs_shims();
1567
- var import_core4 = require("@korajs/core");
1568
- var EmailVerificationError = class extends import_core4.KoraError {
2373
+ var import_core6 = require("@korajs/core");
2374
+ init_password_hash();
2375
+ var PasswordResetError = class extends import_core6.KoraError {
1569
2376
  constructor(message, code, context) {
1570
2377
  super(message, code, context);
1571
- this.name = "EmailVerificationError";
2378
+ this.name = "PasswordResetError";
1572
2379
  }
1573
2380
  };
1574
- var VerificationTokenExpiredError = class extends EmailVerificationError {
2381
+ var ResetTokenExpiredError = class extends PasswordResetError {
1575
2382
  constructor() {
1576
- super("Email verification token has expired.", "VERIFICATION_TOKEN_EXPIRED");
2383
+ super("Password reset token has expired.", "RESET_TOKEN_EXPIRED");
1577
2384
  }
1578
2385
  };
1579
- var VerificationTokenNotFoundError = class extends EmailVerificationError {
2386
+ var ResetTokenNotFoundError = class extends PasswordResetError {
1580
2387
  constructor() {
1581
- super("Email verification token not found or already used.", "VERIFICATION_TOKEN_NOT_FOUND");
2388
+ super("Password reset token not found or already used.", "RESET_TOKEN_NOT_FOUND");
1582
2389
  }
1583
2390
  };
1584
- var InMemoryEmailVerificationStore = class {
2391
+ var ResetRateLimitedError = class extends PasswordResetError {
2392
+ constructor() {
2393
+ super("Too many password reset requests. Please try again later.", "RESET_RATE_LIMITED");
2394
+ }
2395
+ };
2396
+ var InMemoryPasswordResetStore = class {
1585
2397
  tokens = /* @__PURE__ */ new Map();
1586
2398
  async store(token) {
1587
2399
  this.tokens.set(token.token, token);
@@ -1595,11 +2407,11 @@ var InMemoryEmailVerificationStore = class {
1595
2407
  this.tokens.set(token, { ...entry, consumed: true });
1596
2408
  }
1597
2409
  }
1598
- async countActiveForUser(userId) {
2410
+ async countActiveForEmail(email) {
1599
2411
  const now = Date.now();
1600
2412
  let count = 0;
1601
2413
  for (const token of this.tokens.values()) {
1602
- if (token.userId === userId && !token.consumed && now < token.expiresAt) {
2414
+ if (token.email === email && !token.consumed && now < token.expiresAt) {
1603
2415
  count++;
1604
2416
  }
1605
2417
  }
@@ -1617,97 +2429,130 @@ var InMemoryEmailVerificationStore = class {
1617
2429
  return count;
1618
2430
  }
1619
2431
  };
1620
- var DEFAULT_TOKEN_TTL_MS2 = 24 * 60 * 60 * 1e3;
1621
- var DEFAULT_MAX_REQUESTS2 = 3;
1622
- var EmailVerificationManager = class {
2432
+ var DEFAULT_TOKEN_TTL_MS = 60 * 60 * 1e3;
2433
+ var DEFAULT_MAX_REQUESTS = 3;
2434
+ var MIN_PASSWORD_LENGTH2 = 8;
2435
+ var MAX_PASSWORD_LENGTH2 = 128;
2436
+ var PasswordResetManager = class {
1623
2437
  userStore;
1624
- verificationStore;
2438
+ resetStore;
1625
2439
  tokenTtlMs;
1626
- maxRequestsPerUser;
1627
- onVerificationRequired;
2440
+ maxRequestsPerEmail;
2441
+ onResetRequested;
1628
2442
  constructor(config) {
1629
2443
  this.userStore = config.userStore;
1630
- this.verificationStore = config.verificationStore ?? new InMemoryEmailVerificationStore();
1631
- this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS2;
1632
- this.maxRequestsPerUser = config.maxRequestsPerUser ?? DEFAULT_MAX_REQUESTS2;
1633
- this.onVerificationRequired = config.onVerificationRequired;
2444
+ this.resetStore = config.resetStore ?? new InMemoryPasswordResetStore();
2445
+ this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
2446
+ this.maxRequestsPerEmail = config.maxRequestsPerEmail ?? DEFAULT_MAX_REQUESTS;
2447
+ this.onResetRequested = config.onResetRequested;
1634
2448
  }
1635
2449
  /**
1636
- * Send a verification email for a user.
1637
- * Rate-limited to prevent abuse.
2450
+ * Request a password reset for an email.
2451
+ * Always returns success to prevent email enumeration.
2452
+ *
2453
+ * If a callback is configured, invokes it with the token.
2454
+ * In development mode (no callback), returns the token in the response.
1638
2455
  */
1639
- async sendVerification(userId, email) {
2456
+ async requestReset(email) {
1640
2457
  const normalizedEmail = email.toLowerCase().trim();
1641
- const activeCount = await this.verificationStore.countActiveForUser(userId);
1642
- if (activeCount >= this.maxRequestsPerUser) {
1643
- return {
1644
- status: 429,
1645
- body: { error: "Too many verification requests. Please try again later." }
1646
- };
2458
+ const successResponse = {
2459
+ status: 200,
2460
+ body: {
2461
+ data: {
2462
+ message: "If an account with that email exists, a password reset link has been sent."
2463
+ }
2464
+ }
2465
+ };
2466
+ const user = await this.userStore.findByEmail(normalizedEmail);
2467
+ if (!user) {
2468
+ return successResponse;
1647
2469
  }
1648
- const token = generateSecureToken2();
2470
+ const activeCount = await this.resetStore.countActiveForEmail(normalizedEmail);
2471
+ if (activeCount >= this.maxRequestsPerEmail) {
2472
+ return successResponse;
2473
+ }
2474
+ const token = generateSecureToken();
1649
2475
  const now = Date.now();
1650
- const verificationToken = {
2476
+ const resetToken = {
1651
2477
  token,
1652
- userId,
2478
+ userId: user.id,
1653
2479
  email: normalizedEmail,
1654
2480
  createdAt: now,
1655
2481
  expiresAt: now + this.tokenTtlMs,
1656
2482
  consumed: false
1657
2483
  };
1658
- await this.verificationStore.store(verificationToken);
1659
- if (this.onVerificationRequired) {
2484
+ await this.resetStore.store(resetToken);
2485
+ if (this.onResetRequested) {
1660
2486
  try {
1661
- await this.onVerificationRequired(normalizedEmail, token, verificationToken.expiresAt);
2487
+ await this.onResetRequested(normalizedEmail, token, resetToken.expiresAt);
1662
2488
  } catch {
1663
2489
  }
1664
2490
  }
1665
- const responseData = {
1666
- message: "Verification email sent."
1667
- };
1668
- if (!this.onVerificationRequired) {
1669
- responseData.token = token;
2491
+ if (!this.onResetRequested) {
2492
+ successResponse.body = { data: { message: "Password reset token generated.", token } };
1670
2493
  }
1671
- return { status: 200, body: { data: responseData } };
2494
+ return successResponse;
1672
2495
  }
1673
2496
  /**
1674
- * Verify an email using a verification token.
2497
+ * Consume a reset token and set a new password.
1675
2498
  */
1676
- async verifyEmail(token) {
1677
- const verificationToken = await this.verificationStore.get(token);
1678
- if (!verificationToken || verificationToken.consumed) {
1679
- return { status: 404, body: { error: "Verification token not found or already used." } };
2499
+ async resetPassword(token, newPassword) {
2500
+ if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
2501
+ return {
2502
+ status: 400,
2503
+ body: { error: `Password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
2504
+ };
1680
2505
  }
1681
- if (Date.now() > verificationToken.expiresAt) {
1682
- await this.verificationStore.consume(token);
1683
- return { status: 410, body: { error: "Verification token has expired." } };
2506
+ if (newPassword.length > MAX_PASSWORD_LENGTH2) {
2507
+ return {
2508
+ status: 400,
2509
+ body: { error: `Password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
2510
+ };
1684
2511
  }
1685
- await this.verificationStore.consume(token);
1686
- await this.userStore.setEmailVerified(verificationToken.userId, true);
1687
- return {
1688
- status: 200,
1689
- body: {
1690
- data: {
1691
- message: "Email verified successfully.",
1692
- userId: verificationToken.userId,
1693
- email: verificationToken.email
1694
- }
1695
- }
1696
- };
2512
+ const resetToken = await this.resetStore.get(token);
2513
+ if (!resetToken || resetToken.consumed) {
2514
+ return { status: 404, body: { error: "Password reset token not found or already used." } };
2515
+ }
2516
+ if (Date.now() > resetToken.expiresAt) {
2517
+ await this.resetStore.consume(token);
2518
+ return { status: 410, body: { error: "Password reset token has expired." } };
2519
+ }
2520
+ await this.resetStore.consume(token);
2521
+ const hashed = await hashPassword(newPassword);
2522
+ await this.userStore.updatePassword(resetToken.userId, hashed.hash, hashed.salt);
2523
+ return { status: 200, body: { data: { message: "Password has been reset successfully." } } };
1697
2524
  }
1698
2525
  /**
1699
- * Resend verification email for a user.
1700
- * Delegates to sendVerification with rate limiting.
2526
+ * Change password for an authenticated user (requires current password verification).
1701
2527
  */
1702
- async resendVerification(userId) {
2528
+ async changePassword(userId, currentPassword, newPassword) {
2529
+ if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
2530
+ return {
2531
+ status: 400,
2532
+ body: { error: `New password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
2533
+ };
2534
+ }
2535
+ if (newPassword.length > MAX_PASSWORD_LENGTH2) {
2536
+ return {
2537
+ status: 400,
2538
+ body: { error: `New password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
2539
+ };
2540
+ }
1703
2541
  const user = await this.userStore.findById(userId);
1704
2542
  if (!user) {
1705
2543
  return { status: 404, body: { error: "User not found." } };
1706
2544
  }
1707
- return this.sendVerification(userId, user.email);
2545
+ const { verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password_hash(), password_hash_exports));
2546
+ const isValid = await verifyPassword2(currentPassword, user.passwordHash, user.salt);
2547
+ if (!isValid) {
2548
+ return { status: 401, body: { error: "Current password is incorrect." } };
2549
+ }
2550
+ const hashed = await hashPassword(newPassword);
2551
+ await this.userStore.updatePassword(userId, hashed.hash, hashed.salt);
2552
+ return { status: 200, body: { data: { message: "Password changed successfully." } } };
1708
2553
  }
1709
2554
  };
1710
- function generateSecureToken2() {
2555
+ function generateSecureToken() {
1711
2556
  const bytes = new Uint8Array(32);
1712
2557
  globalThis.crypto.getRandomValues(bytes);
1713
2558
  let binary = "";
@@ -1717,246 +2562,164 @@ function generateSecureToken2() {
1717
2562
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1718
2563
  }
1719
2564
 
1720
- // src/provider/built-in/user-store.ts
2565
+ // src/provider/built-in/email-verification.ts
1721
2566
  init_cjs_shims();
1722
- var import_node_crypto5 = require("crypto");
1723
- var import_core5 = require("@korajs/core");
1724
- var DuplicateEmailError = class extends import_core5.KoraError {
2567
+ var import_core7 = require("@korajs/core");
2568
+ var EmailVerificationError = class extends import_core7.KoraError {
2569
+ constructor(message, code, context) {
2570
+ super(message, code, context);
2571
+ this.name = "EmailVerificationError";
2572
+ }
2573
+ };
2574
+ var VerificationTokenExpiredError = class extends EmailVerificationError {
1725
2575
  constructor() {
1726
- super("A user with this email already exists.", "DUPLICATE_EMAIL");
1727
- this.name = "DuplicateEmailError";
2576
+ super("Email verification token has expired.", "VERIFICATION_TOKEN_EXPIRED");
1728
2577
  }
1729
2578
  };
1730
- var InMemoryUserStore = class {
1731
- /** Users indexed by ID */
1732
- usersById = /* @__PURE__ */ new Map();
1733
- /** Users indexed by email (lowercase) for fast lookup */
1734
- usersByEmail = /* @__PURE__ */ new Map();
1735
- /** Devices indexed by device ID */
1736
- devicesById = /* @__PURE__ */ new Map();
1737
- /** Device IDs indexed by user ID for fast listing */
1738
- devicesByUserId = /* @__PURE__ */ new Map();
1739
- /**
1740
- * Create a new user account.
1741
- *
1742
- * @param params - User creation parameters
1743
- * @param params.email - The user's email address (must be unique, case-insensitive)
1744
- * @param params.passwordHash - Hex-encoded PBKDF2 derived key
1745
- * @param params.salt - Hex-encoded salt used during hashing
1746
- * @param params.name - The user's display name
1747
- * @returns The created user (without sensitive credential fields)
1748
- * @throws {DuplicateEmailError} If a user with the same email already exists
1749
- */
1750
- async createUser(params) {
1751
- const normalizedEmail = params.email.toLowerCase();
1752
- if (this.usersByEmail.has(normalizedEmail)) {
1753
- throw new DuplicateEmailError();
1754
- }
1755
- const now = Date.now();
1756
- const id = (0, import_node_crypto5.randomUUID)();
1757
- const storedUser = {
1758
- id,
1759
- email: normalizedEmail,
1760
- name: params.name,
1761
- emailVerified: false,
1762
- createdAt: now,
1763
- passwordHash: params.passwordHash,
1764
- salt: params.salt
1765
- };
1766
- this.usersById.set(id, storedUser);
1767
- this.usersByEmail.set(normalizedEmail, storedUser);
1768
- return toAuthUser(storedUser);
2579
+ var VerificationTokenNotFoundError = class extends EmailVerificationError {
2580
+ constructor() {
2581
+ super("Email verification token not found or already used.", "VERIFICATION_TOKEN_NOT_FOUND");
1769
2582
  }
1770
- /**
1771
- * Find a user by email address.
1772
- *
1773
- * @param email - The email to search for (case-insensitive)
1774
- * @returns The stored user record including credentials, or null if not found
1775
- */
1776
- async findByEmail(email) {
1777
- return this.usersByEmail.get(email.toLowerCase()) ?? null;
2583
+ };
2584
+ var InMemoryEmailVerificationStore = class {
2585
+ tokens = /* @__PURE__ */ new Map();
2586
+ async store(token) {
2587
+ this.tokens.set(token.token, token);
1778
2588
  }
1779
- /**
1780
- * Find a user by ID.
1781
- *
1782
- * @param id - The user ID to search for
1783
- * @returns The stored user record including credentials, or null if not found
1784
- */
1785
- async findById(id) {
1786
- return this.usersById.get(id) ?? null;
2589
+ async get(token) {
2590
+ return this.tokens.get(token) ?? null;
1787
2591
  }
1788
- /**
1789
- * Register a device for a user.
1790
- *
1791
- * If a device with the same ID already exists and is not revoked, it is
1792
- * returned as-is (idempotent registration). If it was previously revoked,
1793
- * it is re-activated with updated details.
1794
- *
1795
- * @param params - Device registration parameters
1796
- * @param params.id - Unique device identifier
1797
- * @param params.userId - ID of the user who owns the device
1798
- * @param params.publicKey - Base64url-encoded device public key or thumbprint
1799
- * @param params.name - Human-readable device name
1800
- * @returns The registered device record
1801
- */
1802
- async registerDevice(params) {
1803
- const existing = this.devicesById.get(params.id);
1804
- if (existing !== void 0 && !existing.revoked) {
1805
- return existing;
1806
- }
1807
- const now = Date.now();
1808
- const device = {
1809
- id: params.id,
1810
- userId: params.userId,
1811
- publicKey: params.publicKey,
1812
- name: params.name,
1813
- revoked: false,
1814
- createdAt: now,
1815
- lastSeenAt: now
1816
- };
1817
- this.devicesById.set(params.id, device);
1818
- let userDevices = this.devicesByUserId.get(params.userId);
1819
- if (userDevices === void 0) {
1820
- userDevices = /* @__PURE__ */ new Set();
1821
- this.devicesByUserId.set(params.userId, userDevices);
2592
+ async consume(token) {
2593
+ const entry = this.tokens.get(token);
2594
+ if (entry) {
2595
+ this.tokens.set(token, { ...entry, consumed: true });
1822
2596
  }
1823
- userDevices.add(params.id);
1824
- return device;
1825
- }
1826
- /**
1827
- * Find a device by its ID.
1828
- *
1829
- * @param deviceId - The device ID to search for
1830
- * @returns The device record, or null if not found
1831
- */
1832
- async findDevice(deviceId) {
1833
- return this.devicesById.get(deviceId) ?? null;
1834
2597
  }
1835
- /**
1836
- * List all devices registered for a user.
1837
- *
1838
- * @param userId - The user ID whose devices to list
1839
- * @returns Array of device records (includes revoked devices)
1840
- */
1841
- async listDevices(userId) {
1842
- const deviceIds = this.devicesByUserId.get(userId);
1843
- if (deviceIds === void 0) {
1844
- return [];
1845
- }
1846
- const devices = [];
1847
- for (const deviceId of deviceIds) {
1848
- const device = this.devicesById.get(deviceId);
1849
- if (device !== void 0) {
1850
- devices.push(device);
2598
+ async countActiveForUser(userId) {
2599
+ const now = Date.now();
2600
+ let count = 0;
2601
+ for (const token of this.tokens.values()) {
2602
+ if (token.userId === userId && !token.consumed && now < token.expiresAt) {
2603
+ count++;
1851
2604
  }
1852
2605
  }
1853
- return devices;
2606
+ return count;
1854
2607
  }
1855
- /**
1856
- * Revoke a device, preventing it from being used for authentication.
1857
- *
1858
- * This is a soft revoke — the device record remains but is marked as revoked.
1859
- * If the device does not exist, this is a no-op.
1860
- *
1861
- * @param deviceId - The ID of the device to revoke
1862
- */
1863
- async revokeDevice(deviceId) {
1864
- const device = this.devicesById.get(deviceId);
1865
- if (device !== void 0) {
1866
- device.revoked = true;
2608
+ async cleanExpired() {
2609
+ const now = Date.now();
2610
+ let count = 0;
2611
+ for (const [key, token] of this.tokens) {
2612
+ if (now > token.expiresAt) {
2613
+ this.tokens.delete(key);
2614
+ count++;
2615
+ }
1867
2616
  }
2617
+ return count;
1868
2618
  }
1869
- /**
1870
- * Set a user's email verification status.
1871
- *
1872
- * @param userId - The user whose email to verify
1873
- * @param verified - Whether the email is verified
1874
- */
1875
- async setEmailVerified(userId, verified) {
1876
- const user = this.usersById.get(userId);
1877
- if (!user) return;
1878
- const updated = { ...user, emailVerified: verified };
1879
- this.usersById.set(userId, updated);
1880
- this.usersByEmail.set(user.email, updated);
1881
- }
1882
- /**
1883
- * Update a user's password hash and salt.
1884
- *
1885
- * @param userId - The user whose password to update
1886
- * @param passwordHash - New hex-encoded PBKDF2 derived key
1887
- * @param salt - New hex-encoded salt
1888
- */
1889
- async updatePassword(userId, passwordHash, salt) {
1890
- const user = this.usersById.get(userId);
1891
- if (!user) return;
1892
- const updated = { ...user, passwordHash, salt };
1893
- this.usersById.set(userId, updated);
1894
- this.usersByEmail.set(user.email, updated);
1895
- }
1896
- /**
1897
- * List all users. For admin/development use.
1898
- */
1899
- async listAll() {
1900
- return [...this.usersById.values()];
2619
+ };
2620
+ var DEFAULT_TOKEN_TTL_MS2 = 24 * 60 * 60 * 1e3;
2621
+ var DEFAULT_MAX_REQUESTS2 = 3;
2622
+ var EmailVerificationManager = class {
2623
+ userStore;
2624
+ verificationStore;
2625
+ tokenTtlMs;
2626
+ maxRequestsPerUser;
2627
+ onVerificationRequired;
2628
+ constructor(config) {
2629
+ this.userStore = config.userStore;
2630
+ this.verificationStore = config.verificationStore ?? new InMemoryEmailVerificationStore();
2631
+ this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS2;
2632
+ this.maxRequestsPerUser = config.maxRequestsPerUser ?? DEFAULT_MAX_REQUESTS2;
2633
+ this.onVerificationRequired = config.onVerificationRequired;
1901
2634
  }
1902
2635
  /**
1903
- * Update a stored user record.
2636
+ * Send a verification email for a user.
2637
+ * Rate-limited to prevent abuse.
1904
2638
  */
1905
- async update(user) {
1906
- const existing = this.usersById.get(user.id);
1907
- if (!existing) return;
1908
- if (existing.email !== user.email) {
1909
- this.usersByEmail.delete(existing.email);
1910
- this.usersByEmail.set(user.email, user);
1911
- } else {
1912
- this.usersByEmail.set(user.email, user);
2639
+ async sendVerification(userId, email) {
2640
+ const normalizedEmail = email.toLowerCase().trim();
2641
+ const activeCount = await this.verificationStore.countActiveForUser(userId);
2642
+ if (activeCount >= this.maxRequestsPerUser) {
2643
+ return {
2644
+ status: 429,
2645
+ body: { error: "Too many verification requests. Please try again later." }
2646
+ };
2647
+ }
2648
+ const token = generateSecureToken2();
2649
+ const now = Date.now();
2650
+ const verificationToken = {
2651
+ token,
2652
+ userId,
2653
+ email: normalizedEmail,
2654
+ createdAt: now,
2655
+ expiresAt: now + this.tokenTtlMs,
2656
+ consumed: false
2657
+ };
2658
+ await this.verificationStore.store(verificationToken);
2659
+ if (this.onVerificationRequired) {
2660
+ try {
2661
+ await this.onVerificationRequired(normalizedEmail, token, verificationToken.expiresAt);
2662
+ } catch {
2663
+ }
1913
2664
  }
1914
- this.usersById.set(user.id, user);
2665
+ const responseData = {
2666
+ message: "Verification email sent."
2667
+ };
2668
+ if (!this.onVerificationRequired) {
2669
+ responseData.token = token;
2670
+ }
2671
+ return { status: 200, body: { data: responseData } };
1915
2672
  }
1916
2673
  /**
1917
- * Delete a user and all associated devices.
2674
+ * Verify an email using a verification token.
1918
2675
  */
1919
- async delete(userId) {
1920
- const user = this.usersById.get(userId);
1921
- if (!user) return;
1922
- this.usersById.delete(userId);
1923
- this.usersByEmail.delete(user.email);
1924
- const deviceIds = this.devicesByUserId.get(userId);
1925
- if (deviceIds) {
1926
- for (const deviceId of deviceIds) {
1927
- this.devicesById.delete(deviceId);
1928
- }
1929
- this.devicesByUserId.delete(userId);
2676
+ async verifyEmail(token) {
2677
+ const verificationToken = await this.verificationStore.get(token);
2678
+ if (!verificationToken || verificationToken.consumed) {
2679
+ return { status: 404, body: { error: "Verification token not found or already used." } };
2680
+ }
2681
+ if (Date.now() > verificationToken.expiresAt) {
2682
+ await this.verificationStore.consume(token);
2683
+ return { status: 410, body: { error: "Verification token has expired." } };
1930
2684
  }
2685
+ await this.verificationStore.consume(token);
2686
+ await this.userStore.setEmailVerified(verificationToken.userId, true);
2687
+ return {
2688
+ status: 200,
2689
+ body: {
2690
+ data: {
2691
+ message: "Email verified successfully.",
2692
+ userId: verificationToken.userId,
2693
+ email: verificationToken.email
2694
+ }
2695
+ }
2696
+ };
1931
2697
  }
1932
2698
  /**
1933
- * Update the last-seen timestamp for a device.
1934
- *
1935
- * Called when a device authenticates or syncs to track activity.
1936
- * If the device does not exist, this is a no-op.
1937
- *
1938
- * @param deviceId - The ID of the device to update
2699
+ * Resend verification email for a user.
2700
+ * Delegates to sendVerification with rate limiting.
1939
2701
  */
1940
- async touchDevice(deviceId) {
1941
- const device = this.devicesById.get(deviceId);
1942
- if (device !== void 0) {
1943
- device.lastSeenAt = Date.now();
2702
+ async resendVerification(userId) {
2703
+ const user = await this.userStore.findById(userId);
2704
+ if (!user) {
2705
+ return { status: 404, body: { error: "User not found." } };
1944
2706
  }
2707
+ return this.sendVerification(userId, user.email);
1945
2708
  }
1946
2709
  };
1947
- function toAuthUser(stored) {
1948
- return {
1949
- id: stored.id,
1950
- email: stored.email,
1951
- name: stored.name,
1952
- emailVerified: stored.emailVerified,
1953
- createdAt: stored.createdAt
1954
- };
2710
+ function generateSecureToken2() {
2711
+ const bytes = new Uint8Array(32);
2712
+ globalThis.crypto.getRandomValues(bytes);
2713
+ let binary = "";
2714
+ for (let i = 0; i < bytes.length; i++) {
2715
+ binary += String.fromCharCode(bytes[i]);
2716
+ }
2717
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1955
2718
  }
1956
2719
 
1957
2720
  // src/provider/built-in/sqlite-user-store.ts
1958
2721
  init_cjs_shims();
1959
- var import_node_crypto6 = require("crypto");
2722
+ var import_node_crypto8 = require("crypto");
1960
2723
  var SqliteUserStore = class {
1961
2724
  db;
1962
2725
  constructor(db) {
@@ -1993,7 +2756,7 @@ var SqliteUserStore = class {
1993
2756
  async createUser(params) {
1994
2757
  const normalizedEmail = params.email.toLowerCase();
1995
2758
  const now = Date.now();
1996
- const id = (0, import_node_crypto6.randomUUID)();
2759
+ const id = (0, import_node_crypto8.randomUUID)();
1997
2760
  try {
1998
2761
  this.db.prepare(`
1999
2762
  INSERT INTO auth_users (id, email, name, email_verified, created_at, password_hash, salt)
@@ -2122,7 +2885,7 @@ function rowToDevice(row) {
2122
2885
 
2123
2886
  // src/provider/built-in/postgres-user-store.ts
2124
2887
  init_cjs_shims();
2125
- var import_node_crypto7 = require("crypto");
2888
+ var import_node_crypto9 = require("crypto");
2126
2889
  var PostgresUserStore = class {
2127
2890
  sql;
2128
2891
  ready;
@@ -2164,7 +2927,7 @@ var PostgresUserStore = class {
2164
2927
  await this.ready;
2165
2928
  const normalizedEmail = params.email.toLowerCase();
2166
2929
  const now = Date.now();
2167
- const id = (0, import_node_crypto7.randomUUID)();
2930
+ const id = (0, import_node_crypto9.randomUUID)();
2168
2931
  try {
2169
2932
  await this.sql`
2170
2933
  INSERT INTO auth_users (id, email, name, email_verified, created_at, password_hash, salt)
@@ -2402,8 +3165,8 @@ var BuiltInProvider = class {
2402
3165
 
2403
3166
  // src/provider/external/external-jwt-provider.ts
2404
3167
  init_cjs_shims();
2405
- var import_core6 = require("@korajs/core");
2406
- var ExternalAuthOperationNotSupportedError = class extends import_core6.KoraError {
3168
+ var import_core8 = require("@korajs/core");
3169
+ var ExternalAuthOperationNotSupportedError = class extends import_core8.KoraError {
2407
3170
  constructor(operation, provider) {
2408
3171
  super(
2409
3172
  `The "${operation}" operation is not supported by the external auth provider "${provider}". Perform this operation through your external auth provider's SDK or dashboard instead.`,
@@ -2413,7 +3176,7 @@ var ExternalAuthOperationNotSupportedError = class extends import_core6.KoraErro
2413
3176
  this.name = "ExternalAuthOperationNotSupportedError";
2414
3177
  }
2415
3178
  };
2416
- var ExternalTokenValidationError = class extends import_core6.KoraError {
3179
+ var ExternalTokenValidationError = class extends import_core8.KoraError {
2417
3180
  constructor(reason, context) {
2418
3181
  super(`External token validation failed: ${reason}`, "AUTH_EXTERNAL_TOKEN_INVALID", context);
2419
3182
  this.name = "ExternalTokenValidationError";
@@ -2708,13 +3471,13 @@ function createSupabaseAdapter(config) {
2708
3471
 
2709
3472
  // src/passkey/passkey-server.ts
2710
3473
  init_cjs_shims();
2711
- var import_node_crypto8 = require("crypto");
2712
- var import_core8 = require("@korajs/core");
3474
+ var import_node_crypto10 = require("crypto");
3475
+ var import_core10 = require("@korajs/core");
2713
3476
 
2714
3477
  // src/passkey/passkey-client.ts
2715
3478
  init_cjs_shims();
2716
- var import_core7 = require("@korajs/core");
2717
- var PasskeyError = class extends import_core7.KoraError {
3479
+ var import_core9 = require("@korajs/core");
3480
+ var PasskeyError = class extends import_core9.KoraError {
2718
3481
  constructor(message, context) {
2719
3482
  super(message, "PASSKEY_ERROR", context);
2720
3483
  this.name = "PasskeyError";
@@ -2796,14 +3559,14 @@ function decodeCbor(data, offset) {
2796
3559
  }
2797
3560
 
2798
3561
  // src/passkey/passkey-server.ts
2799
- var PasskeyVerificationError = class extends import_core8.KoraError {
3562
+ var PasskeyVerificationError = class extends import_core10.KoraError {
2800
3563
  constructor(message, context) {
2801
3564
  super(message, "PASSKEY_VERIFICATION_ERROR", context);
2802
3565
  this.name = "PasskeyVerificationError";
2803
3566
  }
2804
3567
  };
2805
3568
  function generateRegistrationOptions(params) {
2806
- const challengeBytes = (0, import_node_crypto8.randomBytes)(32);
3569
+ const challengeBytes = (0, import_node_crypto10.randomBytes)(32);
2807
3570
  const challenge = toBase64Url(
2808
3571
  challengeBytes.buffer.slice(
2809
3572
  challengeBytes.byteOffset,
@@ -2918,7 +3681,7 @@ async function verifyRegistrationResponse(params) {
2918
3681
  };
2919
3682
  }
2920
3683
  function generateAuthenticationOptions(params) {
2921
- const challengeBytes = (0, import_node_crypto8.randomBytes)(32);
3684
+ const challengeBytes = (0, import_node_crypto10.randomBytes)(32);
2922
3685
  const challenge = toBase64Url(
2923
3686
  challengeBytes.buffer.slice(
2924
3687
  challengeBytes.byteOffset,
@@ -3123,18 +3886,18 @@ function copyComponentToP1363(component, target, targetOffset, componentLength)
3123
3886
 
3124
3887
  // src/encryption/operation-encryptor.ts
3125
3888
  init_cjs_shims();
3126
- var import_core10 = require("@korajs/core");
3889
+ var import_core12 = require("@korajs/core");
3127
3890
 
3128
3891
  // src/encryption/database-encryption.ts
3129
3892
  init_cjs_shims();
3130
- var import_core9 = require("@korajs/core");
3131
- var EncryptionError = class extends import_core9.KoraError {
3893
+ var import_core11 = require("@korajs/core");
3894
+ var EncryptionError = class extends import_core11.KoraError {
3132
3895
  constructor(message, context) {
3133
3896
  super(message, "ENCRYPTION_ERROR", context);
3134
3897
  this.name = "EncryptionError";
3135
3898
  }
3136
3899
  };
3137
- var CryptoUnavailableError2 = class extends import_core9.KoraError {
3900
+ var CryptoUnavailableError2 = class extends import_core11.KoraError {
3138
3901
  constructor() {
3139
3902
  super(
3140
3903
  "Web Crypto API (crypto.subtle) is not available in this environment. Database encryption requires crypto.subtle, which is available in modern browsers and Node.js 20+. If running in SSR, ensure your runtime provides the Web Crypto API.",
@@ -3190,13 +3953,13 @@ async function decryptData(key, ciphertext, iv) {
3190
3953
  // src/encryption/operation-encryptor.ts
3191
3954
  var ENCRYPTED_MARKER = "__kora_encrypted";
3192
3955
  var ENCRYPTION_VERSION = 1;
3193
- var OperationEncryptionError = class extends import_core10.KoraError {
3956
+ var OperationEncryptionError = class extends import_core12.KoraError {
3194
3957
  constructor(message, context) {
3195
3958
  super(message, "OPERATION_ENCRYPTION_ERROR", context);
3196
3959
  this.name = "OperationEncryptionError";
3197
3960
  }
3198
3961
  };
3199
- function toBase64Url2(bytes) {
3962
+ function toBase64Url3(bytes) {
3200
3963
  let binary = "";
3201
3964
  for (let i = 0; i < bytes.length; i++) {
3202
3965
  binary += String.fromCharCode(bytes[i]);
@@ -3315,8 +4078,8 @@ var OperationEncryptor = class {
3315
4078
  const { ciphertext, iv } = await encryptData(this.key, plaintext);
3316
4079
  const envelope = {
3317
4080
  [ENCRYPTED_MARKER]: true,
3318
- ciphertext: toBase64Url2(ciphertext),
3319
- iv: toBase64Url2(iv),
4081
+ ciphertext: toBase64Url3(ciphertext),
4082
+ iv: toBase64Url3(iv),
3320
4083
  algorithm: "AES-256-GCM",
3321
4084
  version: ENCRYPTION_VERSION
3322
4085
  };
@@ -3389,7 +4152,7 @@ init_cjs_shims();
3389
4152
 
3390
4153
  // src/org/org-types.ts
3391
4154
  init_cjs_shims();
3392
- var import_core11 = require("@korajs/core");
4155
+ var import_core13 = require("@korajs/core");
3393
4156
  var ORG_ROLES = ["owner", "admin", "member", "viewer", "billing"];
3394
4157
  var ROLE_HIERARCHY = {
3395
4158
  viewer: 10,
@@ -3402,7 +4165,7 @@ function hasRoleLevel(userRole, requiredRole) {
3402
4165
  return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole];
3403
4166
  }
3404
4167
  var INVITATION_STATUSES = ["pending", "accepted", "revoked", "expired"];
3405
- var OrgError = class extends import_core11.KoraError {
4168
+ var OrgError = class extends import_core13.KoraError {
3406
4169
  constructor(message, code, context) {
3407
4170
  super(message, code, context);
3408
4171
  this.name = "OrgError";
@@ -4199,7 +4962,7 @@ init_cjs_shims();
4199
4962
 
4200
4963
  // src/rbac/rbac-types.ts
4201
4964
  init_cjs_shims();
4202
- var import_core12 = require("@korajs/core");
4965
+ var import_core14 = require("@korajs/core");
4203
4966
  function parsePermission(permission) {
4204
4967
  const colonIndex = permission.indexOf(":");
4205
4968
  if (colonIndex === -1) {
@@ -4243,7 +5006,7 @@ var BUILT_IN_ROLES = [
4243
5006
  permissions: ["*:*"]
4244
5007
  }
4245
5008
  ];
4246
- var RbacError = class extends import_core12.KoraError {
5009
+ var RbacError = class extends import_core14.KoraError {
4247
5010
  constructor(message, code, context) {
4248
5011
  super(message, code, context);
4249
5012
  this.name = "RbacError";
@@ -4563,307 +5326,412 @@ var OrgScopeResolver = class {
4563
5326
  }
4564
5327
  };
4565
5328
 
4566
- // src/provider/oauth/oauth-flow.ts
4567
- init_cjs_shims();
4568
-
4569
- // src/provider/oauth/oauth-types.ts
5329
+ // src/provider/oauth/sqlite-oauth-store.ts
4570
5330
  init_cjs_shims();
4571
- var import_core13 = require("@korajs/core");
4572
- var OAuthError = class extends import_core13.KoraError {
4573
- constructor(message, code, context) {
4574
- super(message, code, context);
4575
- this.name = "OAuthError";
4576
- }
4577
- };
4578
- var OAuthStateMismatchError = class extends OAuthError {
4579
- constructor() {
4580
- super("OAuth state parameter does not match. Possible CSRF attack.", "OAUTH_STATE_MISMATCH");
4581
- }
4582
- };
4583
- var OAuthCodeExchangeError = class extends OAuthError {
4584
- constructor(details) {
4585
- super(
4586
- `Failed to exchange authorization code for tokens.${details ? ` ${details}` : ""}`,
4587
- "OAUTH_CODE_EXCHANGE_FAILED",
4588
- details ? { details } : void 0
4589
- );
5331
+ var import_node_crypto11 = require("crypto");
5332
+ var SqliteOAuthStateStore = class {
5333
+ db;
5334
+ constructor(db) {
5335
+ this.db = db;
5336
+ this.db.pragma("journal_mode = WAL");
5337
+ this.ensureTables();
4590
5338
  }
4591
- };
4592
- var OAuthUserInfoError = class extends OAuthError {
4593
- constructor(details) {
4594
- super(
4595
- `Failed to fetch user info from OAuth provider.${details ? ` ${details}` : ""}`,
4596
- "OAUTH_USER_INFO_FAILED",
4597
- details ? { details } : void 0
5339
+ async store(state) {
5340
+ this.db.prepare(`
5341
+ INSERT OR REPLACE INTO auth_oauth_states
5342
+ (state, provider, redirect_uri, created_at, expires_at, metadata_json, code_verifier)
5343
+ VALUES (?, ?, ?, ?, ?, ?, ?)
5344
+ `).run(
5345
+ state.state,
5346
+ state.provider,
5347
+ state.redirectUri,
5348
+ state.createdAt,
5349
+ state.expiresAt,
5350
+ state.metadata ? JSON.stringify(state.metadata) : null,
5351
+ state.codeVerifier ?? null
4598
5352
  );
4599
5353
  }
4600
- };
4601
- var OAuthProviderNotFoundError = class extends OAuthError {
4602
- constructor(provider) {
4603
- super(`OAuth provider "${provider}" is not configured.`, "OAUTH_PROVIDER_NOT_FOUND", {
4604
- provider
5354
+ async consume(stateValue) {
5355
+ const consumeInTransaction = this.db.transaction(() => {
5356
+ const row = this.db.prepare("SELECT * FROM auth_oauth_states WHERE state = ?").get(stateValue);
5357
+ if (!row) return null;
5358
+ this.db.prepare("DELETE FROM auth_oauth_states WHERE state = ?").run(stateValue);
5359
+ if (Date.now() > row.expires_at) return null;
5360
+ return rowToOAuthState(row);
4605
5361
  });
5362
+ return consumeInTransaction();
4606
5363
  }
4607
- };
5364
+ async cleanExpired() {
5365
+ const result = this.db.prepare("DELETE FROM auth_oauth_states WHERE expires_at < ?").run(Date.now());
5366
+ return result.changes ?? 0;
5367
+ }
5368
+ ensureTables() {
5369
+ this.db.exec(`
5370
+ CREATE TABLE IF NOT EXISTS auth_oauth_states (
5371
+ state TEXT PRIMARY KEY,
5372
+ provider TEXT NOT NULL,
5373
+ redirect_uri TEXT NOT NULL,
5374
+ created_at INTEGER NOT NULL,
5375
+ expires_at INTEGER NOT NULL,
5376
+ metadata_json TEXT,
5377
+ code_verifier TEXT
5378
+ );
4608
5379
 
4609
- // src/provider/oauth/oauth-flow.ts
4610
- var DEFAULT_STATE_TTL_MS = 10 * 60 * 1e3;
4611
- var InMemoryOAuthStateStore = class {
4612
- states = /* @__PURE__ */ new Map();
4613
- async store(state) {
4614
- this.states.set(state.state, state);
5380
+ CREATE INDEX IF NOT EXISTS idx_auth_oauth_states_expires_at
5381
+ ON auth_oauth_states(expires_at);
5382
+ `);
4615
5383
  }
4616
- async consume(stateValue) {
4617
- const state = this.states.get(stateValue);
4618
- if (!state) return null;
4619
- this.states.delete(stateValue);
4620
- if (Date.now() > state.expiresAt) return null;
4621
- return state;
5384
+ };
5385
+ var SqliteLinkedIdentityStore = class {
5386
+ db;
5387
+ constructor(db) {
5388
+ this.db = db;
5389
+ this.db.pragma("journal_mode = WAL");
5390
+ this.ensureTables();
4622
5391
  }
4623
- async cleanExpired() {
4624
- const now = Date.now();
4625
- let count = 0;
4626
- for (const [key, state] of this.states) {
4627
- if (now > state.expiresAt) {
4628
- this.states.delete(key);
4629
- count++;
5392
+ async findByProvider(provider, providerUserId) {
5393
+ const row = this.db.prepare(`
5394
+ SELECT * FROM auth_linked_identities
5395
+ WHERE provider = ? AND provider_user_id = ?
5396
+ `).get(provider, providerUserId);
5397
+ return row ? rowToLinkedIdentity(row) : null;
5398
+ }
5399
+ async findByUser(userId) {
5400
+ const rows = this.db.prepare(`
5401
+ SELECT * FROM auth_linked_identities
5402
+ WHERE user_id = ?
5403
+ ORDER BY linked_at ASC
5404
+ `).all(userId);
5405
+ return rows.map(rowToLinkedIdentity);
5406
+ }
5407
+ async create(params) {
5408
+ const identity = {
5409
+ id: (0, import_node_crypto11.randomUUID)(),
5410
+ userId: params.userId,
5411
+ provider: params.provider,
5412
+ providerUserId: params.providerUserId,
5413
+ email: params.email,
5414
+ linkedAt: Date.now()
5415
+ };
5416
+ try {
5417
+ this.db.prepare(`
5418
+ INSERT INTO auth_linked_identities
5419
+ (id, user_id, provider, provider_user_id, email, linked_at)
5420
+ VALUES (?, ?, ?, ?, ?, ?)
5421
+ `).run(
5422
+ identity.id,
5423
+ identity.userId,
5424
+ identity.provider,
5425
+ identity.providerUserId,
5426
+ identity.email,
5427
+ identity.linkedAt
5428
+ );
5429
+ } catch (error) {
5430
+ if (error instanceof Error && error.message.includes("UNIQUE constraint failed")) {
5431
+ throw new DuplicateLinkedIdentityError(params.provider);
4630
5432
  }
5433
+ throw error;
4631
5434
  }
4632
- return count;
5435
+ return identity;
5436
+ }
5437
+ async delete(userId, provider) {
5438
+ this.db.prepare("DELETE FROM auth_linked_identities WHERE user_id = ? AND provider = ?").run(userId, provider);
5439
+ }
5440
+ ensureTables() {
5441
+ this.db.exec(`
5442
+ CREATE TABLE IF NOT EXISTS auth_linked_identities (
5443
+ id TEXT PRIMARY KEY,
5444
+ user_id TEXT NOT NULL,
5445
+ provider TEXT NOT NULL,
5446
+ provider_user_id TEXT NOT NULL,
5447
+ email TEXT,
5448
+ linked_at INTEGER NOT NULL,
5449
+ UNIQUE(provider, provider_user_id),
5450
+ UNIQUE(user_id, provider)
5451
+ );
5452
+
5453
+ CREATE INDEX IF NOT EXISTS idx_auth_linked_identities_user_id
5454
+ ON auth_linked_identities(user_id);
5455
+ `);
4633
5456
  }
4634
5457
  };
4635
- var OAuthManager = class {
4636
- providers = /* @__PURE__ */ new Map();
4637
- stateStore;
4638
- stateTtlMs;
4639
- fetchFn;
4640
- constructor(config) {
4641
- for (const provider of config.providers) {
4642
- this.providers.set(provider.providerId, provider);
4643
- }
4644
- this.stateStore = config.stateStore ?? new InMemoryOAuthStateStore();
4645
- this.stateTtlMs = config.stateTtlMs ?? DEFAULT_STATE_TTL_MS;
4646
- this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
5458
+ async function createSqliteOAuthStateStore(options) {
5459
+ const Database = await loadBetterSqlite32();
5460
+ const db = new Database(options.filename);
5461
+ return new SqliteOAuthStateStore(db);
5462
+ }
5463
+ async function createSqliteLinkedIdentityStore(options) {
5464
+ const Database = await loadBetterSqlite32();
5465
+ const db = new Database(options.filename);
5466
+ return new SqliteLinkedIdentityStore(db);
5467
+ }
5468
+ async function createSqliteOAuthStores(options) {
5469
+ const Database = await loadBetterSqlite32();
5470
+ const db = new Database(options.filename);
5471
+ return {
5472
+ stateStore: new SqliteOAuthStateStore(db),
5473
+ linkedIdentityStore: new SqliteLinkedIdentityStore(db)
5474
+ };
5475
+ }
5476
+ async function loadBetterSqlite32() {
5477
+ try {
5478
+ const { createRequire } = await import("module");
5479
+ const require2 = createRequire(importMetaUrl);
5480
+ return require2("better-sqlite3");
5481
+ } catch {
5482
+ throw new Error(
5483
+ 'SQLite OAuth stores require the "better-sqlite3" package. Install it in your project dependencies.'
5484
+ );
4647
5485
  }
4648
- /**
4649
- * Generate an authorization URL for the user to visit.
4650
- * Returns the URL and the state parameter for CSRF validation.
4651
- */
4652
- async getAuthorizationUrl(providerId, metadata) {
4653
- const provider = this.getProvider(providerId);
4654
- const state = generateState();
4655
- const now = Date.now();
4656
- const oauthState = {
4657
- state,
4658
- provider: providerId,
4659
- redirectUri: provider.redirectUri,
4660
- createdAt: now,
4661
- expiresAt: now + this.stateTtlMs,
4662
- metadata
4663
- };
4664
- await this.stateStore.store(oauthState);
4665
- const params = new URLSearchParams({
4666
- client_id: provider.clientId,
4667
- redirect_uri: provider.redirectUri,
4668
- response_type: "code",
4669
- scope: provider.scopes.join(" "),
4670
- state
5486
+ }
5487
+ function rowToOAuthState(row) {
5488
+ return {
5489
+ state: row.state,
5490
+ provider: row.provider,
5491
+ redirectUri: row.redirect_uri,
5492
+ createdAt: row.created_at,
5493
+ expiresAt: row.expires_at,
5494
+ metadata: parseMetadata(row.metadata_json),
5495
+ codeVerifier: row.code_verifier ?? void 0
5496
+ };
5497
+ }
5498
+ function parseMetadata(value) {
5499
+ if (!value) return void 0;
5500
+ const parsed = JSON.parse(value);
5501
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
5502
+ }
5503
+ function rowToLinkedIdentity(row) {
5504
+ return {
5505
+ id: row.id,
5506
+ userId: row.user_id,
5507
+ provider: row.provider,
5508
+ providerUserId: row.provider_user_id,
5509
+ email: row.email,
5510
+ linkedAt: row.linked_at
5511
+ };
5512
+ }
5513
+
5514
+ // src/provider/oauth/postgres-oauth-store.ts
5515
+ init_cjs_shims();
5516
+ var import_node_crypto12 = require("crypto");
5517
+ var PostgresOAuthStateStore = class {
5518
+ sql;
5519
+ ready;
5520
+ constructor(sql) {
5521
+ this.sql = sql;
5522
+ this.ready = this.ensureTables();
5523
+ }
5524
+ async store(state) {
5525
+ await this.ready;
5526
+ await this.sql`
5527
+ INSERT INTO auth_oauth_states
5528
+ (state, provider, redirect_uri, created_at, expires_at, metadata_json, code_verifier)
5529
+ VALUES (
5530
+ ${state.state},
5531
+ ${state.provider},
5532
+ ${state.redirectUri},
5533
+ ${state.createdAt},
5534
+ ${state.expiresAt},
5535
+ ${state.metadata ? JSON.stringify(state.metadata) : null},
5536
+ ${state.codeVerifier ?? null}
5537
+ )
5538
+ ON CONFLICT (state) DO UPDATE SET
5539
+ provider = EXCLUDED.provider,
5540
+ redirect_uri = EXCLUDED.redirect_uri,
5541
+ created_at = EXCLUDED.created_at,
5542
+ expires_at = EXCLUDED.expires_at,
5543
+ metadata_json = EXCLUDED.metadata_json,
5544
+ code_verifier = EXCLUDED.code_verifier
5545
+ `;
5546
+ }
5547
+ async consume(stateValue) {
5548
+ await this.ready;
5549
+ return this.sql.begin(async (tx) => {
5550
+ const rows = await tx`
5551
+ DELETE FROM auth_oauth_states
5552
+ WHERE state = ${stateValue}
5553
+ RETURNING *
5554
+ `;
5555
+ const row = rows[0];
5556
+ if (!row || Date.now() > Number(row.expires_at)) {
5557
+ return null;
5558
+ }
5559
+ return rowToOAuthState2(row);
4671
5560
  });
4672
- const url = `${provider.authorizationUrl}?${params.toString()}`;
4673
- return { url, state };
4674
5561
  }
4675
- /**
4676
- * Handle the OAuth callback after the user authorizes.
4677
- * Validates the state parameter, exchanges the code for tokens,
4678
- * and fetches user info.
4679
- *
4680
- * @param providerId - The OAuth provider
4681
- * @param code - The authorization code from the callback
4682
- * @param state - The state parameter from the callback
4683
- * @returns Tokens and user info from the provider
4684
- */
4685
- async handleCallback(providerId, code, state) {
4686
- const provider = this.getProvider(providerId);
4687
- const oauthState = await this.stateStore.consume(state);
4688
- if (!oauthState || oauthState.provider !== providerId) {
4689
- throw new OAuthStateMismatchError();
4690
- }
4691
- const tokens = await this.exchangeCodeForTokens(provider, code);
4692
- const userInfo = await this.fetchUserInfo(provider, tokens.accessToken);
4693
- return { tokens, userInfo, stateMetadata: oauthState.metadata };
5562
+ async cleanExpired() {
5563
+ await this.ready;
5564
+ const rows = await this.sql`
5565
+ DELETE FROM auth_oauth_states
5566
+ WHERE expires_at < ${Date.now()}
5567
+ RETURNING state
5568
+ `;
5569
+ return rows.length;
4694
5570
  }
4695
- /**
4696
- * Get a registered provider by ID.
4697
- */
4698
- getProvider(providerId) {
4699
- const provider = this.providers.get(providerId);
4700
- if (!provider) {
4701
- throw new OAuthProviderNotFoundError(providerId);
4702
- }
4703
- return provider;
5571
+ async ensureTables() {
5572
+ await this.sql`
5573
+ CREATE TABLE IF NOT EXISTS auth_oauth_states (
5574
+ state TEXT PRIMARY KEY,
5575
+ provider TEXT NOT NULL,
5576
+ redirect_uri TEXT NOT NULL,
5577
+ created_at BIGINT NOT NULL,
5578
+ expires_at BIGINT NOT NULL,
5579
+ metadata_json TEXT,
5580
+ code_verifier TEXT
5581
+ )
5582
+ `;
5583
+ await this.sql`
5584
+ CREATE INDEX IF NOT EXISTS idx_auth_oauth_states_expires_at
5585
+ ON auth_oauth_states(expires_at)
5586
+ `;
4704
5587
  }
4705
- /**
4706
- * List all registered provider IDs.
4707
- */
4708
- getProviderIds() {
4709
- return [...this.providers.keys()];
5588
+ };
5589
+ var PostgresLinkedIdentityStore = class {
5590
+ sql;
5591
+ ready;
5592
+ constructor(sql) {
5593
+ this.sql = sql;
5594
+ this.ready = this.ensureTables();
4710
5595
  }
4711
- // --- Private ---
4712
- async exchangeCodeForTokens(provider, code) {
4713
- const body = new URLSearchParams({
4714
- grant_type: "authorization_code",
4715
- code,
4716
- redirect_uri: provider.redirectUri,
4717
- client_id: provider.clientId,
4718
- client_secret: provider.clientSecret
4719
- });
4720
- let response;
5596
+ async findByProvider(provider, providerUserId) {
5597
+ await this.ready;
5598
+ const rows = await this.sql`
5599
+ SELECT * FROM auth_linked_identities
5600
+ WHERE provider = ${provider} AND provider_user_id = ${providerUserId}
5601
+ `;
5602
+ return rows[0] ? rowToLinkedIdentity2(rows[0]) : null;
5603
+ }
5604
+ async findByUser(userId) {
5605
+ await this.ready;
5606
+ const rows = await this.sql`
5607
+ SELECT * FROM auth_linked_identities
5608
+ WHERE user_id = ${userId}
5609
+ ORDER BY linked_at ASC
5610
+ `;
5611
+ return rows.map(rowToLinkedIdentity2);
5612
+ }
5613
+ async create(params) {
5614
+ await this.ready;
5615
+ const identity = {
5616
+ id: (0, import_node_crypto12.randomUUID)(),
5617
+ userId: params.userId,
5618
+ provider: params.provider,
5619
+ providerUserId: params.providerUserId,
5620
+ email: params.email,
5621
+ linkedAt: Date.now()
5622
+ };
4721
5623
  try {
4722
- response = await this.fetchFn(provider.tokenUrl, {
4723
- method: "POST",
4724
- headers: {
4725
- "Content-Type": "application/x-www-form-urlencoded",
4726
- Accept: "application/json"
4727
- },
4728
- body: body.toString()
4729
- });
4730
- } catch (err) {
4731
- throw new OAuthCodeExchangeError(err instanceof Error ? err.message : "Network error");
4732
- }
4733
- if (!response.ok) {
4734
- let details = `HTTP ${response.status}`;
4735
- try {
4736
- const errorBody = await response.text();
4737
- details += `: ${errorBody}`;
4738
- } catch {
5624
+ await this.sql`
5625
+ INSERT INTO auth_linked_identities
5626
+ (id, user_id, provider, provider_user_id, email, linked_at)
5627
+ VALUES (
5628
+ ${identity.id},
5629
+ ${identity.userId},
5630
+ ${identity.provider},
5631
+ ${identity.providerUserId},
5632
+ ${identity.email},
5633
+ ${identity.linkedAt}
5634
+ )
5635
+ `;
5636
+ } catch (error) {
5637
+ if (isUniqueViolation(error)) {
5638
+ throw new DuplicateLinkedIdentityError(params.provider);
4739
5639
  }
4740
- throw new OAuthCodeExchangeError(details);
5640
+ throw error;
4741
5641
  }
4742
- const data = await response.json();
4743
- return {
4744
- accessToken: data.access_token,
4745
- tokenType: data.token_type ?? "Bearer",
4746
- expiresIn: data.expires_in,
4747
- refreshToken: data.refresh_token,
4748
- idToken: data.id_token,
4749
- scope: data.scope
4750
- };
5642
+ return identity;
4751
5643
  }
4752
- async fetchUserInfo(provider, accessToken) {
4753
- let response;
4754
- try {
4755
- response = await this.fetchFn(provider.userInfoUrl, {
4756
- headers: {
4757
- Authorization: `Bearer ${accessToken}`,
4758
- Accept: "application/json"
4759
- }
4760
- });
4761
- } catch (err) {
4762
- throw new OAuthUserInfoError(err instanceof Error ? err.message : "Network error");
4763
- }
4764
- if (!response.ok) {
4765
- throw new OAuthUserInfoError(`HTTP ${response.status}`);
4766
- }
4767
- const profile = await response.json();
4768
- return normalizeUserInfo(provider.providerId, profile);
5644
+ async delete(userId, provider) {
5645
+ await this.ready;
5646
+ await this.sql`
5647
+ DELETE FROM auth_linked_identities
5648
+ WHERE user_id = ${userId} AND provider = ${provider}
5649
+ `;
4769
5650
  }
4770
- };
4771
- function normalizeUserInfo(providerId, profile) {
4772
- switch (providerId) {
4773
- case "google":
4774
- return {
4775
- providerId: profile.sub,
4776
- provider: "google",
4777
- email: profile.email ?? null,
4778
- emailVerified: profile.email_verified ?? false,
4779
- name: profile.name ?? null,
4780
- avatarUrl: profile.picture ?? null,
4781
- rawProfile: profile
4782
- };
4783
- case "github":
4784
- return {
4785
- providerId: String(profile.id),
4786
- provider: "github",
4787
- email: profile.email ?? null,
4788
- emailVerified: false,
4789
- // GitHub doesn't confirm in the profile response
4790
- name: profile.name ?? profile.login ?? null,
4791
- avatarUrl: profile.avatar_url ?? null,
4792
- rawProfile: profile
4793
- };
4794
- case "microsoft":
4795
- return {
4796
- providerId: profile.id,
4797
- provider: "microsoft",
4798
- email: profile.mail ?? profile.userPrincipalName ?? null,
4799
- emailVerified: false,
4800
- name: profile.displayName ?? null,
4801
- avatarUrl: null,
4802
- rawProfile: profile
4803
- };
4804
- default:
4805
- return {
4806
- providerId: String(profile.id ?? profile.sub ?? ""),
4807
- provider: providerId,
4808
- email: profile.email ?? null,
4809
- emailVerified: profile.email_verified ?? false,
4810
- name: profile.name ?? null,
4811
- avatarUrl: profile.picture ?? profile.avatar_url ?? null,
4812
- rawProfile: profile
4813
- };
5651
+ async ensureTables() {
5652
+ await this.sql`
5653
+ CREATE TABLE IF NOT EXISTS auth_linked_identities (
5654
+ id TEXT PRIMARY KEY,
5655
+ user_id TEXT NOT NULL,
5656
+ provider TEXT NOT NULL,
5657
+ provider_user_id TEXT NOT NULL,
5658
+ email TEXT,
5659
+ linked_at BIGINT NOT NULL,
5660
+ UNIQUE(provider, provider_user_id),
5661
+ UNIQUE(user_id, provider)
5662
+ )
5663
+ `;
5664
+ await this.sql`
5665
+ CREATE INDEX IF NOT EXISTS idx_auth_linked_identities_user_id
5666
+ ON auth_linked_identities(user_id)
5667
+ `;
4814
5668
  }
5669
+ };
5670
+ async function createPostgresOAuthStateStore(options) {
5671
+ const postgresClient = await loadPostgresDeps2();
5672
+ const sql = postgresClient(options.connectionString);
5673
+ return new PostgresOAuthStateStore(sql);
4815
5674
  }
4816
- function googleProvider(config) {
5675
+ async function createPostgresLinkedIdentityStore(options) {
5676
+ const postgresClient = await loadPostgresDeps2();
5677
+ const sql = postgresClient(options.connectionString);
5678
+ return new PostgresLinkedIdentityStore(sql);
5679
+ }
5680
+ async function createPostgresOAuthStores(options) {
5681
+ const postgresClient = await loadPostgresDeps2();
5682
+ const sql = postgresClient(options.connectionString);
4817
5683
  return {
4818
- providerId: "google",
4819
- clientId: config.clientId,
4820
- clientSecret: config.clientSecret,
4821
- authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
4822
- tokenUrl: "https://oauth2.googleapis.com/token",
4823
- userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
4824
- scopes: config.scopes ?? ["openid", "email", "profile"],
4825
- redirectUri: config.redirectUri
5684
+ stateStore: new PostgresOAuthStateStore(sql),
5685
+ linkedIdentityStore: new PostgresLinkedIdentityStore(sql)
4826
5686
  };
4827
5687
  }
4828
- function githubProvider(config) {
5688
+ async function loadPostgresDeps2() {
5689
+ try {
5690
+ const dynamicImport = new Function("specifier", "return import(specifier)");
5691
+ const postgresMod = await dynamicImport("postgres");
5692
+ return postgresMod.default;
5693
+ } catch {
5694
+ throw new Error(
5695
+ 'PostgreSQL OAuth stores require the "postgres" package. Install it in your project dependencies.'
5696
+ );
5697
+ }
5698
+ }
5699
+ function rowToOAuthState2(row) {
4829
5700
  return {
4830
- providerId: "github",
4831
- clientId: config.clientId,
4832
- clientSecret: config.clientSecret,
4833
- authorizationUrl: "https://github.com/login/oauth/authorize",
4834
- tokenUrl: "https://github.com/login/oauth/access_token",
4835
- userInfoUrl: "https://api.github.com/user",
4836
- scopes: config.scopes ?? ["read:user", "user:email"],
4837
- redirectUri: config.redirectUri
5701
+ state: row.state,
5702
+ provider: row.provider,
5703
+ redirectUri: row.redirect_uri,
5704
+ createdAt: Number(row.created_at),
5705
+ expiresAt: Number(row.expires_at),
5706
+ metadata: parseMetadata2(row.metadata_json),
5707
+ codeVerifier: row.code_verifier ?? void 0
4838
5708
  };
4839
5709
  }
4840
- function microsoftProvider(config) {
4841
- const tenant = config.tenantId ?? "common";
5710
+ function parseMetadata2(value) {
5711
+ if (!value) return void 0;
5712
+ const parsed = JSON.parse(value);
5713
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
5714
+ }
5715
+ function rowToLinkedIdentity2(row) {
4842
5716
  return {
4843
- providerId: "microsoft",
4844
- clientId: config.clientId,
4845
- clientSecret: config.clientSecret,
4846
- authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
4847
- tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
4848
- userInfoUrl: "https://graph.microsoft.com/v1.0/me",
4849
- scopes: config.scopes ?? ["openid", "email", "profile", "User.Read"],
4850
- redirectUri: config.redirectUri
5717
+ id: row.id,
5718
+ userId: row.user_id,
5719
+ provider: row.provider,
5720
+ providerUserId: row.provider_user_id,
5721
+ email: row.email,
5722
+ linkedAt: Number(row.linked_at)
4851
5723
  };
4852
5724
  }
4853
- function generateState() {
4854
- const bytes = new Uint8Array(32);
4855
- globalThis.crypto.getRandomValues(bytes);
4856
- let binary = "";
4857
- for (let i = 0; i < bytes.length; i++) {
4858
- binary += String.fromCharCode(bytes[i]);
4859
- }
4860
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5725
+ function isUniqueViolation(error) {
5726
+ if (!(error instanceof Error)) return false;
5727
+ const code = error.code;
5728
+ return code === "23505" || error.message.includes("unique constraint") || error.message.includes("duplicate key");
4861
5729
  }
4862
5730
 
4863
5731
  // src/session/session.ts
4864
5732
  init_cjs_shims();
4865
- var import_core14 = require("@korajs/core");
4866
- var SessionError = class extends import_core14.KoraError {
5733
+ var import_core15 = require("@korajs/core");
5734
+ var SessionError = class extends import_core15.KoraError {
4867
5735
  constructor(message, code, context) {
4868
5736
  super(message, code, context);
4869
5737
  this.name = "SessionError";
@@ -5093,8 +5961,8 @@ function generateSessionId() {
5093
5961
 
5094
5962
  // src/mfa/totp.ts
5095
5963
  init_cjs_shims();
5096
- var import_core15 = require("@korajs/core");
5097
- var TotpError = class extends import_core15.KoraError {
5964
+ var import_core16 = require("@korajs/core");
5965
+ var TotpError = class extends import_core16.KoraError {
5098
5966
  constructor(message, code, context) {
5099
5967
  super(message, code, context);
5100
5968
  this.name = "TotpError";
@@ -5518,8 +6386,8 @@ function timingSafeEqual2(a, b) {
5518
6386
 
5519
6387
  // src/admin/admin-api.ts
5520
6388
  init_cjs_shims();
5521
- var import_core16 = require("@korajs/core");
5522
- var AdminApiError = class extends import_core16.KoraError {
6389
+ var import_core17 = require("@korajs/core");
6390
+ var AdminApiError = class extends import_core17.KoraError {
5523
6391
  constructor(message, code, context) {
5524
6392
  super(message, code, context);
5525
6393
  this.name = "AdminApiError";
@@ -5553,7 +6421,7 @@ var AdminApi = class {
5553
6421
  throw new AdminUserNotFoundError(userId);
5554
6422
  }
5555
6423
  await this.audit("admin.user_lookup", adminId, userId, "user");
5556
- return toAuthUser2(user);
6424
+ return toAuthUser3(user);
5557
6425
  }
5558
6426
  /**
5559
6427
  * List users with optional filtering and pagination.
@@ -5571,7 +6439,7 @@ var AdminApi = class {
5571
6439
  filtered = filtered.filter((u) => u.emailVerified === query.emailVerified);
5572
6440
  }
5573
6441
  const total = filtered.length;
5574
- const data = filtered.slice(offset, offset + limit).map(toAuthUser2);
6442
+ const data = filtered.slice(offset, offset + limit).map(toAuthUser3);
5575
6443
  return { data, total, limit, offset };
5576
6444
  }
5577
6445
  /**
@@ -5593,7 +6461,7 @@ var AdminApi = class {
5593
6461
  }
5594
6462
  await this.userStore.update(user);
5595
6463
  await this.audit("user.update", adminId, userId, "user", { updates });
5596
- return toAuthUser2(user);
6464
+ return toAuthUser3(user);
5597
6465
  }
5598
6466
  /**
5599
6467
  * Delete a user and all associated sessions.
@@ -5658,7 +6526,7 @@ var AdminApi = class {
5658
6526
  });
5659
6527
  }
5660
6528
  };
5661
- function toAuthUser2(stored) {
6529
+ function toAuthUser3(stored) {
5662
6530
  return {
5663
6531
  id: stored.id,
5664
6532
  email: stored.email,
@@ -5670,8 +6538,8 @@ function toAuthUser2(stored) {
5670
6538
 
5671
6539
  // src/admin/audit-log.ts
5672
6540
  init_cjs_shims();
5673
- var import_core17 = require("@korajs/core");
5674
- var AuditLogError = class extends import_core17.KoraError {
6541
+ var import_core18 = require("@korajs/core");
6542
+ var AuditLogError = class extends import_core18.KoraError {
5675
6543
  constructor(message, code, context) {
5676
6544
  super(message, code, context);
5677
6545
  this.name = "AuditLogError";
@@ -5797,8 +6665,8 @@ function generateAuditId() {
5797
6665
 
5798
6666
  // src/admin/webhooks.ts
5799
6667
  init_cjs_shims();
5800
- var import_core18 = require("@korajs/core");
5801
- var WebhookError = class extends import_core18.KoraError {
6668
+ var import_core19 = require("@korajs/core");
6669
+ var WebhookError = class extends import_core19.KoraError {
5802
6670
  constructor(message, code, context) {
5803
6671
  super(message, code, context);
5804
6672
  this.name = "WebhookError";
@@ -6028,6 +6896,7 @@ function delay(ms) {
6028
6896
  CannotRemoveOwnerError,
6029
6897
  CircularInheritanceError,
6030
6898
  DuplicateEmailError,
6899
+ DuplicateLinkedIdentityError,
6031
6900
  EmailVerificationError,
6032
6901
  EmailVerificationManager,
6033
6902
  ExternalAuthOperationNotSupportedError,
@@ -6037,6 +6906,7 @@ function delay(ms) {
6037
6906
  InMemoryAuditLogStore,
6038
6907
  InMemoryChallengeStore,
6039
6908
  InMemoryEmailVerificationStore,
6909
+ InMemoryLinkedIdentityStore,
6040
6910
  InMemoryOAuthStateStore,
6041
6911
  InMemoryOrgStore,
6042
6912
  InMemoryPasswordResetStore,
@@ -6069,6 +6939,8 @@ function delay(ms) {
6069
6939
  PasskeyVerificationError,
6070
6940
  PasswordResetError,
6071
6941
  PasswordResetManager,
6942
+ PostgresLinkedIdentityStore,
6943
+ PostgresOAuthStateStore,
6072
6944
  PostgresUserStore,
6073
6945
  ROLE_HIERARCHY,
6074
6946
  RbacEngine,
@@ -6083,6 +6955,8 @@ function delay(ms) {
6083
6955
  SessionManager,
6084
6956
  SessionMfaRequiredError,
6085
6957
  SessionNotFoundError,
6958
+ SqliteLinkedIdentityStore,
6959
+ SqliteOAuthStateStore,
6086
6960
  SqliteUserStore,
6087
6961
  TokenManager,
6088
6962
  TotpAlreadyEnabledError,
@@ -6101,7 +6975,14 @@ function delay(ms) {
6101
6975
  base32Encode,
6102
6976
  computePublicKeyThumbprint,
6103
6977
  createClerkAdapter,
6978
+ createKoraAuthServer,
6979
+ createPostgresLinkedIdentityStore,
6980
+ createPostgresOAuthStateStore,
6981
+ createPostgresOAuthStores,
6104
6982
  createPostgresUserStore,
6983
+ createSqliteLinkedIdentityStore,
6984
+ createSqliteOAuthStateStore,
6985
+ createSqliteOAuthStores,
6105
6986
  createSqliteUserStore,
6106
6987
  createSupabaseAdapter,
6107
6988
  decodeJwt,