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