@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.js CHANGED
@@ -635,7 +635,7 @@ var BuiltInAuthRoutes = class {
635
635
  const userStore = this.userStore;
636
636
  return {
637
637
  async authenticate(token) {
638
- const payload = tokenManager.validateToken(token);
638
+ const payload = await tokenManager.validateTokenWithRevocation(token);
639
639
  if (payload === null || payload.type !== "access") {
640
640
  return null;
641
641
  }
@@ -661,6 +661,9 @@ var BuiltInAuthRoutes = class {
661
661
  }
662
662
  };
663
663
 
664
+ // src/provider/built-in/quickstart-server.ts
665
+ import { randomUUID as randomUUID4 } from "crypto";
666
+
664
667
  // src/tokens/token-manager.ts
665
668
  import { randomBytes as randomBytes2, randomUUID } from "crypto";
666
669
 
@@ -773,7 +776,7 @@ var InMemoryTokenRevocationStore = class {
773
776
  /**
774
777
  * Check if a device has been revoked.
775
778
  */
776
- isDeviceRevoked(deviceId) {
779
+ async isDeviceRevoked(deviceId) {
777
780
  return this.revokedDevices.has(deviceId);
778
781
  }
779
782
  /**
@@ -966,7 +969,7 @@ var TokenManager = class {
966
969
  * Validate a token and check it against the revocation store.
967
970
  *
968
971
  * Like {@link validateToken}, but also checks whether the token's `jti` has been
969
- * revoked. Requires a revocation store to be configured.
972
+ * revoked or belongs to a revoked device. Requires a revocation store to be configured.
970
973
  *
971
974
  * @param token - The JWT string to validate
972
975
  * @returns The decoded {@link TokenPayload} if valid and not revoked, or null otherwise
@@ -981,6 +984,10 @@ var TokenManager = class {
981
984
  if (revoked) {
982
985
  return null;
983
986
  }
987
+ const deviceRevoked = await this.revocationStore.isDeviceRevoked(payload.dev);
988
+ if (deviceRevoked) {
989
+ return null;
990
+ }
984
991
  }
985
992
  return payload;
986
993
  }
@@ -1047,217 +1054,1000 @@ var TokenManager = class {
1047
1054
  }
1048
1055
  };
1049
1056
 
1050
- // src/provider/built-in/password-reset.ts
1057
+ // src/provider/oauth/linked-identity-store.ts
1058
+ import { randomUUID as randomUUID2 } from "crypto";
1051
1059
  import { KoraError as KoraError2 } from "@korajs/core";
1052
- var PasswordResetError = class extends KoraError2 {
1060
+ var DuplicateLinkedIdentityError = class extends KoraError2 {
1061
+ constructor(provider) {
1062
+ super(`This ${provider} account is already linked.`, "DUPLICATE_LINKED_IDENTITY", {
1063
+ provider
1064
+ });
1065
+ this.name = "DuplicateLinkedIdentityError";
1066
+ }
1067
+ };
1068
+ var InMemoryLinkedIdentityStore = class {
1069
+ identitiesById = /* @__PURE__ */ new Map();
1070
+ identityIdByProvider = /* @__PURE__ */ new Map();
1071
+ identityIdsByUser = /* @__PURE__ */ new Map();
1072
+ identityIdByUserProvider = /* @__PURE__ */ new Map();
1073
+ async findByProvider(provider, providerUserId) {
1074
+ const id = this.identityIdByProvider.get(providerIdentityKey(provider, providerUserId));
1075
+ return id ? this.identitiesById.get(id) ?? null : null;
1076
+ }
1077
+ async findByUser(userId) {
1078
+ const ids = this.identityIdsByUser.get(userId);
1079
+ if (!ids) return [];
1080
+ return [...ids].map((id) => this.identitiesById.get(id)).filter((identity) => identity !== void 0);
1081
+ }
1082
+ async create(params) {
1083
+ const providerKey = providerIdentityKey(params.provider, params.providerUserId);
1084
+ const userProviderKeyValue = userProviderKey(params.userId, params.provider);
1085
+ if (this.identityIdByProvider.has(providerKey) || this.identityIdByUserProvider.has(userProviderKeyValue)) {
1086
+ throw new DuplicateLinkedIdentityError(params.provider);
1087
+ }
1088
+ const identity = {
1089
+ id: randomUUID2(),
1090
+ userId: params.userId,
1091
+ provider: params.provider,
1092
+ providerUserId: params.providerUserId,
1093
+ email: params.email,
1094
+ linkedAt: Date.now()
1095
+ };
1096
+ this.identitiesById.set(identity.id, identity);
1097
+ this.identityIdByProvider.set(providerKey, identity.id);
1098
+ this.identityIdByUserProvider.set(userProviderKeyValue, identity.id);
1099
+ const userIdentities = this.identityIdsByUser.get(params.userId) ?? /* @__PURE__ */ new Set();
1100
+ userIdentities.add(identity.id);
1101
+ this.identityIdsByUser.set(params.userId, userIdentities);
1102
+ return identity;
1103
+ }
1104
+ async delete(userId, provider) {
1105
+ const userProviderKeyValue = userProviderKey(userId, provider);
1106
+ const id = this.identityIdByUserProvider.get(userProviderKeyValue);
1107
+ if (!id) return;
1108
+ const identity = this.identitiesById.get(id);
1109
+ this.identitiesById.delete(id);
1110
+ this.identityIdByUserProvider.delete(userProviderKeyValue);
1111
+ if (identity) {
1112
+ this.identityIdByProvider.delete(
1113
+ providerIdentityKey(identity.provider, identity.providerUserId)
1114
+ );
1115
+ }
1116
+ const userIdentities = this.identityIdsByUser.get(userId);
1117
+ userIdentities?.delete(id);
1118
+ if (userIdentities?.size === 0) {
1119
+ this.identityIdsByUser.delete(userId);
1120
+ }
1121
+ }
1122
+ };
1123
+ function providerIdentityKey(provider, providerUserId) {
1124
+ return `${provider}:${providerUserId}`;
1125
+ }
1126
+ function userProviderKey(userId, provider) {
1127
+ return `${userId}:${provider}`;
1128
+ }
1129
+
1130
+ // src/provider/oauth/oauth-types.ts
1131
+ import { KoraError as KoraError3 } from "@korajs/core";
1132
+ var OAuthError = class extends KoraError3 {
1053
1133
  constructor(message, code, context) {
1054
1134
  super(message, code, context);
1055
- this.name = "PasswordResetError";
1135
+ this.name = "OAuthError";
1056
1136
  }
1057
1137
  };
1058
- var ResetTokenExpiredError = class extends PasswordResetError {
1138
+ var OAuthStateMismatchError = class extends OAuthError {
1059
1139
  constructor() {
1060
- super("Password reset token has expired.", "RESET_TOKEN_EXPIRED");
1140
+ super("OAuth state parameter does not match. Possible CSRF attack.", "OAUTH_STATE_MISMATCH");
1061
1141
  }
1062
1142
  };
1063
- var ResetTokenNotFoundError = class extends PasswordResetError {
1064
- constructor() {
1065
- super("Password reset token not found or already used.", "RESET_TOKEN_NOT_FOUND");
1143
+ var OAuthCodeExchangeError = class extends OAuthError {
1144
+ constructor(details) {
1145
+ super(
1146
+ `Failed to exchange authorization code for tokens.${details ? ` ${details}` : ""}`,
1147
+ "OAUTH_CODE_EXCHANGE_FAILED",
1148
+ details ? { details } : void 0
1149
+ );
1066
1150
  }
1067
1151
  };
1068
- var ResetRateLimitedError = class extends PasswordResetError {
1069
- constructor() {
1070
- super("Too many password reset requests. Please try again later.", "RESET_RATE_LIMITED");
1152
+ var OAuthUserInfoError = class extends OAuthError {
1153
+ constructor(details) {
1154
+ super(
1155
+ `Failed to fetch user info from OAuth provider.${details ? ` ${details}` : ""}`,
1156
+ "OAUTH_USER_INFO_FAILED",
1157
+ details ? { details } : void 0
1158
+ );
1071
1159
  }
1072
1160
  };
1073
- var InMemoryPasswordResetStore = class {
1074
- tokens = /* @__PURE__ */ new Map();
1075
- async store(token) {
1076
- this.tokens.set(token.token, token);
1077
- }
1078
- async get(token) {
1079
- return this.tokens.get(token) ?? null;
1161
+ var OAuthProviderNotFoundError = class extends OAuthError {
1162
+ constructor(provider) {
1163
+ super(`OAuth provider "${provider}" is not configured.`, "OAUTH_PROVIDER_NOT_FOUND", {
1164
+ provider
1165
+ });
1080
1166
  }
1081
- async consume(token) {
1082
- const entry = this.tokens.get(token);
1083
- if (entry) {
1084
- this.tokens.set(token, { ...entry, consumed: true });
1085
- }
1167
+ };
1168
+
1169
+ // src/provider/oauth/oauth-flow.ts
1170
+ var DEFAULT_STATE_TTL_MS = 10 * 60 * 1e3;
1171
+ var PKCE_VERIFIER_BYTES = 32;
1172
+ var InMemoryOAuthStateStore = class {
1173
+ states = /* @__PURE__ */ new Map();
1174
+ async store(state) {
1175
+ this.states.set(state.state, state);
1086
1176
  }
1087
- async countActiveForEmail(email) {
1088
- const now = Date.now();
1089
- let count = 0;
1090
- for (const token of this.tokens.values()) {
1091
- if (token.email === email && !token.consumed && now < token.expiresAt) {
1092
- count++;
1093
- }
1094
- }
1095
- return count;
1177
+ async consume(stateValue) {
1178
+ const state = this.states.get(stateValue);
1179
+ if (!state) return null;
1180
+ this.states.delete(stateValue);
1181
+ if (Date.now() > state.expiresAt) return null;
1182
+ return state;
1096
1183
  }
1097
1184
  async cleanExpired() {
1098
1185
  const now = Date.now();
1099
1186
  let count = 0;
1100
- for (const [key, token] of this.tokens) {
1101
- if (now > token.expiresAt) {
1102
- this.tokens.delete(key);
1187
+ for (const [key, state] of this.states) {
1188
+ if (now > state.expiresAt) {
1189
+ this.states.delete(key);
1103
1190
  count++;
1104
1191
  }
1105
1192
  }
1106
1193
  return count;
1107
1194
  }
1108
1195
  };
1109
- var DEFAULT_TOKEN_TTL_MS = 60 * 60 * 1e3;
1110
- var DEFAULT_MAX_REQUESTS = 3;
1111
- var MIN_PASSWORD_LENGTH2 = 8;
1112
- var MAX_PASSWORD_LENGTH2 = 128;
1113
- var PasswordResetManager = class {
1114
- userStore;
1115
- resetStore;
1116
- tokenTtlMs;
1117
- maxRequestsPerEmail;
1118
- onResetRequested;
1196
+ var OAuthManager = class {
1197
+ providers = /* @__PURE__ */ new Map();
1198
+ stateStore;
1199
+ stateTtlMs;
1200
+ fetchFn;
1119
1201
  constructor(config) {
1120
- this.userStore = config.userStore;
1121
- this.resetStore = config.resetStore ?? new InMemoryPasswordResetStore();
1122
- this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
1123
- this.maxRequestsPerEmail = config.maxRequestsPerEmail ?? DEFAULT_MAX_REQUESTS;
1124
- this.onResetRequested = config.onResetRequested;
1202
+ for (const provider of config.providers) {
1203
+ this.providers.set(provider.providerId, provider);
1204
+ }
1205
+ this.stateStore = config.stateStore ?? new InMemoryOAuthStateStore();
1206
+ this.stateTtlMs = config.stateTtlMs ?? DEFAULT_STATE_TTL_MS;
1207
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
1125
1208
  }
1126
1209
  /**
1127
- * Request a password reset for an email.
1128
- * Always returns success to prevent email enumeration.
1129
- *
1130
- * If a callback is configured, invokes it with the token.
1131
- * In development mode (no callback), returns the token in the response.
1210
+ * Generate an authorization URL for the user to visit.
1211
+ * Returns the URL and the state parameter for CSRF validation.
1132
1212
  */
1133
- async requestReset(email) {
1134
- const normalizedEmail = email.toLowerCase().trim();
1135
- const successResponse = {
1136
- status: 200,
1137
- body: {
1138
- data: {
1139
- message: "If an account with that email exists, a password reset link has been sent."
1140
- }
1141
- }
1142
- };
1143
- const user = await this.userStore.findByEmail(normalizedEmail);
1144
- if (!user) {
1145
- return successResponse;
1146
- }
1147
- const activeCount = await this.resetStore.countActiveForEmail(normalizedEmail);
1148
- if (activeCount >= this.maxRequestsPerEmail) {
1149
- return successResponse;
1150
- }
1151
- const token = generateSecureToken();
1213
+ async getAuthorizationUrl(providerId, metadata) {
1214
+ const provider = this.getProvider(providerId);
1215
+ const state = generateState();
1216
+ const codeVerifier = provider.pkce ? generateCodeVerifier() : void 0;
1217
+ const codeChallenge = codeVerifier ? await createCodeChallenge(codeVerifier) : void 0;
1152
1218
  const now = Date.now();
1153
- const resetToken = {
1154
- token,
1155
- userId: user.id,
1156
- email: normalizedEmail,
1219
+ const oauthState = {
1220
+ state,
1221
+ provider: providerId,
1222
+ redirectUri: provider.redirectUri,
1157
1223
  createdAt: now,
1158
- expiresAt: now + this.tokenTtlMs,
1159
- consumed: false
1224
+ expiresAt: now + this.stateTtlMs,
1225
+ metadata,
1226
+ codeVerifier
1160
1227
  };
1161
- await this.resetStore.store(resetToken);
1162
- if (this.onResetRequested) {
1163
- try {
1164
- await this.onResetRequested(normalizedEmail, token, resetToken.expiresAt);
1165
- } catch {
1166
- }
1167
- }
1168
- if (!this.onResetRequested) {
1169
- successResponse.body = { data: { message: "Password reset token generated.", token } };
1228
+ await this.stateStore.store(oauthState);
1229
+ const params = new URLSearchParams({
1230
+ client_id: provider.clientId,
1231
+ redirect_uri: provider.redirectUri,
1232
+ response_type: "code",
1233
+ scope: provider.scopes.join(" "),
1234
+ state
1235
+ });
1236
+ if (codeChallenge) {
1237
+ params.set("code_challenge", codeChallenge);
1238
+ params.set("code_challenge_method", "S256");
1170
1239
  }
1171
- return successResponse;
1240
+ const url = `${provider.authorizationUrl}?${params.toString()}`;
1241
+ return { url, state };
1172
1242
  }
1173
1243
  /**
1174
- * Consume a reset token and set a new password.
1244
+ * Handle the OAuth callback after the user authorizes.
1245
+ * Validates the state parameter, exchanges the code for tokens,
1246
+ * and fetches user info.
1247
+ *
1248
+ * @param providerId - The OAuth provider
1249
+ * @param code - The authorization code from the callback
1250
+ * @param state - The state parameter from the callback
1251
+ * @returns Tokens and user info from the provider
1175
1252
  */
1176
- async resetPassword(token, newPassword) {
1177
- if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
1178
- return {
1179
- status: 400,
1180
- body: { error: `Password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
1181
- };
1182
- }
1183
- if (newPassword.length > MAX_PASSWORD_LENGTH2) {
1184
- return {
1185
- status: 400,
1186
- body: { error: `Password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
1187
- };
1188
- }
1189
- const resetToken = await this.resetStore.get(token);
1190
- if (!resetToken || resetToken.consumed) {
1191
- return { status: 404, body: { error: "Password reset token not found or already used." } };
1253
+ async handleCallback(providerId, code, state) {
1254
+ const provider = this.getProvider(providerId);
1255
+ const oauthState = await this.stateStore.consume(state);
1256
+ if (!oauthState || oauthState.provider !== providerId) {
1257
+ throw new OAuthStateMismatchError();
1192
1258
  }
1193
- if (Date.now() > resetToken.expiresAt) {
1194
- await this.resetStore.consume(token);
1195
- return { status: 410, body: { error: "Password reset token has expired." } };
1259
+ const tokens = await this.exchangeCodeForTokens(provider, code, oauthState);
1260
+ const userInfo = await this.fetchUserInfo(provider, tokens.accessToken);
1261
+ return { tokens, userInfo, stateMetadata: oauthState.metadata };
1262
+ }
1263
+ /**
1264
+ * Get a registered provider by ID.
1265
+ */
1266
+ getProvider(providerId) {
1267
+ const provider = this.providers.get(providerId);
1268
+ if (!provider) {
1269
+ throw new OAuthProviderNotFoundError(providerId);
1196
1270
  }
1197
- await this.resetStore.consume(token);
1198
- const hashed = await hashPassword(newPassword);
1199
- await this.userStore.updatePassword(resetToken.userId, hashed.hash, hashed.salt);
1200
- return { status: 200, body: { data: { message: "Password has been reset successfully." } } };
1271
+ return provider;
1201
1272
  }
1202
1273
  /**
1203
- * Change password for an authenticated user (requires current password verification).
1274
+ * List all registered provider IDs.
1204
1275
  */
1205
- async changePassword(userId, currentPassword, newPassword) {
1206
- if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
1207
- return {
1208
- status: 400,
1209
- body: { error: `New password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
1210
- };
1276
+ getProviderIds() {
1277
+ return [...this.providers.keys()];
1278
+ }
1279
+ // --- Private ---
1280
+ async exchangeCodeForTokens(provider, code, oauthState) {
1281
+ const body = new URLSearchParams({
1282
+ grant_type: "authorization_code",
1283
+ code,
1284
+ redirect_uri: provider.redirectUri,
1285
+ client_id: provider.clientId
1286
+ });
1287
+ if (provider.clientSecret) {
1288
+ body.set("client_secret", provider.clientSecret);
1211
1289
  }
1212
- if (newPassword.length > MAX_PASSWORD_LENGTH2) {
1213
- return {
1214
- status: 400,
1215
- body: { error: `New password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
1216
- };
1290
+ if (oauthState.codeVerifier) {
1291
+ body.set("code_verifier", oauthState.codeVerifier);
1217
1292
  }
1218
- const user = await this.userStore.findById(userId);
1219
- if (!user) {
1220
- return { status: 404, body: { error: "User not found." } };
1293
+ let response;
1294
+ try {
1295
+ response = await this.fetchFn(provider.tokenUrl, {
1296
+ method: "POST",
1297
+ headers: {
1298
+ "Content-Type": "application/x-www-form-urlencoded",
1299
+ Accept: "application/json"
1300
+ },
1301
+ body: body.toString()
1302
+ });
1303
+ } catch (err) {
1304
+ throw new OAuthCodeExchangeError(err instanceof Error ? err.message : "Network error");
1221
1305
  }
1222
- const { verifyPassword: verifyPassword2 } = await import("./password-hash-QRBG6BNJ.js");
1223
- const isValid = await verifyPassword2(currentPassword, user.passwordHash, user.salt);
1224
- if (!isValid) {
1225
- return { status: 401, body: { error: "Current password is incorrect." } };
1306
+ if (!response.ok) {
1307
+ let details = `HTTP ${response.status}`;
1308
+ try {
1309
+ const errorBody = await response.text();
1310
+ details += `: ${errorBody}`;
1311
+ } catch {
1312
+ }
1313
+ throw new OAuthCodeExchangeError(details);
1314
+ }
1315
+ const data = await response.json();
1316
+ return {
1317
+ accessToken: data.access_token,
1318
+ tokenType: data.token_type ?? "Bearer",
1319
+ expiresIn: data.expires_in,
1320
+ refreshToken: data.refresh_token,
1321
+ idToken: data.id_token,
1322
+ scope: data.scope
1323
+ };
1324
+ }
1325
+ async fetchUserInfo(provider, accessToken) {
1326
+ let response;
1327
+ try {
1328
+ response = await this.fetchFn(provider.userInfoUrl, {
1329
+ headers: {
1330
+ Authorization: `Bearer ${accessToken}`,
1331
+ Accept: "application/json"
1332
+ }
1333
+ });
1334
+ } catch (err) {
1335
+ throw new OAuthUserInfoError(err instanceof Error ? err.message : "Network error");
1336
+ }
1337
+ if (!response.ok) {
1338
+ throw new OAuthUserInfoError(`HTTP ${response.status}`);
1339
+ }
1340
+ const profile = await response.json();
1341
+ return normalizeUserInfo(provider.providerId, profile);
1342
+ }
1343
+ };
1344
+ function normalizeUserInfo(providerId, profile) {
1345
+ switch (providerId) {
1346
+ case "google":
1347
+ return {
1348
+ providerId: profile.sub,
1349
+ provider: "google",
1350
+ email: profile.email ?? null,
1351
+ emailVerified: profile.email_verified ?? false,
1352
+ name: profile.name ?? null,
1353
+ avatarUrl: profile.picture ?? null,
1354
+ rawProfile: profile
1355
+ };
1356
+ case "github":
1357
+ return {
1358
+ providerId: String(profile.id),
1359
+ provider: "github",
1360
+ email: profile.email ?? null,
1361
+ emailVerified: false,
1362
+ // GitHub doesn't confirm in the profile response
1363
+ name: profile.name ?? profile.login ?? null,
1364
+ avatarUrl: profile.avatar_url ?? null,
1365
+ rawProfile: profile
1366
+ };
1367
+ case "microsoft":
1368
+ return {
1369
+ providerId: profile.id,
1370
+ provider: "microsoft",
1371
+ email: profile.mail ?? profile.userPrincipalName ?? null,
1372
+ emailVerified: false,
1373
+ name: profile.displayName ?? null,
1374
+ avatarUrl: null,
1375
+ rawProfile: profile
1376
+ };
1377
+ default:
1378
+ return {
1379
+ providerId: String(profile.id ?? profile.sub ?? ""),
1380
+ provider: providerId,
1381
+ email: profile.email ?? null,
1382
+ emailVerified: profile.email_verified ?? false,
1383
+ name: profile.name ?? null,
1384
+ avatarUrl: profile.picture ?? profile.avatar_url ?? null,
1385
+ rawProfile: profile
1386
+ };
1387
+ }
1388
+ }
1389
+ function googleProvider(config) {
1390
+ return {
1391
+ providerId: "google",
1392
+ clientId: config.clientId,
1393
+ clientSecret: config.clientSecret,
1394
+ authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1395
+ tokenUrl: "https://oauth2.googleapis.com/token",
1396
+ userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
1397
+ scopes: config.scopes ?? ["openid", "email", "profile"],
1398
+ redirectUri: config.redirectUri,
1399
+ pkce: config.pkce
1400
+ };
1401
+ }
1402
+ function githubProvider(config) {
1403
+ return {
1404
+ providerId: "github",
1405
+ clientId: config.clientId,
1406
+ clientSecret: config.clientSecret,
1407
+ authorizationUrl: "https://github.com/login/oauth/authorize",
1408
+ tokenUrl: "https://github.com/login/oauth/access_token",
1409
+ userInfoUrl: "https://api.github.com/user",
1410
+ scopes: config.scopes ?? ["read:user", "user:email"],
1411
+ redirectUri: config.redirectUri,
1412
+ pkce: config.pkce
1413
+ };
1414
+ }
1415
+ function microsoftProvider(config) {
1416
+ const tenant = config.tenantId ?? "common";
1417
+ return {
1418
+ providerId: "microsoft",
1419
+ clientId: config.clientId,
1420
+ clientSecret: config.clientSecret,
1421
+ authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
1422
+ tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
1423
+ userInfoUrl: "https://graph.microsoft.com/v1.0/me",
1424
+ scopes: config.scopes ?? ["openid", "email", "profile", "User.Read"],
1425
+ redirectUri: config.redirectUri,
1426
+ pkce: config.pkce
1427
+ };
1428
+ }
1429
+ function generateState() {
1430
+ const bytes = new Uint8Array(32);
1431
+ globalThis.crypto.getRandomValues(bytes);
1432
+ return toBase64Url2(bytes);
1433
+ }
1434
+ function generateCodeVerifier() {
1435
+ const bytes = new Uint8Array(PKCE_VERIFIER_BYTES);
1436
+ globalThis.crypto.getRandomValues(bytes);
1437
+ return toBase64Url2(bytes);
1438
+ }
1439
+ async function createCodeChallenge(codeVerifier) {
1440
+ const data = new TextEncoder().encode(codeVerifier);
1441
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
1442
+ return toBase64Url2(new Uint8Array(digest));
1443
+ }
1444
+ function toBase64Url2(bytes) {
1445
+ let binary = "";
1446
+ for (let i = 0; i < bytes.length; i++) {
1447
+ binary += String.fromCharCode(bytes[i]);
1448
+ }
1449
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1450
+ }
1451
+
1452
+ // src/provider/built-in/user-store.ts
1453
+ import { randomUUID as randomUUID3 } from "crypto";
1454
+ import { KoraError as KoraError4 } from "@korajs/core";
1455
+ var DuplicateEmailError = class extends KoraError4 {
1456
+ constructor() {
1457
+ super("A user with this email already exists.", "DUPLICATE_EMAIL");
1458
+ this.name = "DuplicateEmailError";
1459
+ }
1460
+ };
1461
+ var InMemoryUserStore = class {
1462
+ /** Users indexed by ID */
1463
+ usersById = /* @__PURE__ */ new Map();
1464
+ /** Users indexed by email (lowercase) for fast lookup */
1465
+ usersByEmail = /* @__PURE__ */ new Map();
1466
+ /** Devices indexed by device ID */
1467
+ devicesById = /* @__PURE__ */ new Map();
1468
+ /** Device IDs indexed by user ID for fast listing */
1469
+ devicesByUserId = /* @__PURE__ */ new Map();
1470
+ /**
1471
+ * Create a new user account.
1472
+ *
1473
+ * @param params - User creation parameters
1474
+ * @param params.email - The user's email address (must be unique, case-insensitive)
1475
+ * @param params.passwordHash - Hex-encoded PBKDF2 derived key
1476
+ * @param params.salt - Hex-encoded salt used during hashing
1477
+ * @param params.name - The user's display name
1478
+ * @returns The created user (without sensitive credential fields)
1479
+ * @throws {DuplicateEmailError} If a user with the same email already exists
1480
+ */
1481
+ async createUser(params) {
1482
+ const normalizedEmail = params.email.toLowerCase();
1483
+ if (this.usersByEmail.has(normalizedEmail)) {
1484
+ throw new DuplicateEmailError();
1485
+ }
1486
+ const now = Date.now();
1487
+ const id = randomUUID3();
1488
+ const storedUser = {
1489
+ id,
1490
+ email: normalizedEmail,
1491
+ name: params.name,
1492
+ emailVerified: false,
1493
+ createdAt: now,
1494
+ passwordHash: params.passwordHash,
1495
+ salt: params.salt
1496
+ };
1497
+ this.usersById.set(id, storedUser);
1498
+ this.usersByEmail.set(normalizedEmail, storedUser);
1499
+ return toAuthUser(storedUser);
1500
+ }
1501
+ /**
1502
+ * Find a user by email address.
1503
+ *
1504
+ * @param email - The email to search for (case-insensitive)
1505
+ * @returns The stored user record including credentials, or null if not found
1506
+ */
1507
+ async findByEmail(email) {
1508
+ return this.usersByEmail.get(email.toLowerCase()) ?? null;
1509
+ }
1510
+ /**
1511
+ * Find a user by ID.
1512
+ *
1513
+ * @param id - The user ID to search for
1514
+ * @returns The stored user record including credentials, or null if not found
1515
+ */
1516
+ async findById(id) {
1517
+ return this.usersById.get(id) ?? null;
1518
+ }
1519
+ /**
1520
+ * Register a device for a user.
1521
+ *
1522
+ * If a device with the same ID already exists and is not revoked, it is
1523
+ * returned as-is (idempotent registration). If it was previously revoked,
1524
+ * it is re-activated with updated details.
1525
+ *
1526
+ * @param params - Device registration parameters
1527
+ * @param params.id - Unique device identifier
1528
+ * @param params.userId - ID of the user who owns the device
1529
+ * @param params.publicKey - Base64url-encoded device public key or thumbprint
1530
+ * @param params.name - Human-readable device name
1531
+ * @returns The registered device record
1532
+ */
1533
+ async registerDevice(params) {
1534
+ const existing = this.devicesById.get(params.id);
1535
+ if (existing !== void 0 && !existing.revoked) {
1536
+ return existing;
1537
+ }
1538
+ const now = Date.now();
1539
+ const device = {
1540
+ id: params.id,
1541
+ userId: params.userId,
1542
+ publicKey: params.publicKey,
1543
+ name: params.name,
1544
+ revoked: false,
1545
+ createdAt: now,
1546
+ lastSeenAt: now
1547
+ };
1548
+ this.devicesById.set(params.id, device);
1549
+ let userDevices = this.devicesByUserId.get(params.userId);
1550
+ if (userDevices === void 0) {
1551
+ userDevices = /* @__PURE__ */ new Set();
1552
+ this.devicesByUserId.set(params.userId, userDevices);
1553
+ }
1554
+ userDevices.add(params.id);
1555
+ return device;
1556
+ }
1557
+ /**
1558
+ * Find a device by its ID.
1559
+ *
1560
+ * @param deviceId - The device ID to search for
1561
+ * @returns The device record, or null if not found
1562
+ */
1563
+ async findDevice(deviceId) {
1564
+ return this.devicesById.get(deviceId) ?? null;
1565
+ }
1566
+ /**
1567
+ * List all devices registered for a user.
1568
+ *
1569
+ * @param userId - The user ID whose devices to list
1570
+ * @returns Array of device records (includes revoked devices)
1571
+ */
1572
+ async listDevices(userId) {
1573
+ const deviceIds = this.devicesByUserId.get(userId);
1574
+ if (deviceIds === void 0) {
1575
+ return [];
1576
+ }
1577
+ const devices = [];
1578
+ for (const deviceId of deviceIds) {
1579
+ const device = this.devicesById.get(deviceId);
1580
+ if (device !== void 0) {
1581
+ devices.push(device);
1582
+ }
1583
+ }
1584
+ return devices;
1585
+ }
1586
+ /**
1587
+ * Revoke a device, preventing it from being used for authentication.
1588
+ *
1589
+ * This is a soft revoke — the device record remains but is marked as revoked.
1590
+ * If the device does not exist, this is a no-op.
1591
+ *
1592
+ * @param deviceId - The ID of the device to revoke
1593
+ */
1594
+ async revokeDevice(deviceId) {
1595
+ const device = this.devicesById.get(deviceId);
1596
+ if (device !== void 0) {
1597
+ device.revoked = true;
1598
+ }
1599
+ }
1600
+ /**
1601
+ * Set a user's email verification status.
1602
+ *
1603
+ * @param userId - The user whose email to verify
1604
+ * @param verified - Whether the email is verified
1605
+ */
1606
+ async setEmailVerified(userId, verified) {
1607
+ const user = this.usersById.get(userId);
1608
+ if (!user) return;
1609
+ const updated = { ...user, emailVerified: verified };
1610
+ this.usersById.set(userId, updated);
1611
+ this.usersByEmail.set(user.email, updated);
1612
+ }
1613
+ /**
1614
+ * Update a user's password hash and salt.
1615
+ *
1616
+ * @param userId - The user whose password to update
1617
+ * @param passwordHash - New hex-encoded PBKDF2 derived key
1618
+ * @param salt - New hex-encoded salt
1619
+ */
1620
+ async updatePassword(userId, passwordHash, salt) {
1621
+ const user = this.usersById.get(userId);
1622
+ if (!user) return;
1623
+ const updated = { ...user, passwordHash, salt };
1624
+ this.usersById.set(userId, updated);
1625
+ this.usersByEmail.set(user.email, updated);
1626
+ }
1627
+ /**
1628
+ * List all users. For admin/development use.
1629
+ */
1630
+ async listAll() {
1631
+ return [...this.usersById.values()];
1632
+ }
1633
+ /**
1634
+ * Update a stored user record.
1635
+ */
1636
+ async update(user) {
1637
+ const existing = this.usersById.get(user.id);
1638
+ if (!existing) return;
1639
+ if (existing.email !== user.email) {
1640
+ this.usersByEmail.delete(existing.email);
1641
+ this.usersByEmail.set(user.email, user);
1642
+ } else {
1643
+ this.usersByEmail.set(user.email, user);
1644
+ }
1645
+ this.usersById.set(user.id, user);
1646
+ }
1647
+ /**
1648
+ * Delete a user and all associated devices.
1649
+ */
1650
+ async delete(userId) {
1651
+ const user = this.usersById.get(userId);
1652
+ if (!user) return;
1653
+ this.usersById.delete(userId);
1654
+ this.usersByEmail.delete(user.email);
1655
+ const deviceIds = this.devicesByUserId.get(userId);
1656
+ if (deviceIds) {
1657
+ for (const deviceId of deviceIds) {
1658
+ this.devicesById.delete(deviceId);
1659
+ }
1660
+ this.devicesByUserId.delete(userId);
1661
+ }
1662
+ }
1663
+ /**
1664
+ * Update the last-seen timestamp for a device.
1665
+ *
1666
+ * Called when a device authenticates or syncs to track activity.
1667
+ * If the device does not exist, this is a no-op.
1668
+ *
1669
+ * @param deviceId - The ID of the device to update
1670
+ */
1671
+ async touchDevice(deviceId) {
1672
+ const device = this.devicesById.get(deviceId);
1673
+ if (device !== void 0) {
1674
+ device.lastSeenAt = Date.now();
1226
1675
  }
1227
- const hashed = await hashPassword(newPassword);
1228
- await this.userStore.updatePassword(userId, hashed.hash, hashed.salt);
1229
- return { status: 200, body: { data: { message: "Password changed successfully." } } };
1230
1676
  }
1231
1677
  };
1232
- function generateSecureToken() {
1233
- const bytes = new Uint8Array(32);
1234
- globalThis.crypto.getRandomValues(bytes);
1235
- let binary = "";
1236
- for (let i = 0; i < bytes.length; i++) {
1237
- binary += String.fromCharCode(bytes[i]);
1678
+ function toAuthUser(stored) {
1679
+ return {
1680
+ id: stored.id,
1681
+ email: stored.email,
1682
+ name: stored.name,
1683
+ emailVerified: stored.emailVerified,
1684
+ createdAt: stored.createdAt
1685
+ };
1686
+ }
1687
+
1688
+ // src/provider/built-in/quickstart-server.ts
1689
+ function createKoraAuthServer(options = {}) {
1690
+ const userStore = options.userStore ?? new InMemoryUserStore();
1691
+ const tokenManager = options.tokenManager ?? createDefaultTokenManager(options);
1692
+ const routes = new BuiltInAuthRoutes({
1693
+ userStore,
1694
+ tokenManager,
1695
+ challengeStore: options.challengeStore,
1696
+ rateLimiter: options.rateLimiter
1697
+ });
1698
+ const oauth = options.oauth ? createOAuthRuntime(options.oauth) : void 0;
1699
+ const path = normalizePath(options.path ?? "/auth");
1700
+ return {
1701
+ routes,
1702
+ userStore,
1703
+ tokenManager,
1704
+ oauth: oauth?.manager,
1705
+ linkedIdentityStore: oauth?.linkedIdentityStore,
1706
+ auth: routes.toSyncAuthProvider(),
1707
+ handleRequest(request) {
1708
+ return handleAuthRequest(routes, path, request, oauth, userStore, tokenManager);
1709
+ }
1710
+ };
1711
+ }
1712
+ function createDefaultTokenManager(options) {
1713
+ const secret = options.jwtSecret ?? readEnvSecret();
1714
+ if (!secret && isProduction()) {
1715
+ throw new Error(
1716
+ "createKoraAuthServer requires jwtSecret in production. Set KORA_AUTH_SECRET or pass jwtSecret."
1717
+ );
1238
1718
  }
1239
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1719
+ return new TokenManager({
1720
+ secret: secret ?? TokenManager.generateSecret(),
1721
+ revocationStore: new InMemoryTokenRevocationStore(),
1722
+ ...options.tokenManagerOptions
1723
+ });
1724
+ }
1725
+ function createOAuthRuntime(config) {
1726
+ return {
1727
+ manager: new OAuthManager(config),
1728
+ linkedIdentityStore: config.linkedIdentityStore ?? new InMemoryLinkedIdentityStore(),
1729
+ createNewUsers: config.createNewUsers ?? true,
1730
+ autoLinkVerifiedEmail: config.autoLinkVerifiedEmail ?? false,
1731
+ allowUnlinkLastIdentity: config.allowUnlinkLastIdentity ?? false
1732
+ };
1733
+ }
1734
+ async function handleAuthRequest(routes, pathPrefix, request, oauth, userStore, tokenManager) {
1735
+ const path = normalizePath(request.path);
1736
+ const relativePath = path === pathPrefix ? "/" : path.slice(pathPrefix.length);
1737
+ const method = request.method.toUpperCase();
1738
+ const body = isRecord(request.body) ? request.body : {};
1739
+ const token = extractBearerToken(request.headers);
1740
+ if (path !== pathPrefix && !path.startsWith(`${pathPrefix}/`)) {
1741
+ return notFound();
1742
+ }
1743
+ if (relativePath.startsWith("/oauth/")) {
1744
+ return handleOAuthRequest({
1745
+ oauth,
1746
+ userStore,
1747
+ tokenManager,
1748
+ relativePath,
1749
+ method,
1750
+ body,
1751
+ query: request.query,
1752
+ token
1753
+ });
1754
+ }
1755
+ if (method === "POST" && relativePath === "/signup") {
1756
+ return routes.handleSignUp(body, request.ip);
1757
+ }
1758
+ if (method === "POST" && relativePath === "/signin") {
1759
+ return routes.handleSignIn(body, request.ip);
1760
+ }
1761
+ if (method === "POST" && relativePath === "/refresh") {
1762
+ return routes.handleRefresh(body);
1763
+ }
1764
+ if (method === "POST" && relativePath === "/signout") {
1765
+ return routes.handleSignOut(token, body);
1766
+ }
1767
+ if (method === "GET" && relativePath === "/me") {
1768
+ return routes.handleGetMe(token);
1769
+ }
1770
+ if (method === "GET" && relativePath === "/devices") {
1771
+ return routes.handleListDevices(token);
1772
+ }
1773
+ if (method === "POST" && relativePath === "/device/register") {
1774
+ return routes.handleDeviceRegister(token, body);
1775
+ }
1776
+ if (method === "POST" && relativePath === "/device/challenge") {
1777
+ const deviceId = typeof body.deviceId === "string" ? body.deviceId : "";
1778
+ return routes.handleDeviceChallenge(token, deviceId);
1779
+ }
1780
+ if (method === "POST" && relativePath === "/device/verify") {
1781
+ return routes.handleDeviceVerify(body);
1782
+ }
1783
+ if (method === "DELETE" && relativePath.startsWith("/device/")) {
1784
+ return routes.handleRevokeDevice(token, relativePath.slice("/device/".length));
1785
+ }
1786
+ return notFound();
1787
+ }
1788
+ async function handleOAuthRequest(params) {
1789
+ const { oauth, userStore, tokenManager, relativePath, method, body, query, token } = params;
1790
+ if (!oauth) {
1791
+ return notFound();
1792
+ }
1793
+ try {
1794
+ if (method === "GET" && relativePath === "/oauth/links") {
1795
+ const authUser = await requireAuthUser(tokenManager, userStore, token);
1796
+ if ("status" in authUser) return authUser;
1797
+ const identities = await oauth.linkedIdentityStore.findByUser(authUser.id);
1798
+ return { status: 200, body: { data: identities } };
1799
+ }
1800
+ const match = /^\/oauth\/([^/]+)(?:\/(callback|link))?$/.exec(relativePath);
1801
+ if (!match) {
1802
+ return notFound();
1803
+ }
1804
+ const provider = decodeURIComponent(match[1]);
1805
+ const action = match[2];
1806
+ if (method === "GET" && !action) {
1807
+ const { url, state } = await oauth.manager.getAuthorizationUrl(
1808
+ provider,
1809
+ metadataFromQuery(query)
1810
+ );
1811
+ return { status: 200, body: { data: { url, state } } };
1812
+ }
1813
+ if ((method === "GET" || method === "POST") && action === "callback") {
1814
+ const code = readString(method === "GET" ? queryValue(query, "code") : body.code);
1815
+ const state = readString(method === "GET" ? queryValue(query, "state") : body.state);
1816
+ if (!code || !state) {
1817
+ return { status: 400, body: { error: "OAuth callback requires code and state." } };
1818
+ }
1819
+ return completeOAuthSignIn({
1820
+ oauth,
1821
+ userStore,
1822
+ tokenManager,
1823
+ provider,
1824
+ code,
1825
+ state,
1826
+ deviceId: readString(body.deviceId),
1827
+ devicePublicKey: readString(body.devicePublicKey)
1828
+ });
1829
+ }
1830
+ if (method === "POST" && action === "link") {
1831
+ const authUser = await requireAuthUser(tokenManager, userStore, token);
1832
+ if ("status" in authUser) return authUser;
1833
+ const code = readString(body.code);
1834
+ const state = readString(body.state);
1835
+ if (!code || !state) {
1836
+ return { status: 400, body: { error: "OAuth linking requires code and state." } };
1837
+ }
1838
+ return linkOAuthIdentity(oauth, authUser.id, provider, code, state);
1839
+ }
1840
+ if (method === "DELETE" && action === "link") {
1841
+ const authUser = await requireAuthUser(tokenManager, userStore, token);
1842
+ if ("status" in authUser) return authUser;
1843
+ const identities = await oauth.linkedIdentityStore.findByUser(authUser.id);
1844
+ if (!oauth.allowUnlinkLastIdentity && identities.length <= 1) {
1845
+ return {
1846
+ status: 409,
1847
+ body: {
1848
+ error: "Cannot unlink the last OAuth identity unless allowUnlinkLastIdentity is enabled."
1849
+ }
1850
+ };
1851
+ }
1852
+ await oauth.linkedIdentityStore.delete(authUser.id, provider);
1853
+ return { status: 200, body: { data: { ok: true } } };
1854
+ }
1855
+ } catch (error) {
1856
+ return oauthErrorResponse(error);
1857
+ }
1858
+ return notFound();
1859
+ }
1860
+ async function completeOAuthSignIn(params) {
1861
+ const { oauth, userStore, tokenManager, provider, code, state, deviceId, devicePublicKey } = params;
1862
+ const { userInfo, stateMetadata } = await oauth.manager.handleCallback(provider, code, state);
1863
+ const linkedIdentity = await oauth.linkedIdentityStore.findByProvider(
1864
+ userInfo.provider,
1865
+ userInfo.providerId
1866
+ );
1867
+ let user;
1868
+ let identity;
1869
+ if (linkedIdentity) {
1870
+ const storedUser = await userStore.findById(linkedIdentity.userId);
1871
+ if (!storedUser) {
1872
+ return { status: 409, body: { error: "Linked OAuth account has no matching user." } };
1873
+ }
1874
+ user = toAuthUser2(storedUser);
1875
+ identity = linkedIdentity;
1876
+ } else {
1877
+ const resolved = await resolveOAuthUser(userStore, oauth, userInfo);
1878
+ if ("status" in resolved) return resolved;
1879
+ user = resolved;
1880
+ identity = await oauth.linkedIdentityStore.create({
1881
+ userId: user.id,
1882
+ provider: userInfo.provider,
1883
+ providerUserId: userInfo.providerId,
1884
+ email: userInfo.email
1885
+ });
1886
+ }
1887
+ const resolvedDeviceId = deviceId ?? readString(stateMetadata?.deviceId) ?? `device-${user.id}`;
1888
+ await userStore.registerDevice({
1889
+ id: resolvedDeviceId,
1890
+ userId: user.id,
1891
+ publicKey: devicePublicKey ?? readString(stateMetadata?.devicePublicKey) ?? "",
1892
+ name: deviceId ? "Device" : "Browser"
1893
+ });
1894
+ const tokens = tokenManager.issueTokens(user.id, resolvedDeviceId);
1895
+ return { status: 200, body: { data: { user, tokens, identity } } };
1896
+ }
1897
+ async function resolveOAuthUser(userStore, oauth, userInfo) {
1898
+ if (!userInfo.email) {
1899
+ return { status: 400, body: { error: "OAuth provider did not return an email address." } };
1900
+ }
1901
+ const existingUser = await userStore.findByEmail(userInfo.email);
1902
+ if (existingUser) {
1903
+ if (oauth.autoLinkVerifiedEmail && userInfo.emailVerified) {
1904
+ return toAuthUser2(existingUser);
1905
+ }
1906
+ return {
1907
+ status: 409,
1908
+ body: { error: "OAuth account is not linked. Sign in and link this provider first." }
1909
+ };
1910
+ }
1911
+ if (!oauth.createNewUsers) {
1912
+ return { status: 403, body: { error: "OAuth sign-up is disabled for this application." } };
1913
+ }
1914
+ const credential = await hashPassword(randomUUID4());
1915
+ const user = await userStore.createUser({
1916
+ email: userInfo.email,
1917
+ passwordHash: credential.hash,
1918
+ salt: credential.salt,
1919
+ name: userInfo.name ?? userInfo.email.split("@")[0] ?? userInfo.email
1920
+ });
1921
+ if (userInfo.emailVerified) {
1922
+ await userStore.setEmailVerified(user.id, true);
1923
+ return { ...user, emailVerified: true };
1924
+ }
1925
+ return user;
1926
+ }
1927
+ async function linkOAuthIdentity(oauth, userId, provider, code, state) {
1928
+ const { userInfo } = await oauth.manager.handleCallback(provider, code, state);
1929
+ const existing = await oauth.linkedIdentityStore.findByProvider(
1930
+ userInfo.provider,
1931
+ userInfo.providerId
1932
+ );
1933
+ if (existing && existing.userId !== userId) {
1934
+ return { status: 409, body: { error: "This OAuth account is already linked to another user." } };
1935
+ }
1936
+ if (existing) {
1937
+ return { status: 200, body: { data: existing } };
1938
+ }
1939
+ const identity = await oauth.linkedIdentityStore.create({
1940
+ userId,
1941
+ provider: userInfo.provider,
1942
+ providerUserId: userInfo.providerId,
1943
+ email: userInfo.email
1944
+ });
1945
+ return { status: 201, body: { data: identity } };
1946
+ }
1947
+ async function requireAuthUser(tokenManager, userStore, token) {
1948
+ if (!token) {
1949
+ return { status: 401, body: { error: "Authorization token required." } };
1950
+ }
1951
+ const payload = await tokenManager.validateToken(token);
1952
+ if (!payload || payload.type !== "access") {
1953
+ return { status: 401, body: { error: "Invalid or expired token." } };
1954
+ }
1955
+ const user = await userStore.findById(payload.sub);
1956
+ if (!user) {
1957
+ return { status: 401, body: { error: "User not found." } };
1958
+ }
1959
+ return toAuthUser2(user);
1960
+ }
1961
+ function extractBearerToken(headers) {
1962
+ const authorization = headers?.authorization ?? headers?.Authorization;
1963
+ const value = Array.isArray(authorization) ? authorization[0] : authorization;
1964
+ if (!value?.startsWith("Bearer ")) {
1965
+ return "";
1966
+ }
1967
+ return value.slice("Bearer ".length).trim();
1968
+ }
1969
+ function normalizePath(path) {
1970
+ const withoutQuery = path.split("?")[0] || "/";
1971
+ const normalized = withoutQuery.startsWith("/") ? withoutQuery : `/${withoutQuery}`;
1972
+ return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
1973
+ }
1974
+ function metadataFromQuery(query) {
1975
+ if (!query) return void 0;
1976
+ const metadata = {};
1977
+ for (const [key, value] of Object.entries(query)) {
1978
+ if (key === "code" || key === "state") continue;
1979
+ if (value !== void 0) {
1980
+ metadata[key] = value;
1981
+ }
1982
+ }
1983
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
1984
+ }
1985
+ function queryValue(query, key) {
1986
+ return query?.[key];
1987
+ }
1988
+ function readString(value) {
1989
+ if (typeof value === "string" && value.length > 0) return value;
1990
+ if (Array.isArray(value) && typeof value[0] === "string" && value[0].length > 0) {
1991
+ return value[0];
1992
+ }
1993
+ return void 0;
1994
+ }
1995
+ function oauthErrorResponse(error) {
1996
+ if (error instanceof DuplicateLinkedIdentityError) {
1997
+ return { status: 409, body: { error: error.message } };
1998
+ }
1999
+ if (error instanceof OAuthError) {
2000
+ const status = error.code === "OAUTH_PROVIDER_NOT_FOUND" ? 404 : 400;
2001
+ return { status, body: { error: error.message } };
2002
+ }
2003
+ throw error;
2004
+ }
2005
+ function toAuthUser2(user) {
2006
+ return {
2007
+ id: user.id,
2008
+ email: user.email,
2009
+ name: user.name,
2010
+ emailVerified: user.emailVerified,
2011
+ createdAt: user.createdAt
2012
+ };
2013
+ }
2014
+ function isRecord(value) {
2015
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2016
+ }
2017
+ function notFound() {
2018
+ return { status: 404, body: { error: "Not found" } };
2019
+ }
2020
+ function readEnvSecret() {
2021
+ return typeof process !== "undefined" ? process.env.KORA_AUTH_SECRET : void 0;
2022
+ }
2023
+ function isProduction() {
2024
+ return typeof process !== "undefined" && process.env.NODE_ENV === "production";
1240
2025
  }
1241
2026
 
1242
- // src/provider/built-in/email-verification.ts
1243
- import { KoraError as KoraError3 } from "@korajs/core";
1244
- var EmailVerificationError = class extends KoraError3 {
2027
+ // src/provider/built-in/password-reset.ts
2028
+ import { KoraError as KoraError5 } from "@korajs/core";
2029
+ var PasswordResetError = class extends KoraError5 {
1245
2030
  constructor(message, code, context) {
1246
2031
  super(message, code, context);
1247
- this.name = "EmailVerificationError";
2032
+ this.name = "PasswordResetError";
1248
2033
  }
1249
2034
  };
1250
- var VerificationTokenExpiredError = class extends EmailVerificationError {
2035
+ var ResetTokenExpiredError = class extends PasswordResetError {
1251
2036
  constructor() {
1252
- super("Email verification token has expired.", "VERIFICATION_TOKEN_EXPIRED");
2037
+ super("Password reset token has expired.", "RESET_TOKEN_EXPIRED");
1253
2038
  }
1254
2039
  };
1255
- var VerificationTokenNotFoundError = class extends EmailVerificationError {
2040
+ var ResetTokenNotFoundError = class extends PasswordResetError {
1256
2041
  constructor() {
1257
- super("Email verification token not found or already used.", "VERIFICATION_TOKEN_NOT_FOUND");
2042
+ super("Password reset token not found or already used.", "RESET_TOKEN_NOT_FOUND");
1258
2043
  }
1259
2044
  };
1260
- var InMemoryEmailVerificationStore = class {
2045
+ var ResetRateLimitedError = class extends PasswordResetError {
2046
+ constructor() {
2047
+ super("Too many password reset requests. Please try again later.", "RESET_RATE_LIMITED");
2048
+ }
2049
+ };
2050
+ var InMemoryPasswordResetStore = class {
1261
2051
  tokens = /* @__PURE__ */ new Map();
1262
2052
  async store(token) {
1263
2053
  this.tokens.set(token.token, token);
@@ -1271,11 +2061,11 @@ var InMemoryEmailVerificationStore = class {
1271
2061
  this.tokens.set(token, { ...entry, consumed: true });
1272
2062
  }
1273
2063
  }
1274
- async countActiveForUser(userId) {
2064
+ async countActiveForEmail(email) {
1275
2065
  const now = Date.now();
1276
2066
  let count = 0;
1277
2067
  for (const token of this.tokens.values()) {
1278
- if (token.userId === userId && !token.consumed && now < token.expiresAt) {
2068
+ if (token.email === email && !token.consumed && now < token.expiresAt) {
1279
2069
  count++;
1280
2070
  }
1281
2071
  }
@@ -1293,97 +2083,130 @@ var InMemoryEmailVerificationStore = class {
1293
2083
  return count;
1294
2084
  }
1295
2085
  };
1296
- var DEFAULT_TOKEN_TTL_MS2 = 24 * 60 * 60 * 1e3;
1297
- var DEFAULT_MAX_REQUESTS2 = 3;
1298
- var EmailVerificationManager = class {
2086
+ var DEFAULT_TOKEN_TTL_MS = 60 * 60 * 1e3;
2087
+ var DEFAULT_MAX_REQUESTS = 3;
2088
+ var MIN_PASSWORD_LENGTH2 = 8;
2089
+ var MAX_PASSWORD_LENGTH2 = 128;
2090
+ var PasswordResetManager = class {
1299
2091
  userStore;
1300
- verificationStore;
2092
+ resetStore;
1301
2093
  tokenTtlMs;
1302
- maxRequestsPerUser;
1303
- onVerificationRequired;
2094
+ maxRequestsPerEmail;
2095
+ onResetRequested;
1304
2096
  constructor(config) {
1305
2097
  this.userStore = config.userStore;
1306
- this.verificationStore = config.verificationStore ?? new InMemoryEmailVerificationStore();
1307
- this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS2;
1308
- this.maxRequestsPerUser = config.maxRequestsPerUser ?? DEFAULT_MAX_REQUESTS2;
1309
- this.onVerificationRequired = config.onVerificationRequired;
2098
+ this.resetStore = config.resetStore ?? new InMemoryPasswordResetStore();
2099
+ this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
2100
+ this.maxRequestsPerEmail = config.maxRequestsPerEmail ?? DEFAULT_MAX_REQUESTS;
2101
+ this.onResetRequested = config.onResetRequested;
1310
2102
  }
1311
2103
  /**
1312
- * Send a verification email for a user.
1313
- * Rate-limited to prevent abuse.
2104
+ * Request a password reset for an email.
2105
+ * Always returns success to prevent email enumeration.
2106
+ *
2107
+ * If a callback is configured, invokes it with the token.
2108
+ * In development mode (no callback), returns the token in the response.
1314
2109
  */
1315
- async sendVerification(userId, email) {
2110
+ async requestReset(email) {
1316
2111
  const normalizedEmail = email.toLowerCase().trim();
1317
- const activeCount = await this.verificationStore.countActiveForUser(userId);
1318
- if (activeCount >= this.maxRequestsPerUser) {
1319
- return {
1320
- status: 429,
1321
- body: { error: "Too many verification requests. Please try again later." }
1322
- };
2112
+ const successResponse = {
2113
+ status: 200,
2114
+ body: {
2115
+ data: {
2116
+ message: "If an account with that email exists, a password reset link has been sent."
2117
+ }
2118
+ }
2119
+ };
2120
+ const user = await this.userStore.findByEmail(normalizedEmail);
2121
+ if (!user) {
2122
+ return successResponse;
1323
2123
  }
1324
- const token = generateSecureToken2();
2124
+ const activeCount = await this.resetStore.countActiveForEmail(normalizedEmail);
2125
+ if (activeCount >= this.maxRequestsPerEmail) {
2126
+ return successResponse;
2127
+ }
2128
+ const token = generateSecureToken();
1325
2129
  const now = Date.now();
1326
- const verificationToken = {
2130
+ const resetToken = {
1327
2131
  token,
1328
- userId,
2132
+ userId: user.id,
1329
2133
  email: normalizedEmail,
1330
2134
  createdAt: now,
1331
2135
  expiresAt: now + this.tokenTtlMs,
1332
2136
  consumed: false
1333
2137
  };
1334
- await this.verificationStore.store(verificationToken);
1335
- if (this.onVerificationRequired) {
2138
+ await this.resetStore.store(resetToken);
2139
+ if (this.onResetRequested) {
1336
2140
  try {
1337
- await this.onVerificationRequired(normalizedEmail, token, verificationToken.expiresAt);
2141
+ await this.onResetRequested(normalizedEmail, token, resetToken.expiresAt);
1338
2142
  } catch {
1339
2143
  }
1340
2144
  }
1341
- const responseData = {
1342
- message: "Verification email sent."
1343
- };
1344
- if (!this.onVerificationRequired) {
1345
- responseData.token = token;
2145
+ if (!this.onResetRequested) {
2146
+ successResponse.body = { data: { message: "Password reset token generated.", token } };
1346
2147
  }
1347
- return { status: 200, body: { data: responseData } };
2148
+ return successResponse;
1348
2149
  }
1349
2150
  /**
1350
- * Verify an email using a verification token.
2151
+ * Consume a reset token and set a new password.
1351
2152
  */
1352
- async verifyEmail(token) {
1353
- const verificationToken = await this.verificationStore.get(token);
1354
- if (!verificationToken || verificationToken.consumed) {
1355
- return { status: 404, body: { error: "Verification token not found or already used." } };
2153
+ async resetPassword(token, newPassword) {
2154
+ if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
2155
+ return {
2156
+ status: 400,
2157
+ body: { error: `Password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
2158
+ };
1356
2159
  }
1357
- if (Date.now() > verificationToken.expiresAt) {
1358
- await this.verificationStore.consume(token);
1359
- return { status: 410, body: { error: "Verification token has expired." } };
2160
+ if (newPassword.length > MAX_PASSWORD_LENGTH2) {
2161
+ return {
2162
+ status: 400,
2163
+ body: { error: `Password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
2164
+ };
1360
2165
  }
1361
- await this.verificationStore.consume(token);
1362
- await this.userStore.setEmailVerified(verificationToken.userId, true);
1363
- return {
1364
- status: 200,
1365
- body: {
1366
- data: {
1367
- message: "Email verified successfully.",
1368
- userId: verificationToken.userId,
1369
- email: verificationToken.email
1370
- }
1371
- }
1372
- };
2166
+ const resetToken = await this.resetStore.get(token);
2167
+ if (!resetToken || resetToken.consumed) {
2168
+ return { status: 404, body: { error: "Password reset token not found or already used." } };
2169
+ }
2170
+ if (Date.now() > resetToken.expiresAt) {
2171
+ await this.resetStore.consume(token);
2172
+ return { status: 410, body: { error: "Password reset token has expired." } };
2173
+ }
2174
+ await this.resetStore.consume(token);
2175
+ const hashed = await hashPassword(newPassword);
2176
+ await this.userStore.updatePassword(resetToken.userId, hashed.hash, hashed.salt);
2177
+ return { status: 200, body: { data: { message: "Password has been reset successfully." } } };
1373
2178
  }
1374
2179
  /**
1375
- * Resend verification email for a user.
1376
- * Delegates to sendVerification with rate limiting.
2180
+ * Change password for an authenticated user (requires current password verification).
1377
2181
  */
1378
- async resendVerification(userId) {
2182
+ async changePassword(userId, currentPassword, newPassword) {
2183
+ if (typeof newPassword !== "string" || newPassword.length < MIN_PASSWORD_LENGTH2) {
2184
+ return {
2185
+ status: 400,
2186
+ body: { error: `New password must be at least ${MIN_PASSWORD_LENGTH2} characters.` }
2187
+ };
2188
+ }
2189
+ if (newPassword.length > MAX_PASSWORD_LENGTH2) {
2190
+ return {
2191
+ status: 400,
2192
+ body: { error: `New password must be at most ${MAX_PASSWORD_LENGTH2} characters.` }
2193
+ };
2194
+ }
1379
2195
  const user = await this.userStore.findById(userId);
1380
2196
  if (!user) {
1381
2197
  return { status: 404, body: { error: "User not found." } };
1382
2198
  }
1383
- return this.sendVerification(userId, user.email);
2199
+ const { verifyPassword: verifyPassword2 } = await import("./password-hash-QRBG6BNJ.js");
2200
+ const isValid = await verifyPassword2(currentPassword, user.passwordHash, user.salt);
2201
+ if (!isValid) {
2202
+ return { status: 401, body: { error: "Current password is incorrect." } };
2203
+ }
2204
+ const hashed = await hashPassword(newPassword);
2205
+ await this.userStore.updatePassword(userId, hashed.hash, hashed.salt);
2206
+ return { status: 200, body: { data: { message: "Password changed successfully." } } };
1384
2207
  }
1385
2208
  };
1386
- function generateSecureToken2() {
2209
+ function generateSecureToken() {
1387
2210
  const bytes = new Uint8Array(32);
1388
2211
  globalThis.crypto.getRandomValues(bytes);
1389
2212
  let binary = "";
@@ -1393,244 +2216,162 @@ function generateSecureToken2() {
1393
2216
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1394
2217
  }
1395
2218
 
1396
- // src/provider/built-in/user-store.ts
1397
- import { randomUUID as randomUUID2 } from "crypto";
1398
- import { KoraError as KoraError4 } from "@korajs/core";
1399
- var DuplicateEmailError = class extends KoraError4 {
2219
+ // src/provider/built-in/email-verification.ts
2220
+ import { KoraError as KoraError6 } from "@korajs/core";
2221
+ var EmailVerificationError = class extends KoraError6 {
2222
+ constructor(message, code, context) {
2223
+ super(message, code, context);
2224
+ this.name = "EmailVerificationError";
2225
+ }
2226
+ };
2227
+ var VerificationTokenExpiredError = class extends EmailVerificationError {
1400
2228
  constructor() {
1401
- super("A user with this email already exists.", "DUPLICATE_EMAIL");
1402
- this.name = "DuplicateEmailError";
2229
+ super("Email verification token has expired.", "VERIFICATION_TOKEN_EXPIRED");
1403
2230
  }
1404
2231
  };
1405
- var InMemoryUserStore = class {
1406
- /** Users indexed by ID */
1407
- usersById = /* @__PURE__ */ new Map();
1408
- /** Users indexed by email (lowercase) for fast lookup */
1409
- usersByEmail = /* @__PURE__ */ new Map();
1410
- /** Devices indexed by device ID */
1411
- devicesById = /* @__PURE__ */ new Map();
1412
- /** Device IDs indexed by user ID for fast listing */
1413
- devicesByUserId = /* @__PURE__ */ new Map();
1414
- /**
1415
- * Create a new user account.
1416
- *
1417
- * @param params - User creation parameters
1418
- * @param params.email - The user's email address (must be unique, case-insensitive)
1419
- * @param params.passwordHash - Hex-encoded PBKDF2 derived key
1420
- * @param params.salt - Hex-encoded salt used during hashing
1421
- * @param params.name - The user's display name
1422
- * @returns The created user (without sensitive credential fields)
1423
- * @throws {DuplicateEmailError} If a user with the same email already exists
1424
- */
1425
- async createUser(params) {
1426
- const normalizedEmail = params.email.toLowerCase();
1427
- if (this.usersByEmail.has(normalizedEmail)) {
1428
- throw new DuplicateEmailError();
1429
- }
1430
- const now = Date.now();
1431
- const id = randomUUID2();
1432
- const storedUser = {
1433
- id,
1434
- email: normalizedEmail,
1435
- name: params.name,
1436
- emailVerified: false,
1437
- createdAt: now,
1438
- passwordHash: params.passwordHash,
1439
- salt: params.salt
1440
- };
1441
- this.usersById.set(id, storedUser);
1442
- this.usersByEmail.set(normalizedEmail, storedUser);
1443
- return toAuthUser(storedUser);
2232
+ var VerificationTokenNotFoundError = class extends EmailVerificationError {
2233
+ constructor() {
2234
+ super("Email verification token not found or already used.", "VERIFICATION_TOKEN_NOT_FOUND");
1444
2235
  }
1445
- /**
1446
- * Find a user by email address.
1447
- *
1448
- * @param email - The email to search for (case-insensitive)
1449
- * @returns The stored user record including credentials, or null if not found
1450
- */
1451
- async findByEmail(email) {
1452
- return this.usersByEmail.get(email.toLowerCase()) ?? null;
2236
+ };
2237
+ var InMemoryEmailVerificationStore = class {
2238
+ tokens = /* @__PURE__ */ new Map();
2239
+ async store(token) {
2240
+ this.tokens.set(token.token, token);
1453
2241
  }
1454
- /**
1455
- * Find a user by ID.
1456
- *
1457
- * @param id - The user ID to search for
1458
- * @returns The stored user record including credentials, or null if not found
1459
- */
1460
- async findById(id) {
1461
- return this.usersById.get(id) ?? null;
2242
+ async get(token) {
2243
+ return this.tokens.get(token) ?? null;
1462
2244
  }
1463
- /**
1464
- * Register a device for a user.
1465
- *
1466
- * If a device with the same ID already exists and is not revoked, it is
1467
- * returned as-is (idempotent registration). If it was previously revoked,
1468
- * it is re-activated with updated details.
1469
- *
1470
- * @param params - Device registration parameters
1471
- * @param params.id - Unique device identifier
1472
- * @param params.userId - ID of the user who owns the device
1473
- * @param params.publicKey - Base64url-encoded device public key or thumbprint
1474
- * @param params.name - Human-readable device name
1475
- * @returns The registered device record
1476
- */
1477
- async registerDevice(params) {
1478
- const existing = this.devicesById.get(params.id);
1479
- if (existing !== void 0 && !existing.revoked) {
1480
- return existing;
1481
- }
1482
- const now = Date.now();
1483
- const device = {
1484
- id: params.id,
1485
- userId: params.userId,
1486
- publicKey: params.publicKey,
1487
- name: params.name,
1488
- revoked: false,
1489
- createdAt: now,
1490
- lastSeenAt: now
1491
- };
1492
- this.devicesById.set(params.id, device);
1493
- let userDevices = this.devicesByUserId.get(params.userId);
1494
- if (userDevices === void 0) {
1495
- userDevices = /* @__PURE__ */ new Set();
1496
- this.devicesByUserId.set(params.userId, userDevices);
2245
+ async consume(token) {
2246
+ const entry = this.tokens.get(token);
2247
+ if (entry) {
2248
+ this.tokens.set(token, { ...entry, consumed: true });
1497
2249
  }
1498
- userDevices.add(params.id);
1499
- return device;
1500
2250
  }
1501
- /**
1502
- * Find a device by its ID.
1503
- *
1504
- * @param deviceId - The device ID to search for
1505
- * @returns The device record, or null if not found
1506
- */
1507
- async findDevice(deviceId) {
1508
- return this.devicesById.get(deviceId) ?? null;
1509
- }
1510
- /**
1511
- * List all devices registered for a user.
1512
- *
1513
- * @param userId - The user ID whose devices to list
1514
- * @returns Array of device records (includes revoked devices)
1515
- */
1516
- async listDevices(userId) {
1517
- const deviceIds = this.devicesByUserId.get(userId);
1518
- if (deviceIds === void 0) {
1519
- return [];
1520
- }
1521
- const devices = [];
1522
- for (const deviceId of deviceIds) {
1523
- const device = this.devicesById.get(deviceId);
1524
- if (device !== void 0) {
1525
- devices.push(device);
2251
+ async countActiveForUser(userId) {
2252
+ const now = Date.now();
2253
+ let count = 0;
2254
+ for (const token of this.tokens.values()) {
2255
+ if (token.userId === userId && !token.consumed && now < token.expiresAt) {
2256
+ count++;
1526
2257
  }
1527
2258
  }
1528
- return devices;
2259
+ return count;
1529
2260
  }
1530
- /**
1531
- * Revoke a device, preventing it from being used for authentication.
1532
- *
1533
- * This is a soft revoke — the device record remains but is marked as revoked.
1534
- * If the device does not exist, this is a no-op.
1535
- *
1536
- * @param deviceId - The ID of the device to revoke
1537
- */
1538
- async revokeDevice(deviceId) {
1539
- const device = this.devicesById.get(deviceId);
1540
- if (device !== void 0) {
1541
- device.revoked = true;
2261
+ async cleanExpired() {
2262
+ const now = Date.now();
2263
+ let count = 0;
2264
+ for (const [key, token] of this.tokens) {
2265
+ if (now > token.expiresAt) {
2266
+ this.tokens.delete(key);
2267
+ count++;
2268
+ }
1542
2269
  }
2270
+ return count;
1543
2271
  }
1544
- /**
1545
- * Set a user's email verification status.
1546
- *
1547
- * @param userId - The user whose email to verify
1548
- * @param verified - Whether the email is verified
1549
- */
1550
- async setEmailVerified(userId, verified) {
1551
- const user = this.usersById.get(userId);
1552
- if (!user) return;
1553
- const updated = { ...user, emailVerified: verified };
1554
- this.usersById.set(userId, updated);
1555
- this.usersByEmail.set(user.email, updated);
1556
- }
1557
- /**
1558
- * Update a user's password hash and salt.
1559
- *
1560
- * @param userId - The user whose password to update
1561
- * @param passwordHash - New hex-encoded PBKDF2 derived key
1562
- * @param salt - New hex-encoded salt
1563
- */
1564
- async updatePassword(userId, passwordHash, salt) {
1565
- const user = this.usersById.get(userId);
1566
- if (!user) return;
1567
- const updated = { ...user, passwordHash, salt };
1568
- this.usersById.set(userId, updated);
1569
- this.usersByEmail.set(user.email, updated);
1570
- }
1571
- /**
1572
- * List all users. For admin/development use.
1573
- */
1574
- async listAll() {
1575
- return [...this.usersById.values()];
2272
+ };
2273
+ var DEFAULT_TOKEN_TTL_MS2 = 24 * 60 * 60 * 1e3;
2274
+ var DEFAULT_MAX_REQUESTS2 = 3;
2275
+ var EmailVerificationManager = class {
2276
+ userStore;
2277
+ verificationStore;
2278
+ tokenTtlMs;
2279
+ maxRequestsPerUser;
2280
+ onVerificationRequired;
2281
+ constructor(config) {
2282
+ this.userStore = config.userStore;
2283
+ this.verificationStore = config.verificationStore ?? new InMemoryEmailVerificationStore();
2284
+ this.tokenTtlMs = config.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS2;
2285
+ this.maxRequestsPerUser = config.maxRequestsPerUser ?? DEFAULT_MAX_REQUESTS2;
2286
+ this.onVerificationRequired = config.onVerificationRequired;
1576
2287
  }
1577
2288
  /**
1578
- * Update a stored user record.
2289
+ * Send a verification email for a user.
2290
+ * Rate-limited to prevent abuse.
1579
2291
  */
1580
- async update(user) {
1581
- const existing = this.usersById.get(user.id);
1582
- if (!existing) return;
1583
- if (existing.email !== user.email) {
1584
- this.usersByEmail.delete(existing.email);
1585
- this.usersByEmail.set(user.email, user);
1586
- } else {
1587
- this.usersByEmail.set(user.email, user);
2292
+ async sendVerification(userId, email) {
2293
+ const normalizedEmail = email.toLowerCase().trim();
2294
+ const activeCount = await this.verificationStore.countActiveForUser(userId);
2295
+ if (activeCount >= this.maxRequestsPerUser) {
2296
+ return {
2297
+ status: 429,
2298
+ body: { error: "Too many verification requests. Please try again later." }
2299
+ };
1588
2300
  }
1589
- this.usersById.set(user.id, user);
2301
+ const token = generateSecureToken2();
2302
+ const now = Date.now();
2303
+ const verificationToken = {
2304
+ token,
2305
+ userId,
2306
+ email: normalizedEmail,
2307
+ createdAt: now,
2308
+ expiresAt: now + this.tokenTtlMs,
2309
+ consumed: false
2310
+ };
2311
+ await this.verificationStore.store(verificationToken);
2312
+ if (this.onVerificationRequired) {
2313
+ try {
2314
+ await this.onVerificationRequired(normalizedEmail, token, verificationToken.expiresAt);
2315
+ } catch {
2316
+ }
2317
+ }
2318
+ const responseData = {
2319
+ message: "Verification email sent."
2320
+ };
2321
+ if (!this.onVerificationRequired) {
2322
+ responseData.token = token;
2323
+ }
2324
+ return { status: 200, body: { data: responseData } };
1590
2325
  }
1591
2326
  /**
1592
- * Delete a user and all associated devices.
2327
+ * Verify an email using a verification token.
1593
2328
  */
1594
- async delete(userId) {
1595
- const user = this.usersById.get(userId);
1596
- if (!user) return;
1597
- this.usersById.delete(userId);
1598
- this.usersByEmail.delete(user.email);
1599
- const deviceIds = this.devicesByUserId.get(userId);
1600
- if (deviceIds) {
1601
- for (const deviceId of deviceIds) {
1602
- this.devicesById.delete(deviceId);
1603
- }
1604
- this.devicesByUserId.delete(userId);
2329
+ async verifyEmail(token) {
2330
+ const verificationToken = await this.verificationStore.get(token);
2331
+ if (!verificationToken || verificationToken.consumed) {
2332
+ return { status: 404, body: { error: "Verification token not found or already used." } };
2333
+ }
2334
+ if (Date.now() > verificationToken.expiresAt) {
2335
+ await this.verificationStore.consume(token);
2336
+ return { status: 410, body: { error: "Verification token has expired." } };
1605
2337
  }
2338
+ await this.verificationStore.consume(token);
2339
+ await this.userStore.setEmailVerified(verificationToken.userId, true);
2340
+ return {
2341
+ status: 200,
2342
+ body: {
2343
+ data: {
2344
+ message: "Email verified successfully.",
2345
+ userId: verificationToken.userId,
2346
+ email: verificationToken.email
2347
+ }
2348
+ }
2349
+ };
1606
2350
  }
1607
2351
  /**
1608
- * Update the last-seen timestamp for a device.
1609
- *
1610
- * Called when a device authenticates or syncs to track activity.
1611
- * If the device does not exist, this is a no-op.
1612
- *
1613
- * @param deviceId - The ID of the device to update
2352
+ * Resend verification email for a user.
2353
+ * Delegates to sendVerification with rate limiting.
1614
2354
  */
1615
- async touchDevice(deviceId) {
1616
- const device = this.devicesById.get(deviceId);
1617
- if (device !== void 0) {
1618
- device.lastSeenAt = Date.now();
2355
+ async resendVerification(userId) {
2356
+ const user = await this.userStore.findById(userId);
2357
+ if (!user) {
2358
+ return { status: 404, body: { error: "User not found." } };
1619
2359
  }
2360
+ return this.sendVerification(userId, user.email);
1620
2361
  }
1621
2362
  };
1622
- function toAuthUser(stored) {
1623
- return {
1624
- id: stored.id,
1625
- email: stored.email,
1626
- name: stored.name,
1627
- emailVerified: stored.emailVerified,
1628
- createdAt: stored.createdAt
1629
- };
2363
+ function generateSecureToken2() {
2364
+ const bytes = new Uint8Array(32);
2365
+ globalThis.crypto.getRandomValues(bytes);
2366
+ let binary = "";
2367
+ for (let i = 0; i < bytes.length; i++) {
2368
+ binary += String.fromCharCode(bytes[i]);
2369
+ }
2370
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1630
2371
  }
1631
2372
 
1632
2373
  // src/provider/built-in/sqlite-user-store.ts
1633
- import { randomUUID as randomUUID3 } from "crypto";
2374
+ import { randomUUID as randomUUID5 } from "crypto";
1634
2375
  var SqliteUserStore = class {
1635
2376
  db;
1636
2377
  constructor(db) {
@@ -1667,7 +2408,7 @@ var SqliteUserStore = class {
1667
2408
  async createUser(params) {
1668
2409
  const normalizedEmail = params.email.toLowerCase();
1669
2410
  const now = Date.now();
1670
- const id = randomUUID3();
2411
+ const id = randomUUID5();
1671
2412
  try {
1672
2413
  this.db.prepare(`
1673
2414
  INSERT INTO auth_users (id, email, name, email_verified, created_at, password_hash, salt)
@@ -1795,7 +2536,7 @@ function rowToDevice(row) {
1795
2536
  }
1796
2537
 
1797
2538
  // src/provider/built-in/postgres-user-store.ts
1798
- import { randomUUID as randomUUID4 } from "crypto";
2539
+ import { randomUUID as randomUUID6 } from "crypto";
1799
2540
  var PostgresUserStore = class {
1800
2541
  sql;
1801
2542
  ready;
@@ -1837,7 +2578,7 @@ var PostgresUserStore = class {
1837
2578
  await this.ready;
1838
2579
  const normalizedEmail = params.email.toLowerCase();
1839
2580
  const now = Date.now();
1840
- const id = randomUUID4();
2581
+ const id = randomUUID6();
1841
2582
  try {
1842
2583
  await this.sql`
1843
2584
  INSERT INTO auth_users (id, email, name, email_verified, created_at, password_hash, salt)
@@ -2073,8 +2814,8 @@ var BuiltInProvider = class {
2073
2814
  };
2074
2815
 
2075
2816
  // src/provider/external/external-jwt-provider.ts
2076
- import { KoraError as KoraError5 } from "@korajs/core";
2077
- var ExternalAuthOperationNotSupportedError = class extends KoraError5 {
2817
+ import { KoraError as KoraError7 } from "@korajs/core";
2818
+ var ExternalAuthOperationNotSupportedError = class extends KoraError7 {
2078
2819
  constructor(operation, provider) {
2079
2820
  super(
2080
2821
  `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.`,
@@ -2084,7 +2825,7 @@ var ExternalAuthOperationNotSupportedError = class extends KoraError5 {
2084
2825
  this.name = "ExternalAuthOperationNotSupportedError";
2085
2826
  }
2086
2827
  };
2087
- var ExternalTokenValidationError = class extends KoraError5 {
2828
+ var ExternalTokenValidationError = class extends KoraError7 {
2088
2829
  constructor(reason, context) {
2089
2830
  super(`External token validation failed: ${reason}`, "AUTH_EXTERNAL_TOKEN_INVALID", context);
2090
2831
  this.name = "ExternalTokenValidationError";
@@ -2377,8 +3118,8 @@ function createSupabaseAdapter(config) {
2377
3118
 
2378
3119
  // src/passkey/passkey-server.ts
2379
3120
  import { randomBytes as randomBytes3 } from "crypto";
2380
- import { KoraError as KoraError6 } from "@korajs/core";
2381
- var PasskeyVerificationError = class extends KoraError6 {
3121
+ import { KoraError as KoraError8 } from "@korajs/core";
3122
+ var PasskeyVerificationError = class extends KoraError8 {
2382
3123
  constructor(message, context) {
2383
3124
  super(message, "PASSKEY_VERIFICATION_ERROR", context);
2384
3125
  this.name = "PasskeyVerificationError";
@@ -2704,7 +3445,7 @@ function copyComponentToP1363(component, target, targetOffset, componentLength)
2704
3445
  }
2705
3446
 
2706
3447
  // src/org/org-types.ts
2707
- import { KoraError as KoraError7 } from "@korajs/core";
3448
+ import { KoraError as KoraError9 } from "@korajs/core";
2708
3449
  var ORG_ROLES = ["owner", "admin", "member", "viewer", "billing"];
2709
3450
  var ROLE_HIERARCHY = {
2710
3451
  viewer: 10,
@@ -2717,7 +3458,7 @@ function hasRoleLevel(userRole, requiredRole) {
2717
3458
  return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole];
2718
3459
  }
2719
3460
  var INVITATION_STATUSES = ["pending", "accepted", "revoked", "expired"];
2720
- var OrgError = class extends KoraError7 {
3461
+ var OrgError = class extends KoraError9 {
2721
3462
  constructor(message, code, context) {
2722
3463
  super(message, code, context);
2723
3464
  this.name = "OrgError";
@@ -3509,7 +4250,7 @@ function slugify(name) {
3509
4250
  }
3510
4251
 
3511
4252
  // src/rbac/rbac-types.ts
3512
- import { KoraError as KoraError8 } from "@korajs/core";
4253
+ import { KoraError as KoraError10 } from "@korajs/core";
3513
4254
  function parsePermission(permission) {
3514
4255
  const colonIndex = permission.indexOf(":");
3515
4256
  if (colonIndex === -1) {
@@ -3553,7 +4294,7 @@ var BUILT_IN_ROLES = [
3553
4294
  permissions: ["*:*"]
3554
4295
  }
3555
4296
  ];
3556
- var RbacError = class extends KoraError8 {
4297
+ var RbacError = class extends KoraError10 {
3557
4298
  constructor(message, code, context) {
3558
4299
  super(message, code, context);
3559
4300
  this.name = "RbacError";
@@ -3872,302 +4613,409 @@ var OrgScopeResolver = class {
3872
4613
  }
3873
4614
  };
3874
4615
 
3875
- // src/provider/oauth/oauth-types.ts
3876
- import { KoraError as KoraError9 } from "@korajs/core";
3877
- var OAuthError = class extends KoraError9 {
3878
- constructor(message, code, context) {
3879
- super(message, code, context);
3880
- this.name = "OAuthError";
3881
- }
3882
- };
3883
- var OAuthStateMismatchError = class extends OAuthError {
3884
- constructor() {
3885
- super("OAuth state parameter does not match. Possible CSRF attack.", "OAUTH_STATE_MISMATCH");
3886
- }
3887
- };
3888
- var OAuthCodeExchangeError = class extends OAuthError {
3889
- constructor(details) {
3890
- super(
3891
- `Failed to exchange authorization code for tokens.${details ? ` ${details}` : ""}`,
3892
- "OAUTH_CODE_EXCHANGE_FAILED",
3893
- details ? { details } : void 0
3894
- );
4616
+ // src/provider/oauth/sqlite-oauth-store.ts
4617
+ import { randomUUID as randomUUID7 } from "crypto";
4618
+ var SqliteOAuthStateStore = class {
4619
+ db;
4620
+ constructor(db) {
4621
+ this.db = db;
4622
+ this.db.pragma("journal_mode = WAL");
4623
+ this.ensureTables();
3895
4624
  }
3896
- };
3897
- var OAuthUserInfoError = class extends OAuthError {
3898
- constructor(details) {
3899
- super(
3900
- `Failed to fetch user info from OAuth provider.${details ? ` ${details}` : ""}`,
3901
- "OAUTH_USER_INFO_FAILED",
3902
- details ? { details } : void 0
4625
+ async store(state) {
4626
+ this.db.prepare(`
4627
+ INSERT OR REPLACE INTO auth_oauth_states
4628
+ (state, provider, redirect_uri, created_at, expires_at, metadata_json, code_verifier)
4629
+ VALUES (?, ?, ?, ?, ?, ?, ?)
4630
+ `).run(
4631
+ state.state,
4632
+ state.provider,
4633
+ state.redirectUri,
4634
+ state.createdAt,
4635
+ state.expiresAt,
4636
+ state.metadata ? JSON.stringify(state.metadata) : null,
4637
+ state.codeVerifier ?? null
3903
4638
  );
3904
4639
  }
3905
- };
3906
- var OAuthProviderNotFoundError = class extends OAuthError {
3907
- constructor(provider) {
3908
- super(`OAuth provider "${provider}" is not configured.`, "OAUTH_PROVIDER_NOT_FOUND", {
3909
- provider
4640
+ async consume(stateValue) {
4641
+ const consumeInTransaction = this.db.transaction(() => {
4642
+ const row = this.db.prepare("SELECT * FROM auth_oauth_states WHERE state = ?").get(stateValue);
4643
+ if (!row) return null;
4644
+ this.db.prepare("DELETE FROM auth_oauth_states WHERE state = ?").run(stateValue);
4645
+ if (Date.now() > row.expires_at) return null;
4646
+ return rowToOAuthState(row);
3910
4647
  });
4648
+ return consumeInTransaction();
3911
4649
  }
3912
- };
4650
+ async cleanExpired() {
4651
+ const result = this.db.prepare("DELETE FROM auth_oauth_states WHERE expires_at < ?").run(Date.now());
4652
+ return result.changes ?? 0;
4653
+ }
4654
+ ensureTables() {
4655
+ this.db.exec(`
4656
+ CREATE TABLE IF NOT EXISTS auth_oauth_states (
4657
+ state TEXT PRIMARY KEY,
4658
+ provider TEXT NOT NULL,
4659
+ redirect_uri TEXT NOT NULL,
4660
+ created_at INTEGER NOT NULL,
4661
+ expires_at INTEGER NOT NULL,
4662
+ metadata_json TEXT,
4663
+ code_verifier TEXT
4664
+ );
3913
4665
 
3914
- // src/provider/oauth/oauth-flow.ts
3915
- var DEFAULT_STATE_TTL_MS = 10 * 60 * 1e3;
3916
- var InMemoryOAuthStateStore = class {
3917
- states = /* @__PURE__ */ new Map();
3918
- async store(state) {
3919
- this.states.set(state.state, state);
4666
+ CREATE INDEX IF NOT EXISTS idx_auth_oauth_states_expires_at
4667
+ ON auth_oauth_states(expires_at);
4668
+ `);
3920
4669
  }
3921
- async consume(stateValue) {
3922
- const state = this.states.get(stateValue);
3923
- if (!state) return null;
3924
- this.states.delete(stateValue);
3925
- if (Date.now() > state.expiresAt) return null;
3926
- return state;
4670
+ };
4671
+ var SqliteLinkedIdentityStore = class {
4672
+ db;
4673
+ constructor(db) {
4674
+ this.db = db;
4675
+ this.db.pragma("journal_mode = WAL");
4676
+ this.ensureTables();
3927
4677
  }
3928
- async cleanExpired() {
3929
- const now = Date.now();
3930
- let count = 0;
3931
- for (const [key, state] of this.states) {
3932
- if (now > state.expiresAt) {
3933
- this.states.delete(key);
3934
- count++;
4678
+ async findByProvider(provider, providerUserId) {
4679
+ const row = this.db.prepare(`
4680
+ SELECT * FROM auth_linked_identities
4681
+ WHERE provider = ? AND provider_user_id = ?
4682
+ `).get(provider, providerUserId);
4683
+ return row ? rowToLinkedIdentity(row) : null;
4684
+ }
4685
+ async findByUser(userId) {
4686
+ const rows = this.db.prepare(`
4687
+ SELECT * FROM auth_linked_identities
4688
+ WHERE user_id = ?
4689
+ ORDER BY linked_at ASC
4690
+ `).all(userId);
4691
+ return rows.map(rowToLinkedIdentity);
4692
+ }
4693
+ async create(params) {
4694
+ const identity = {
4695
+ id: randomUUID7(),
4696
+ userId: params.userId,
4697
+ provider: params.provider,
4698
+ providerUserId: params.providerUserId,
4699
+ email: params.email,
4700
+ linkedAt: Date.now()
4701
+ };
4702
+ try {
4703
+ this.db.prepare(`
4704
+ INSERT INTO auth_linked_identities
4705
+ (id, user_id, provider, provider_user_id, email, linked_at)
4706
+ VALUES (?, ?, ?, ?, ?, ?)
4707
+ `).run(
4708
+ identity.id,
4709
+ identity.userId,
4710
+ identity.provider,
4711
+ identity.providerUserId,
4712
+ identity.email,
4713
+ identity.linkedAt
4714
+ );
4715
+ } catch (error) {
4716
+ if (error instanceof Error && error.message.includes("UNIQUE constraint failed")) {
4717
+ throw new DuplicateLinkedIdentityError(params.provider);
3935
4718
  }
4719
+ throw error;
3936
4720
  }
3937
- return count;
4721
+ return identity;
4722
+ }
4723
+ async delete(userId, provider) {
4724
+ this.db.prepare("DELETE FROM auth_linked_identities WHERE user_id = ? AND provider = ?").run(userId, provider);
4725
+ }
4726
+ ensureTables() {
4727
+ this.db.exec(`
4728
+ CREATE TABLE IF NOT EXISTS auth_linked_identities (
4729
+ id TEXT PRIMARY KEY,
4730
+ user_id TEXT NOT NULL,
4731
+ provider TEXT NOT NULL,
4732
+ provider_user_id TEXT NOT NULL,
4733
+ email TEXT,
4734
+ linked_at INTEGER NOT NULL,
4735
+ UNIQUE(provider, provider_user_id),
4736
+ UNIQUE(user_id, provider)
4737
+ );
4738
+
4739
+ CREATE INDEX IF NOT EXISTS idx_auth_linked_identities_user_id
4740
+ ON auth_linked_identities(user_id);
4741
+ `);
3938
4742
  }
3939
4743
  };
3940
- var OAuthManager = class {
3941
- providers = /* @__PURE__ */ new Map();
3942
- stateStore;
3943
- stateTtlMs;
3944
- fetchFn;
3945
- constructor(config) {
3946
- for (const provider of config.providers) {
3947
- this.providers.set(provider.providerId, provider);
3948
- }
3949
- this.stateStore = config.stateStore ?? new InMemoryOAuthStateStore();
3950
- this.stateTtlMs = config.stateTtlMs ?? DEFAULT_STATE_TTL_MS;
3951
- this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
4744
+ async function createSqliteOAuthStateStore(options) {
4745
+ const Database = await loadBetterSqlite32();
4746
+ const db = new Database(options.filename);
4747
+ return new SqliteOAuthStateStore(db);
4748
+ }
4749
+ async function createSqliteLinkedIdentityStore(options) {
4750
+ const Database = await loadBetterSqlite32();
4751
+ const db = new Database(options.filename);
4752
+ return new SqliteLinkedIdentityStore(db);
4753
+ }
4754
+ async function createSqliteOAuthStores(options) {
4755
+ const Database = await loadBetterSqlite32();
4756
+ const db = new Database(options.filename);
4757
+ return {
4758
+ stateStore: new SqliteOAuthStateStore(db),
4759
+ linkedIdentityStore: new SqliteLinkedIdentityStore(db)
4760
+ };
4761
+ }
4762
+ async function loadBetterSqlite32() {
4763
+ try {
4764
+ const { createRequire } = await import("module");
4765
+ const require2 = createRequire(import.meta.url);
4766
+ return require2("better-sqlite3");
4767
+ } catch {
4768
+ throw new Error(
4769
+ 'SQLite OAuth stores require the "better-sqlite3" package. Install it in your project dependencies.'
4770
+ );
3952
4771
  }
3953
- /**
3954
- * Generate an authorization URL for the user to visit.
3955
- * Returns the URL and the state parameter for CSRF validation.
3956
- */
3957
- async getAuthorizationUrl(providerId, metadata) {
3958
- const provider = this.getProvider(providerId);
3959
- const state = generateState();
3960
- const now = Date.now();
3961
- const oauthState = {
3962
- state,
3963
- provider: providerId,
3964
- redirectUri: provider.redirectUri,
3965
- createdAt: now,
3966
- expiresAt: now + this.stateTtlMs,
3967
- metadata
3968
- };
3969
- await this.stateStore.store(oauthState);
3970
- const params = new URLSearchParams({
3971
- client_id: provider.clientId,
3972
- redirect_uri: provider.redirectUri,
3973
- response_type: "code",
3974
- scope: provider.scopes.join(" "),
3975
- state
4772
+ }
4773
+ function rowToOAuthState(row) {
4774
+ return {
4775
+ state: row.state,
4776
+ provider: row.provider,
4777
+ redirectUri: row.redirect_uri,
4778
+ createdAt: row.created_at,
4779
+ expiresAt: row.expires_at,
4780
+ metadata: parseMetadata(row.metadata_json),
4781
+ codeVerifier: row.code_verifier ?? void 0
4782
+ };
4783
+ }
4784
+ function parseMetadata(value) {
4785
+ if (!value) return void 0;
4786
+ const parsed = JSON.parse(value);
4787
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
4788
+ }
4789
+ function rowToLinkedIdentity(row) {
4790
+ return {
4791
+ id: row.id,
4792
+ userId: row.user_id,
4793
+ provider: row.provider,
4794
+ providerUserId: row.provider_user_id,
4795
+ email: row.email,
4796
+ linkedAt: row.linked_at
4797
+ };
4798
+ }
4799
+
4800
+ // src/provider/oauth/postgres-oauth-store.ts
4801
+ import { randomUUID as randomUUID8 } from "crypto";
4802
+ var PostgresOAuthStateStore = class {
4803
+ sql;
4804
+ ready;
4805
+ constructor(sql) {
4806
+ this.sql = sql;
4807
+ this.ready = this.ensureTables();
4808
+ }
4809
+ async store(state) {
4810
+ await this.ready;
4811
+ await this.sql`
4812
+ INSERT INTO auth_oauth_states
4813
+ (state, provider, redirect_uri, created_at, expires_at, metadata_json, code_verifier)
4814
+ VALUES (
4815
+ ${state.state},
4816
+ ${state.provider},
4817
+ ${state.redirectUri},
4818
+ ${state.createdAt},
4819
+ ${state.expiresAt},
4820
+ ${state.metadata ? JSON.stringify(state.metadata) : null},
4821
+ ${state.codeVerifier ?? null}
4822
+ )
4823
+ ON CONFLICT (state) DO UPDATE SET
4824
+ provider = EXCLUDED.provider,
4825
+ redirect_uri = EXCLUDED.redirect_uri,
4826
+ created_at = EXCLUDED.created_at,
4827
+ expires_at = EXCLUDED.expires_at,
4828
+ metadata_json = EXCLUDED.metadata_json,
4829
+ code_verifier = EXCLUDED.code_verifier
4830
+ `;
4831
+ }
4832
+ async consume(stateValue) {
4833
+ await this.ready;
4834
+ return this.sql.begin(async (tx) => {
4835
+ const rows = await tx`
4836
+ DELETE FROM auth_oauth_states
4837
+ WHERE state = ${stateValue}
4838
+ RETURNING *
4839
+ `;
4840
+ const row = rows[0];
4841
+ if (!row || Date.now() > Number(row.expires_at)) {
4842
+ return null;
4843
+ }
4844
+ return rowToOAuthState2(row);
3976
4845
  });
3977
- const url = `${provider.authorizationUrl}?${params.toString()}`;
3978
- return { url, state };
3979
4846
  }
3980
- /**
3981
- * Handle the OAuth callback after the user authorizes.
3982
- * Validates the state parameter, exchanges the code for tokens,
3983
- * and fetches user info.
3984
- *
3985
- * @param providerId - The OAuth provider
3986
- * @param code - The authorization code from the callback
3987
- * @param state - The state parameter from the callback
3988
- * @returns Tokens and user info from the provider
3989
- */
3990
- async handleCallback(providerId, code, state) {
3991
- const provider = this.getProvider(providerId);
3992
- const oauthState = await this.stateStore.consume(state);
3993
- if (!oauthState || oauthState.provider !== providerId) {
3994
- throw new OAuthStateMismatchError();
3995
- }
3996
- const tokens = await this.exchangeCodeForTokens(provider, code);
3997
- const userInfo = await this.fetchUserInfo(provider, tokens.accessToken);
3998
- return { tokens, userInfo, stateMetadata: oauthState.metadata };
4847
+ async cleanExpired() {
4848
+ await this.ready;
4849
+ const rows = await this.sql`
4850
+ DELETE FROM auth_oauth_states
4851
+ WHERE expires_at < ${Date.now()}
4852
+ RETURNING state
4853
+ `;
4854
+ return rows.length;
3999
4855
  }
4000
- /**
4001
- * Get a registered provider by ID.
4002
- */
4003
- getProvider(providerId) {
4004
- const provider = this.providers.get(providerId);
4005
- if (!provider) {
4006
- throw new OAuthProviderNotFoundError(providerId);
4007
- }
4008
- return provider;
4856
+ async ensureTables() {
4857
+ await this.sql`
4858
+ CREATE TABLE IF NOT EXISTS auth_oauth_states (
4859
+ state TEXT PRIMARY KEY,
4860
+ provider TEXT NOT NULL,
4861
+ redirect_uri TEXT NOT NULL,
4862
+ created_at BIGINT NOT NULL,
4863
+ expires_at BIGINT NOT NULL,
4864
+ metadata_json TEXT,
4865
+ code_verifier TEXT
4866
+ )
4867
+ `;
4868
+ await this.sql`
4869
+ CREATE INDEX IF NOT EXISTS idx_auth_oauth_states_expires_at
4870
+ ON auth_oauth_states(expires_at)
4871
+ `;
4009
4872
  }
4010
- /**
4011
- * List all registered provider IDs.
4012
- */
4013
- getProviderIds() {
4014
- return [...this.providers.keys()];
4873
+ };
4874
+ var PostgresLinkedIdentityStore = class {
4875
+ sql;
4876
+ ready;
4877
+ constructor(sql) {
4878
+ this.sql = sql;
4879
+ this.ready = this.ensureTables();
4015
4880
  }
4016
- // --- Private ---
4017
- async exchangeCodeForTokens(provider, code) {
4018
- const body = new URLSearchParams({
4019
- grant_type: "authorization_code",
4020
- code,
4021
- redirect_uri: provider.redirectUri,
4022
- client_id: provider.clientId,
4023
- client_secret: provider.clientSecret
4024
- });
4025
- let response;
4881
+ async findByProvider(provider, providerUserId) {
4882
+ await this.ready;
4883
+ const rows = await this.sql`
4884
+ SELECT * FROM auth_linked_identities
4885
+ WHERE provider = ${provider} AND provider_user_id = ${providerUserId}
4886
+ `;
4887
+ return rows[0] ? rowToLinkedIdentity2(rows[0]) : null;
4888
+ }
4889
+ async findByUser(userId) {
4890
+ await this.ready;
4891
+ const rows = await this.sql`
4892
+ SELECT * FROM auth_linked_identities
4893
+ WHERE user_id = ${userId}
4894
+ ORDER BY linked_at ASC
4895
+ `;
4896
+ return rows.map(rowToLinkedIdentity2);
4897
+ }
4898
+ async create(params) {
4899
+ await this.ready;
4900
+ const identity = {
4901
+ id: randomUUID8(),
4902
+ userId: params.userId,
4903
+ provider: params.provider,
4904
+ providerUserId: params.providerUserId,
4905
+ email: params.email,
4906
+ linkedAt: Date.now()
4907
+ };
4026
4908
  try {
4027
- response = await this.fetchFn(provider.tokenUrl, {
4028
- method: "POST",
4029
- headers: {
4030
- "Content-Type": "application/x-www-form-urlencoded",
4031
- Accept: "application/json"
4032
- },
4033
- body: body.toString()
4034
- });
4035
- } catch (err) {
4036
- throw new OAuthCodeExchangeError(err instanceof Error ? err.message : "Network error");
4037
- }
4038
- if (!response.ok) {
4039
- let details = `HTTP ${response.status}`;
4040
- try {
4041
- const errorBody = await response.text();
4042
- details += `: ${errorBody}`;
4043
- } catch {
4909
+ await this.sql`
4910
+ INSERT INTO auth_linked_identities
4911
+ (id, user_id, provider, provider_user_id, email, linked_at)
4912
+ VALUES (
4913
+ ${identity.id},
4914
+ ${identity.userId},
4915
+ ${identity.provider},
4916
+ ${identity.providerUserId},
4917
+ ${identity.email},
4918
+ ${identity.linkedAt}
4919
+ )
4920
+ `;
4921
+ } catch (error) {
4922
+ if (isUniqueViolation(error)) {
4923
+ throw new DuplicateLinkedIdentityError(params.provider);
4044
4924
  }
4045
- throw new OAuthCodeExchangeError(details);
4925
+ throw error;
4046
4926
  }
4047
- const data = await response.json();
4048
- return {
4049
- accessToken: data.access_token,
4050
- tokenType: data.token_type ?? "Bearer",
4051
- expiresIn: data.expires_in,
4052
- refreshToken: data.refresh_token,
4053
- idToken: data.id_token,
4054
- scope: data.scope
4055
- };
4927
+ return identity;
4056
4928
  }
4057
- async fetchUserInfo(provider, accessToken) {
4058
- let response;
4059
- try {
4060
- response = await this.fetchFn(provider.userInfoUrl, {
4061
- headers: {
4062
- Authorization: `Bearer ${accessToken}`,
4063
- Accept: "application/json"
4064
- }
4065
- });
4066
- } catch (err) {
4067
- throw new OAuthUserInfoError(err instanceof Error ? err.message : "Network error");
4068
- }
4069
- if (!response.ok) {
4070
- throw new OAuthUserInfoError(`HTTP ${response.status}`);
4071
- }
4072
- const profile = await response.json();
4073
- return normalizeUserInfo(provider.providerId, profile);
4929
+ async delete(userId, provider) {
4930
+ await this.ready;
4931
+ await this.sql`
4932
+ DELETE FROM auth_linked_identities
4933
+ WHERE user_id = ${userId} AND provider = ${provider}
4934
+ `;
4074
4935
  }
4075
- };
4076
- function normalizeUserInfo(providerId, profile) {
4077
- switch (providerId) {
4078
- case "google":
4079
- return {
4080
- providerId: profile.sub,
4081
- provider: "google",
4082
- email: profile.email ?? null,
4083
- emailVerified: profile.email_verified ?? false,
4084
- name: profile.name ?? null,
4085
- avatarUrl: profile.picture ?? null,
4086
- rawProfile: profile
4087
- };
4088
- case "github":
4089
- return {
4090
- providerId: String(profile.id),
4091
- provider: "github",
4092
- email: profile.email ?? null,
4093
- emailVerified: false,
4094
- // GitHub doesn't confirm in the profile response
4095
- name: profile.name ?? profile.login ?? null,
4096
- avatarUrl: profile.avatar_url ?? null,
4097
- rawProfile: profile
4098
- };
4099
- case "microsoft":
4100
- return {
4101
- providerId: profile.id,
4102
- provider: "microsoft",
4103
- email: profile.mail ?? profile.userPrincipalName ?? null,
4104
- emailVerified: false,
4105
- name: profile.displayName ?? null,
4106
- avatarUrl: null,
4107
- rawProfile: profile
4108
- };
4109
- default:
4110
- return {
4111
- providerId: String(profile.id ?? profile.sub ?? ""),
4112
- provider: providerId,
4113
- email: profile.email ?? null,
4114
- emailVerified: profile.email_verified ?? false,
4115
- name: profile.name ?? null,
4116
- avatarUrl: profile.picture ?? profile.avatar_url ?? null,
4117
- rawProfile: profile
4118
- };
4936
+ async ensureTables() {
4937
+ await this.sql`
4938
+ CREATE TABLE IF NOT EXISTS auth_linked_identities (
4939
+ id TEXT PRIMARY KEY,
4940
+ user_id TEXT NOT NULL,
4941
+ provider TEXT NOT NULL,
4942
+ provider_user_id TEXT NOT NULL,
4943
+ email TEXT,
4944
+ linked_at BIGINT NOT NULL,
4945
+ UNIQUE(provider, provider_user_id),
4946
+ UNIQUE(user_id, provider)
4947
+ )
4948
+ `;
4949
+ await this.sql`
4950
+ CREATE INDEX IF NOT EXISTS idx_auth_linked_identities_user_id
4951
+ ON auth_linked_identities(user_id)
4952
+ `;
4119
4953
  }
4954
+ };
4955
+ async function createPostgresOAuthStateStore(options) {
4956
+ const postgresClient = await loadPostgresDeps2();
4957
+ const sql = postgresClient(options.connectionString);
4958
+ return new PostgresOAuthStateStore(sql);
4120
4959
  }
4121
- function googleProvider(config) {
4960
+ async function createPostgresLinkedIdentityStore(options) {
4961
+ const postgresClient = await loadPostgresDeps2();
4962
+ const sql = postgresClient(options.connectionString);
4963
+ return new PostgresLinkedIdentityStore(sql);
4964
+ }
4965
+ async function createPostgresOAuthStores(options) {
4966
+ const postgresClient = await loadPostgresDeps2();
4967
+ const sql = postgresClient(options.connectionString);
4122
4968
  return {
4123
- providerId: "google",
4124
- clientId: config.clientId,
4125
- clientSecret: config.clientSecret,
4126
- authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
4127
- tokenUrl: "https://oauth2.googleapis.com/token",
4128
- userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
4129
- scopes: config.scopes ?? ["openid", "email", "profile"],
4130
- redirectUri: config.redirectUri
4969
+ stateStore: new PostgresOAuthStateStore(sql),
4970
+ linkedIdentityStore: new PostgresLinkedIdentityStore(sql)
4131
4971
  };
4132
4972
  }
4133
- function githubProvider(config) {
4973
+ async function loadPostgresDeps2() {
4974
+ try {
4975
+ const dynamicImport = new Function("specifier", "return import(specifier)");
4976
+ const postgresMod = await dynamicImport("postgres");
4977
+ return postgresMod.default;
4978
+ } catch {
4979
+ throw new Error(
4980
+ 'PostgreSQL OAuth stores require the "postgres" package. Install it in your project dependencies.'
4981
+ );
4982
+ }
4983
+ }
4984
+ function rowToOAuthState2(row) {
4134
4985
  return {
4135
- providerId: "github",
4136
- clientId: config.clientId,
4137
- clientSecret: config.clientSecret,
4138
- authorizationUrl: "https://github.com/login/oauth/authorize",
4139
- tokenUrl: "https://github.com/login/oauth/access_token",
4140
- userInfoUrl: "https://api.github.com/user",
4141
- scopes: config.scopes ?? ["read:user", "user:email"],
4142
- redirectUri: config.redirectUri
4986
+ state: row.state,
4987
+ provider: row.provider,
4988
+ redirectUri: row.redirect_uri,
4989
+ createdAt: Number(row.created_at),
4990
+ expiresAt: Number(row.expires_at),
4991
+ metadata: parseMetadata2(row.metadata_json),
4992
+ codeVerifier: row.code_verifier ?? void 0
4143
4993
  };
4144
4994
  }
4145
- function microsoftProvider(config) {
4146
- const tenant = config.tenantId ?? "common";
4995
+ function parseMetadata2(value) {
4996
+ if (!value) return void 0;
4997
+ const parsed = JSON.parse(value);
4998
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
4999
+ }
5000
+ function rowToLinkedIdentity2(row) {
4147
5001
  return {
4148
- providerId: "microsoft",
4149
- clientId: config.clientId,
4150
- clientSecret: config.clientSecret,
4151
- authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
4152
- tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
4153
- userInfoUrl: "https://graph.microsoft.com/v1.0/me",
4154
- scopes: config.scopes ?? ["openid", "email", "profile", "User.Read"],
4155
- redirectUri: config.redirectUri
5002
+ id: row.id,
5003
+ userId: row.user_id,
5004
+ provider: row.provider,
5005
+ providerUserId: row.provider_user_id,
5006
+ email: row.email,
5007
+ linkedAt: Number(row.linked_at)
4156
5008
  };
4157
5009
  }
4158
- function generateState() {
4159
- const bytes = new Uint8Array(32);
4160
- globalThis.crypto.getRandomValues(bytes);
4161
- let binary = "";
4162
- for (let i = 0; i < bytes.length; i++) {
4163
- binary += String.fromCharCode(bytes[i]);
4164
- }
4165
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5010
+ function isUniqueViolation(error) {
5011
+ if (!(error instanceof Error)) return false;
5012
+ const code = error.code;
5013
+ return code === "23505" || error.message.includes("unique constraint") || error.message.includes("duplicate key");
4166
5014
  }
4167
5015
 
4168
5016
  // src/session/session.ts
4169
- import { KoraError as KoraError10 } from "@korajs/core";
4170
- var SessionError = class extends KoraError10 {
5017
+ import { KoraError as KoraError11 } from "@korajs/core";
5018
+ var SessionError = class extends KoraError11 {
4171
5019
  constructor(message, code, context) {
4172
5020
  super(message, code, context);
4173
5021
  this.name = "SessionError";
@@ -4396,8 +5244,8 @@ function generateSessionId() {
4396
5244
  }
4397
5245
 
4398
5246
  // src/mfa/totp.ts
4399
- import { KoraError as KoraError11 } from "@korajs/core";
4400
- var TotpError = class extends KoraError11 {
5247
+ import { KoraError as KoraError12 } from "@korajs/core";
5248
+ var TotpError = class extends KoraError12 {
4401
5249
  constructor(message, code, context) {
4402
5250
  super(message, code, context);
4403
5251
  this.name = "TotpError";
@@ -4820,8 +5668,8 @@ function timingSafeEqual(a, b) {
4820
5668
  }
4821
5669
 
4822
5670
  // src/admin/admin-api.ts
4823
- import { KoraError as KoraError12 } from "@korajs/core";
4824
- var AdminApiError = class extends KoraError12 {
5671
+ import { KoraError as KoraError13 } from "@korajs/core";
5672
+ var AdminApiError = class extends KoraError13 {
4825
5673
  constructor(message, code, context) {
4826
5674
  super(message, code, context);
4827
5675
  this.name = "AdminApiError";
@@ -4855,7 +5703,7 @@ var AdminApi = class {
4855
5703
  throw new AdminUserNotFoundError(userId);
4856
5704
  }
4857
5705
  await this.audit("admin.user_lookup", adminId, userId, "user");
4858
- return toAuthUser2(user);
5706
+ return toAuthUser3(user);
4859
5707
  }
4860
5708
  /**
4861
5709
  * List users with optional filtering and pagination.
@@ -4873,7 +5721,7 @@ var AdminApi = class {
4873
5721
  filtered = filtered.filter((u) => u.emailVerified === query.emailVerified);
4874
5722
  }
4875
5723
  const total = filtered.length;
4876
- const data = filtered.slice(offset, offset + limit).map(toAuthUser2);
5724
+ const data = filtered.slice(offset, offset + limit).map(toAuthUser3);
4877
5725
  return { data, total, limit, offset };
4878
5726
  }
4879
5727
  /**
@@ -4895,7 +5743,7 @@ var AdminApi = class {
4895
5743
  }
4896
5744
  await this.userStore.update(user);
4897
5745
  await this.audit("user.update", adminId, userId, "user", { updates });
4898
- return toAuthUser2(user);
5746
+ return toAuthUser3(user);
4899
5747
  }
4900
5748
  /**
4901
5749
  * Delete a user and all associated sessions.
@@ -4960,7 +5808,7 @@ var AdminApi = class {
4960
5808
  });
4961
5809
  }
4962
5810
  };
4963
- function toAuthUser2(stored) {
5811
+ function toAuthUser3(stored) {
4964
5812
  return {
4965
5813
  id: stored.id,
4966
5814
  email: stored.email,
@@ -4971,8 +5819,8 @@ function toAuthUser2(stored) {
4971
5819
  }
4972
5820
 
4973
5821
  // src/admin/audit-log.ts
4974
- import { KoraError as KoraError13 } from "@korajs/core";
4975
- var AuditLogError = class extends KoraError13 {
5822
+ import { KoraError as KoraError14 } from "@korajs/core";
5823
+ var AuditLogError = class extends KoraError14 {
4976
5824
  constructor(message, code, context) {
4977
5825
  super(message, code, context);
4978
5826
  this.name = "AuditLogError";
@@ -5097,8 +5945,8 @@ function generateAuditId() {
5097
5945
  }
5098
5946
 
5099
5947
  // src/admin/webhooks.ts
5100
- import { KoraError as KoraError14 } from "@korajs/core";
5101
- var WebhookError = class extends KoraError14 {
5948
+ import { KoraError as KoraError15 } from "@korajs/core";
5949
+ var WebhookError = class extends KoraError15 {
5102
5950
  constructor(message, code, context) {
5103
5951
  super(message, code, context);
5104
5952
  this.name = "WebhookError";
@@ -5327,6 +6175,7 @@ export {
5327
6175
  CannotRemoveOwnerError,
5328
6176
  CircularInheritanceError,
5329
6177
  DuplicateEmailError,
6178
+ DuplicateLinkedIdentityError,
5330
6179
  EmailVerificationError,
5331
6180
  EmailVerificationManager,
5332
6181
  ExternalAuthOperationNotSupportedError,
@@ -5336,6 +6185,7 @@ export {
5336
6185
  InMemoryAuditLogStore,
5337
6186
  InMemoryChallengeStore,
5338
6187
  InMemoryEmailVerificationStore,
6188
+ InMemoryLinkedIdentityStore,
5339
6189
  InMemoryOAuthStateStore,
5340
6190
  InMemoryOrgStore,
5341
6191
  InMemoryPasswordResetStore,
@@ -5368,6 +6218,8 @@ export {
5368
6218
  PasskeyVerificationError,
5369
6219
  PasswordResetError,
5370
6220
  PasswordResetManager,
6221
+ PostgresLinkedIdentityStore,
6222
+ PostgresOAuthStateStore,
5371
6223
  PostgresUserStore,
5372
6224
  ROLE_HIERARCHY,
5373
6225
  RbacEngine,
@@ -5382,6 +6234,8 @@ export {
5382
6234
  SessionManager,
5383
6235
  SessionMfaRequiredError,
5384
6236
  SessionNotFoundError,
6237
+ SqliteLinkedIdentityStore,
6238
+ SqliteOAuthStateStore,
5385
6239
  SqliteUserStore,
5386
6240
  TokenManager,
5387
6241
  TotpAlreadyEnabledError,
@@ -5400,7 +6254,14 @@ export {
5400
6254
  base32Encode,
5401
6255
  computePublicKeyThumbprint,
5402
6256
  createClerkAdapter,
6257
+ createKoraAuthServer,
6258
+ createPostgresLinkedIdentityStore,
6259
+ createPostgresOAuthStateStore,
6260
+ createPostgresOAuthStores,
5403
6261
  createPostgresUserStore,
6262
+ createSqliteLinkedIdentityStore,
6263
+ createSqliteOAuthStateStore,
6264
+ createSqliteOAuthStores,
5404
6265
  createSqliteUserStore,
5405
6266
  createSupabaseAdapter,
5406
6267
  decodeJwt,