sporades 0.9.8 โ 0.9.9
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/CHANGES.md +20 -3
- package/bin/sporades-host-helper.js +1 -1
- package/bin/sporades.js +241 -26
- package/dist/auth-admission.d.ts +1 -0
- package/dist/auth-admission.d.ts.map +1 -1
- package/dist/auth-admission.js +22 -6
- package/dist/auth-admission.js.map +1 -1
- package/dist/auth-runtime.d.ts +3 -0
- package/dist/auth-runtime.d.ts.map +1 -1
- package/dist/auth-runtime.js +64 -2
- package/dist/auth-runtime.js.map +1 -1
- package/dist/cli/cli-version.d.ts +1 -1
- package/dist/cli/cli-version.js +1 -1
- package/dist/database-runtime.d.ts.map +1 -1
- package/dist/database-runtime.js +54 -6
- package/dist/database-runtime.js.map +1 -1
- package/dist/generated-source-manifest.json +2 -2
- package/dist/server-runtime-source.d.ts.map +1 -1
- package/dist/server-runtime-source.js +93 -2
- package/dist/server-runtime-source.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -3
- package/dist/server.js.map +1 -1
- package/dist/templates/client-runtime-template.d.ts.map +1 -1
- package/dist/templates/client-runtime-template.js +14 -0
- package/dist/templates/client-runtime-template.js.map +1 -1
- package/package.json +1 -1
- package/src/types/client.d.ts +9 -2
- package/src/types/server.d.ts +3 -1
package/CHANGES.md
CHANGED
|
@@ -4,7 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
Changes since v0.9.7.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### ๐ Features
|
|
8
|
+
|
|
9
|
+
- Add purpose-bound reauthentication proofs (c4ddd032).
|
|
10
|
+
|
|
11
|
+
### ๐ Documentation
|
|
12
|
+
|
|
13
|
+
- Request Google signed reauthentication time (2fa291dc).
|
|
14
|
+
- Verify OAuth reauthentication freshness (7a6c989a).
|
|
15
|
+
- Persist and serialize email reauthentication (74ef1c50).
|
|
16
|
+
- Harden email reauthentication contracts (a919df94).
|
|
17
|
+
- Sweep expired proofs before guarded mutations (c888d70c).
|
|
18
|
+
- Harden reauthentication lifecycle (c8189c58).
|
|
19
|
+
- Bind reauthentication to active sessions (e692028f).
|
|
20
|
+
|
|
21
|
+
### ๐งช Tests
|
|
22
|
+
|
|
23
|
+
- Retire proofs during Session rotation (b1e25b48).
|
|
24
|
+
- Harden reauthentication failure and ordering proof (8f933b91).
|
|
25
|
+
- Require active User for proof consumption (465bf5a4).
|
|
26
|
+
- Recheck OAuth authorization at callback (19c4159c).
|
|
8
27
|
|
|
9
28
|
## Unreleased - 2026-08-26
|
|
10
29
|
|
|
@@ -51,5 +70,3 @@ generated artifacts, and documentation.
|
|
|
51
70
|
not select a current Team or automatically partition Capsule data; Sporades
|
|
52
71
|
never sends Join-link email. See the [Built-in Teams reference](https://mgscox.github.io/sporades/reference/teams).
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
|
|
@@ -622,7 +622,7 @@ function writeEnvelope(result, failed = false) {
|
|
|
622
622
|
}
|
|
623
623
|
|
|
624
624
|
// src/cli/cli-version.ts
|
|
625
|
-
var CLI_VERSION = "0.9.
|
|
625
|
+
var CLI_VERSION = "0.9.9";
|
|
626
626
|
|
|
627
627
|
// src/cli/schedule-inspection-envelope.ts
|
|
628
628
|
var SCHEDULE_DIAGNOSTIC_FIELDS = /* @__PURE__ */ new Set([
|
package/bin/sporades.js
CHANGED
|
@@ -210,6 +210,9 @@ export const auth = {
|
|
|
210
210
|
signIn(provider, credentials, options) {
|
|
211
211
|
return connect().signIn(provider, credentials, options);
|
|
212
212
|
},
|
|
213
|
+
reauthenticate(provider, credentials, purpose) {
|
|
214
|
+
return connect().reauthenticate(provider, credentials, purpose);
|
|
215
|
+
},
|
|
213
216
|
signOut() {
|
|
214
217
|
return connect().signOut();
|
|
215
218
|
},
|
|
@@ -311,6 +314,9 @@ export function createHooks(primitives) {
|
|
|
311
314
|
signIn(provider, credentials, options) {
|
|
312
315
|
return connect().signIn(provider, credentials, options);
|
|
313
316
|
},
|
|
317
|
+
reauthenticate(provider, credentials, purpose) {
|
|
318
|
+
return connect().reauthenticate(provider, credentials, purpose);
|
|
319
|
+
},
|
|
314
320
|
signOut() {
|
|
315
321
|
return connect().signOut();
|
|
316
322
|
},
|
|
@@ -372,6 +378,7 @@ export function createVueComposables(primitives) {
|
|
|
372
378
|
state.isAuthenticated = () => Boolean(state.auth?.isAuthenticated);
|
|
373
379
|
state.signUp = (provider, credentials, options) => connect().signUp(provider, credentials, options);
|
|
374
380
|
state.signIn = (provider, credentials, options) => connect().signIn(provider, credentials, options);
|
|
381
|
+
state.reauthenticate = (provider, credentials, purpose) => connect().reauthenticate(provider, credentials, purpose);
|
|
375
382
|
state.signOut = () => connect().signOut();
|
|
376
383
|
state.setPassword = (email, currentPassword, newPassword) => connect().setPassword(email, currentPassword, newPassword);
|
|
377
384
|
return state;
|
|
@@ -425,6 +432,7 @@ export function createSolidPrimitives(primitives) {
|
|
|
425
432
|
isAuthenticated: () => Boolean(state().auth?.isAuthenticated),
|
|
426
433
|
signUp: (provider, credentials, options) => connect().signUp(provider, credentials, options),
|
|
427
434
|
signIn: (provider, credentials, options) => connect().signIn(provider, credentials, options),
|
|
435
|
+
reauthenticate: (provider, credentials, purpose) => connect().reauthenticate(provider, credentials, purpose),
|
|
428
436
|
signOut: () => connect().signOut(),
|
|
429
437
|
setPassword: (email, currentPassword, newPassword) => connect().setPassword(email, currentPassword, newPassword),
|
|
430
438
|
};
|
|
@@ -524,6 +532,7 @@ export function createLitControllers() {
|
|
|
524
532
|
controller.isAuthenticated = () => Boolean(controller.state.auth?.isAuthenticated);
|
|
525
533
|
controller.signUp = (provider, credentials, options) => connect().signUp(provider, credentials, options);
|
|
526
534
|
controller.signIn = (provider, credentials, options) => connect().signIn(provider, credentials, options);
|
|
535
|
+
controller.reauthenticate = (provider, credentials, purpose) => connect().reauthenticate(provider, credentials, purpose);
|
|
527
536
|
controller.signOut = () => connect().signOut();
|
|
528
537
|
controller.setPassword = (email, currentPassword, newPassword) => connect().setPassword(email, currentPassword, newPassword);
|
|
529
538
|
return controller;
|
|
@@ -600,6 +609,7 @@ export function createInfernoAdapters() {
|
|
|
600
609
|
adapter.isAuthenticated = () => Boolean(adapter.state.auth?.isAuthenticated);
|
|
601
610
|
adapter.signUp = (provider, credentials, options) => connect().signUp(provider, credentials, options);
|
|
602
611
|
adapter.signIn = (provider, credentials, options) => connect().signIn(provider, credentials, options);
|
|
612
|
+
adapter.reauthenticate = (provider, credentials, purpose) => connect().reauthenticate(provider, credentials, purpose);
|
|
603
613
|
adapter.signOut = () => connect().signOut();
|
|
604
614
|
adapter.setPassword = (email, currentPassword, newPassword) => connect().setPassword(email, currentPassword, newPassword);
|
|
605
615
|
return adapter;
|
|
@@ -651,6 +661,7 @@ export function createSvelteStores() {
|
|
|
651
661
|
subscribe: store.subscribe,
|
|
652
662
|
signUp: (provider, credentials, options) => connect().signUp(provider, credentials, options),
|
|
653
663
|
signIn: (provider, credentials, options) => connect().signIn(provider, credentials, options),
|
|
664
|
+
reauthenticate: (provider, credentials, purpose) => connect().reauthenticate(provider, credentials, purpose),
|
|
654
665
|
signOut: () => connect().signOut(),
|
|
655
666
|
setPassword: (email, currentPassword, newPassword) => connect().setPassword(email, currentPassword, newPassword),
|
|
656
667
|
};
|
|
@@ -1165,6 +1176,9 @@ function createConnection() {
|
|
|
1165
1176
|
return result;
|
|
1166
1177
|
});
|
|
1167
1178
|
},
|
|
1179
|
+
reauthenticate(provider, credentials, purpose) {
|
|
1180
|
+
return request("auth.reauthenticate", { provider, credentials, purpose, returnTo: window.location.href }).then((result) => { if (result.data?.url) window.location.assign(result.data.url); return result; });
|
|
1181
|
+
},
|
|
1168
1182
|
signOut() {
|
|
1169
1183
|
return request("auth.signOut").then(async (result) => {
|
|
1170
1184
|
if (!result.error && result.data?.ok === true) {
|
|
@@ -2282,8 +2296,8 @@ function readAuthRequirements(handler) {
|
|
|
2282
2296
|
return typeof handler === "function" ? handler[AUTH_REQUIREMENTS] ?? null : null;
|
|
2283
2297
|
}
|
|
2284
2298
|
function normalizeAuthRequirements(options = {}) {
|
|
2285
|
-
if (!isPlainObject(options) || Object.keys(options).some((key) => !["linked", "credentials", "scopes"].includes(key))) {
|
|
2286
|
-
throw invalidAuthRequirements("Use only linked, credentials, and
|
|
2299
|
+
if (!isPlainObject(options) || Object.keys(options).some((key) => !["linked", "credentials", "scopes", "reauthentication"].includes(key))) {
|
|
2300
|
+
throw invalidAuthRequirements("Use only linked, credentials, scopes, and reauthentication in a declarative Auth requirement.");
|
|
2287
2301
|
}
|
|
2288
2302
|
if ("linked" in options && typeof options.linked !== "boolean") {
|
|
2289
2303
|
throw invalidAuthRequirements("linked must be a boolean when supplied.");
|
|
@@ -2294,10 +2308,13 @@ function normalizeAuthRequirements(options = {}) {
|
|
|
2294
2308
|
code: "INVALID_AUTH_REQUIREMENTS",
|
|
2295
2309
|
hint: "Required scopes must be unique concrete strings declared by the Capsule."
|
|
2296
2310
|
});
|
|
2311
|
+
const reauthentication = options.reauthentication;
|
|
2312
|
+
if (reauthentication !== void 0 && (typeof reauthentication !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(reauthentication))) throw invalidAuthRequirements("reauthentication must be a declared concrete purpose.");
|
|
2297
2313
|
return Object.freeze({
|
|
2298
2314
|
linked: options.linked === true,
|
|
2299
2315
|
credentials: Object.freeze(credentials),
|
|
2300
|
-
scopes: Object.freeze(scopes)
|
|
2316
|
+
scopes: Object.freeze(scopes),
|
|
2317
|
+
reauthentication: reauthentication ?? null
|
|
2301
2318
|
});
|
|
2302
2319
|
}
|
|
2303
2320
|
function normalizeCapsuleAuthDefinition(definition) {
|
|
@@ -2323,10 +2340,17 @@ function normalizeCapsuleAuthDefinition(definition) {
|
|
|
2323
2340
|
}
|
|
2324
2341
|
if (Object.hasOwn(normalized, "auth") && normalized.auth !== void 0) {
|
|
2325
2342
|
const registration = normalized.auth?.registration;
|
|
2326
|
-
|
|
2343
|
+
const reauthentication = normalized.auth?.reauthentication;
|
|
2344
|
+
if (!isPlainObject(normalized.auth) || Object.keys(normalized.auth).some((key) => !["registration", "reauthentication"].includes(key)) || registration !== void 0 && (!isPlainObject(registration) || typeof registration.admit !== "function" || typeof registration.finalize !== "function" || Object.keys(registration).some((key) => key !== "admit" && key !== "finalize"))) {
|
|
2327
2345
|
throw commandError("Invalid Capsule Registration Admission declaration.", "Declare auth.registration with both admit and finalize server functions.", "INVALID_REGISTRATION_ADMISSION");
|
|
2328
2346
|
}
|
|
2329
|
-
|
|
2347
|
+
if (reauthentication !== void 0 && (!isPlainObject(reauthentication) || Object.keys(reauthentication).some((key) => !["purposes", "authorize"].includes(key)) || reauthentication.authorize !== void 0 && typeof reauthentication.authorize !== "function" || !isPlainObject(reauthentication.purposes) || Object.keys(reauthentication.purposes).length < 1 || Object.entries(reauthentication.purposes).some(([purpose, policy]) => !/^[a-z][a-z0-9-]{0,63}$/.test(purpose) || !isPlainObject(policy) || Object.keys(policy).some((key) => key !== "maxAgeSeconds") || !Number.isInteger(policy.maxAgeSeconds) || policy.maxAgeSeconds < 1 || policy.maxAgeSeconds > 900))) throw commandError("Invalid Reauthentication declaration.", "Declare concrete purposes with maxAgeSeconds from 1 through 900 and an optional authorize callback.", "INVALID_REAUTHENTICATION_DECLARATION");
|
|
2348
|
+
normalized = { ...normalized, auth: Object.freeze({ ...registration ? { registration: Object.freeze(registration) } : {}, ...reauthentication ? { reauthentication: Object.freeze({ purposes: Object.freeze(Object.fromEntries(Object.entries(reauthentication.purposes).map(([purpose, policy]) => [purpose, Object.freeze({ maxAgeSeconds: policy.maxAgeSeconds })]))), ...reauthentication.authorize ? { authorize: reauthentication.authorize } : {} }) } : {} }) };
|
|
2349
|
+
}
|
|
2350
|
+
const declaredPurposes = new Set(Object.keys(normalized.auth?.reauthentication?.purposes ?? {}));
|
|
2351
|
+
for (const item of Object.values(normalized.mutations ?? {})) {
|
|
2352
|
+
const purpose = readAuthRequirements(item?.handler)?.reauthentication;
|
|
2353
|
+
if (purpose && !declaredPurposes.has(purpose)) throw invalidAuthRequirements("Every reauthentication purpose must be declared by the Capsule.");
|
|
2330
2354
|
}
|
|
2331
2355
|
return normalizeFileAccessKeyPolicy(normalized);
|
|
2332
2356
|
}
|
|
@@ -2373,12 +2397,15 @@ function normalizeFileAccessKeyPolicy(definition) {
|
|
|
2373
2397
|
}
|
|
2374
2398
|
function validateCapsuleAuthRequirements(definition) {
|
|
2375
2399
|
const declaredScopes = new Set(definition.accessKeys?.scopes ?? []);
|
|
2376
|
-
for (const collection of [definition.queries, definition.mutations, definition.endpoints, definition.messages]) {
|
|
2400
|
+
for (const [kind, collection] of [["query", definition.queries], ["mutation", definition.mutations], ["endpoint", definition.endpoints], ["message", definition.messages]]) {
|
|
2377
2401
|
for (const item of Object.values(collection ?? {})) {
|
|
2378
2402
|
const requirements = readAuthRequirements(item?.handler);
|
|
2379
2403
|
if (!requirements) {
|
|
2380
2404
|
continue;
|
|
2381
2405
|
}
|
|
2406
|
+
if (requirements.reauthentication && kind !== "mutation") {
|
|
2407
|
+
throw invalidAuthRequirements("Reauthentication proofs may only guard mutations.");
|
|
2408
|
+
}
|
|
2382
2409
|
for (const scope of requirements.scopes) {
|
|
2383
2410
|
if (!declaredScopes.has(scope)) {
|
|
2384
2411
|
throw invalidAuthRequirements("Every required scope must be declared in capsule({ accessKeys: { scopes } }).");
|
|
@@ -2479,8 +2506,8 @@ function normalizeUserAuthOptions(options = {}) {
|
|
|
2479
2506
|
}
|
|
2480
2507
|
|
|
2481
2508
|
function normalizeGuardOptions(options = {}) {
|
|
2482
|
-
if (!plainObject(options) || Object.keys(options).some((key) => !["linked", "credentials", "scopes"].includes(key))) {
|
|
2483
|
-
throw authRequirementsError("Use only linked, credentials, and
|
|
2509
|
+
if (!plainObject(options) || Object.keys(options).some((key) => !["linked", "credentials", "scopes", "reauthentication"].includes(key))) {
|
|
2510
|
+
throw authRequirementsError("Use only linked, credentials, scopes, and reauthentication in a declarative Auth requirement.");
|
|
2484
2511
|
}
|
|
2485
2512
|
if ("linked" in options && typeof options.linked !== "boolean") throw authRequirementsError("linked must be a boolean when supplied.");
|
|
2486
2513
|
const credentials = options.credentials === undefined ? ["session", "access-key"] : options.credentials;
|
|
@@ -2491,7 +2518,9 @@ function normalizeGuardOptions(options = {}) {
|
|
|
2491
2518
|
if (!Array.isArray(scopes) || (options.scopes !== undefined && scopes.length === 0) || scopes.length > 1024 || scopes.some((scope) => typeof scope !== "string" || scope.length === 0 || scope.includes("*") || new TextEncoder().encode(scope).byteLength > 256) || new Set(scopes).size !== scopes.length) {
|
|
2492
2519
|
throw authRequirementsError("Required scopes must be unique concrete strings declared by the Capsule.");
|
|
2493
2520
|
}
|
|
2494
|
-
|
|
2521
|
+
const reauthentication = options.reauthentication;
|
|
2522
|
+
if (reauthentication !== undefined && (typeof reauthentication !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(reauthentication))) throw authRequirementsError("reauthentication must be a declared concrete purpose.");
|
|
2523
|
+
return Object.freeze({ linked: options.linked === true, credentials: Object.freeze([...credentials]), scopes: Object.freeze([...scopes]), reauthentication: reauthentication ?? null });
|
|
2495
2524
|
}
|
|
2496
2525
|
|
|
2497
2526
|
function decorateAuth(options, handler) {
|
|
@@ -12767,6 +12796,7 @@ function createGoogleOAuthProviderAdapter(database) {
|
|
|
12767
12796
|
provider: "google",
|
|
12768
12797
|
responseMode: "query",
|
|
12769
12798
|
enabled: configured,
|
|
12799
|
+
reauthenticationFreshness: "signed-auth-time",
|
|
12770
12800
|
begin(context) {
|
|
12771
12801
|
const clientId = database.serverEnv[google.clientIdEnv];
|
|
12772
12802
|
const params = new URLSearchParams({
|
|
@@ -12779,6 +12809,10 @@ function createGoogleOAuthProviderAdapter(database) {
|
|
|
12779
12809
|
code_challenge: context.pkceChallenge,
|
|
12780
12810
|
code_challenge_method: "S256"
|
|
12781
12811
|
});
|
|
12812
|
+
if (context.reauthentication) {
|
|
12813
|
+
params.set("max_age", "0");
|
|
12814
|
+
params.set("claims", JSON.stringify({ id_token: { auth_time: { essential: true } } }));
|
|
12815
|
+
}
|
|
12782
12816
|
return { url: `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}` };
|
|
12783
12817
|
},
|
|
12784
12818
|
complete(context) {
|
|
@@ -12814,6 +12848,7 @@ function createAppleOAuthProviderAdapter(database) {
|
|
|
12814
12848
|
provider: "apple",
|
|
12815
12849
|
responseMode: "form_post",
|
|
12816
12850
|
enabled: configured,
|
|
12851
|
+
reauthenticationFreshness: null,
|
|
12817
12852
|
begin(context) {
|
|
12818
12853
|
if (!appleOAuthOriginEligible(new URL(context.redirectUri).origin)) {
|
|
12819
12854
|
throw commandError(
|
|
@@ -12831,6 +12866,7 @@ function createAppleOAuthProviderAdapter(database) {
|
|
|
12831
12866
|
state: context.state,
|
|
12832
12867
|
nonce: context.nonce
|
|
12833
12868
|
});
|
|
12869
|
+
if (context.reauthentication) params.set("prompt", "login");
|
|
12834
12870
|
return { url: `https://appleid.apple.com/auth/authorize?${params.toString()}` };
|
|
12835
12871
|
},
|
|
12836
12872
|
complete(context) {
|
|
@@ -12949,6 +12985,7 @@ function createFacebookOAuthProviderAdapter(database) {
|
|
|
12949
12985
|
provider: "facebook",
|
|
12950
12986
|
responseMode: "query",
|
|
12951
12987
|
enabled: configured,
|
|
12988
|
+
reauthenticationFreshness: null,
|
|
12952
12989
|
begin(context) {
|
|
12953
12990
|
const clientId = database.serverEnv[facebook.clientIdEnv];
|
|
12954
12991
|
if (typeof clientId !== "string" || clientId.length < 1 || clientId.length > 4096) {
|
|
@@ -12965,6 +13002,7 @@ function createFacebookOAuthProviderAdapter(database) {
|
|
|
12965
13002
|
scope: "public_profile,email",
|
|
12966
13003
|
state: context.state
|
|
12967
13004
|
});
|
|
13005
|
+
if (context.reauthentication) params.set("auth_type", "reauthorize");
|
|
12968
13006
|
const authorizationUrl = facebookOAuthEndpoint(
|
|
12969
13007
|
process.env.SPORADES_FACEBOOK_AUTH_URL,
|
|
12970
13008
|
`https://www.facebook.com/${graphVersion}/dialog/oauth`
|
|
@@ -13303,7 +13341,8 @@ async function verifyGoogleIdentityToken(database, token, expectedNonce) {
|
|
|
13303
13341
|
email: normalizeSimulatedText(claims.email)?.toLowerCase() ?? null,
|
|
13304
13342
|
emailVerified: claims.email_verified === true,
|
|
13305
13343
|
displayName: normalizeSimulatedText(claims.name) ?? normalizeSimulatedText(claims.email) ?? "Google user",
|
|
13306
|
-
picture: normalizeSimulatedText(claims.picture)
|
|
13344
|
+
picture: normalizeSimulatedText(claims.picture),
|
|
13345
|
+
reauthenticatedAt: Number.isSafeInteger(claims.auth_time) && claims.auth_time >= 0 ? new Date(claims.auth_time * 1e3).toISOString() : null
|
|
13307
13346
|
};
|
|
13308
13347
|
}
|
|
13309
13348
|
function createMicrosoftOAuthProviderAdapter(database) {
|
|
@@ -13313,6 +13352,7 @@ function createMicrosoftOAuthProviderAdapter(database) {
|
|
|
13313
13352
|
provider: "microsoft",
|
|
13314
13353
|
responseMode: "query",
|
|
13315
13354
|
enabled: configured,
|
|
13355
|
+
reauthenticationFreshness: "signed-auth-time",
|
|
13316
13356
|
async begin(context) {
|
|
13317
13357
|
const discovery = await discoverMicrosoftOpenIdConfiguration(database, microsoft.tenant);
|
|
13318
13358
|
const clientId = database.serverEnv[microsoft.clientIdEnv];
|
|
@@ -13327,6 +13367,10 @@ function createMicrosoftOAuthProviderAdapter(database) {
|
|
|
13327
13367
|
code_challenge: context.pkceChallenge,
|
|
13328
13368
|
code_challenge_method: "S256"
|
|
13329
13369
|
});
|
|
13370
|
+
if (context.reauthentication) {
|
|
13371
|
+
params.set("prompt", "login");
|
|
13372
|
+
params.set("max_age", "0");
|
|
13373
|
+
}
|
|
13330
13374
|
return { url: `${discovery.authorization_endpoint}?${params.toString()}` };
|
|
13331
13375
|
},
|
|
13332
13376
|
complete(context) {
|
|
@@ -13590,7 +13634,7 @@ async function verifyMicrosoftIdentityToken(database, token, expectedNonce, disc
|
|
|
13590
13634
|
const numericDate = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
13591
13635
|
const optionalNumericDate = (value) => value === void 0 || numericDate(value);
|
|
13592
13636
|
const optionalProfile = (value, max) => value === void 0 || value === null || typeof value === "string" && value.length <= max;
|
|
13593
|
-
const structurallyValid = header2.alg === "RS256" && visible(header2.kid, 255) && visible(claims.iss, 2048) && validAudience && numericDate(claims.exp) && optionalNumericDate(claims.nbf) && optionalNumericDate(claims.iat) && visible(claims.nonce, 512) && typeof claims.tid === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(claims.tid) && visible(claims.sub, 255) && optionalProfile(claims.email, 1024) && optionalProfile(claims.name, 1024) && optionalProfile(claims.preferred_username, 1024);
|
|
13637
|
+
const structurallyValid = header2.alg === "RS256" && visible(header2.kid, 255) && visible(claims.iss, 2048) && validAudience && numericDate(claims.exp) && optionalNumericDate(claims.nbf) && optionalNumericDate(claims.iat) && optionalNumericDate(claims.auth_time) && visible(claims.nonce, 512) && typeof claims.tid === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(claims.tid) && visible(claims.sub, 255) && optionalProfile(claims.email, 1024) && optionalProfile(claims.name, 1024) && optionalProfile(claims.preferred_username, 1024);
|
|
13594
13638
|
if (!structurallyValid) {
|
|
13595
13639
|
throw commandError("Microsoft identity token was invalid.", "Retry Microsoft sign-in.", "OAUTH_ID_TOKEN_INVALID");
|
|
13596
13640
|
}
|
|
@@ -13629,7 +13673,8 @@ async function verifyMicrosoftIdentityToken(database, token, expectedNonce, disc
|
|
|
13629
13673
|
email,
|
|
13630
13674
|
emailVerified: null,
|
|
13631
13675
|
displayName: normalizeSimulatedText(claims.name) ?? normalizeSimulatedText(claims.preferred_username) ?? email ?? "Microsoft user",
|
|
13632
|
-
picture: null
|
|
13676
|
+
picture: null,
|
|
13677
|
+
reauthenticatedAt: Number.isSafeInteger(claims.auth_time) && claims.auth_time >= 0 ? new Date(claims.auth_time * 1e3).toISOString() : null
|
|
13633
13678
|
};
|
|
13634
13679
|
}
|
|
13635
13680
|
async function verifyAppleIdentityToken(database, token, expectedNonce) {
|
|
@@ -14377,7 +14422,7 @@ async function resolveAnonymousSession(database, sessionToken) {
|
|
|
14377
14422
|
const existing = await database.adapter.readAuthSessionWithUser(sessionToken);
|
|
14378
14423
|
if (existing) {
|
|
14379
14424
|
if (isExpiredSession(existing)) {
|
|
14380
|
-
await database.adapter.deleteAuthSession(sessionToken);
|
|
14425
|
+
await database.adapter.withTransaction((tx) => tx.deleteAuthSession(sessionToken));
|
|
14381
14426
|
} else {
|
|
14382
14427
|
return sessionFromRow(existing);
|
|
14383
14428
|
}
|
|
@@ -14933,8 +14978,38 @@ async function routeSporadesAuth(database, request, response) {
|
|
|
14933
14978
|
redirectUri: stateRow.redirectUri,
|
|
14934
14979
|
nonce: stateRow.nonce,
|
|
14935
14980
|
pkceVerifier: stateRow.pkceVerifier,
|
|
14936
|
-
parameters
|
|
14981
|
+
parameters,
|
|
14982
|
+
reauthentication: Boolean(stateRow.reauthPurpose)
|
|
14937
14983
|
});
|
|
14984
|
+
if (stateRow.reauthPurpose) {
|
|
14985
|
+
const subject = normalizeSimulatedText(profile?.subject ?? profile?.sub);
|
|
14986
|
+
const policy = database.reauthenticationPolicy?.[stateRow.reauthPurpose];
|
|
14987
|
+
const now2 = database.clock.now();
|
|
14988
|
+
const authenticatedAt = typeof profile?.reauthenticatedAt === "string" ? Date.parse(profile.reauthenticatedAt) : NaN;
|
|
14989
|
+
const flowStartedAt = Date.parse(stateRow.createdAt);
|
|
14990
|
+
const freshnessVerified = adapter.reauthenticationFreshness === "signed-auth-time" && Number.isFinite(authenticatedAt) && Number.isFinite(flowStartedAt) && authenticatedAt >= flowStartedAt - 1e3 && authenticatedAt <= now2.getTime() + 6e4;
|
|
14991
|
+
let authorized = false;
|
|
14992
|
+
try {
|
|
14993
|
+
authorized = await database.adapter.withTransaction(async (tx) => {
|
|
14994
|
+
const current = await tx.readAuthSessionWithUser(stateRow.sessionToken);
|
|
14995
|
+
const identity = subject ? await tx.findAuthIdentityByProviderSubject(provider, subject) : null;
|
|
14996
|
+
if (!freshnessVerified || !policy || !current || current.token !== stateRow.sessionToken || current.userId !== stateRow.reauthUserId || !current.isAuthenticated || current.isGuest || Date.parse(current.expiresAt) <= now2.getTime() || identity?.userId !== current.userId) return false;
|
|
14997
|
+
const currentAuth = sessionFromRow(current).auth;
|
|
14998
|
+
if (!await database.authorizeReauthentication(tx, currentAuth, stateRow.reauthPurpose)) return false;
|
|
14999
|
+
await tx.replaceReauthenticationProof({ id: nodeCryptoModule3.randomUUID(), userId: current.userId, sessionToken: stateRow.sessionToken, purpose: stateRow.reauthPurpose, createdAt: now2.toISOString(), expiresAt: new Date(now2.getTime() + policy.maxAgeSeconds * 1e3).toISOString() });
|
|
15000
|
+
return true;
|
|
15001
|
+
});
|
|
15002
|
+
} catch {
|
|
15003
|
+
try {
|
|
15004
|
+
await database.log?.emit?.({ category: "platform", event: "auth.reauthentication.authorization_failed", level: "error", message: "Reauthentication authorization policy failed.", data: { provider, purpose: stateRow.reauthPurpose } });
|
|
15005
|
+
} catch {
|
|
15006
|
+
}
|
|
15007
|
+
throw commandError("Reauthentication failed.", "Verify the current linked identity and retry.", "REAUTHENTICATION_FAILED");
|
|
15008
|
+
}
|
|
15009
|
+
if (!authorized) throw commandError("Reauthentication failed.", "Verify the current linked identity and retry.", "REAUTHENTICATION_FAILED");
|
|
15010
|
+
writeRedirect(response, stateRow.returnTo);
|
|
15011
|
+
return true;
|
|
15012
|
+
}
|
|
14938
15013
|
const session = await resolveAnonymousSession(database, stateRow.sessionToken);
|
|
14939
15014
|
let result;
|
|
14940
15015
|
try {
|
|
@@ -15023,6 +15098,8 @@ async function beginOAuthSignIn(database, session, provider, options) {
|
|
|
15023
15098
|
const admission = boundedRegistrationInput(options.registration?.admission);
|
|
15024
15099
|
if (admission === invalidRegistrationAdmission) return registrationDenied();
|
|
15025
15100
|
const registrationCiphertext = admission === void 0 ? null : await sealOAuthRegistration(database, admission, { provider, sessionToken: session.token, redirectUri, nonce, expiresAt });
|
|
15101
|
+
const reauthentication = options.reauthentication;
|
|
15102
|
+
if (reauthentication?.purpose && adapter.reauthenticationFreshness !== "signed-auth-time") return { ok: false, error: { code: "REAUTHENTICATION_FAILED", message: "Reauthentication failed.", hint: "Verify the current linked identity and retry." } };
|
|
15026
15103
|
let started;
|
|
15027
15104
|
try {
|
|
15028
15105
|
started = await adapter.begin({
|
|
@@ -15031,7 +15108,8 @@ async function beginOAuthSignIn(database, session, provider, options) {
|
|
|
15031
15108
|
nonce,
|
|
15032
15109
|
redirectUri,
|
|
15033
15110
|
pkceChallenge,
|
|
15034
|
-
pkceChallengeMethod: "S256"
|
|
15111
|
+
pkceChallengeMethod: "S256",
|
|
15112
|
+
reauthentication: Boolean(reauthentication?.purpose)
|
|
15035
15113
|
});
|
|
15036
15114
|
} catch {
|
|
15037
15115
|
return {
|
|
@@ -15063,7 +15141,9 @@ async function beginOAuthSignIn(database, session, provider, options) {
|
|
|
15063
15141
|
expiresAt,
|
|
15064
15142
|
nonce,
|
|
15065
15143
|
pkceVerifier,
|
|
15066
|
-
registrationCiphertext
|
|
15144
|
+
registrationCiphertext,
|
|
15145
|
+
reauthPurpose: reauthentication?.purpose ?? null,
|
|
15146
|
+
reauthUserId: reauthentication?.userId ?? null
|
|
15067
15147
|
});
|
|
15068
15148
|
return { ok: true, url: started.url };
|
|
15069
15149
|
}
|
|
@@ -15248,6 +15328,13 @@ function createAnonymousAuthTables(sqlite, _authConfig = null) {
|
|
|
15248
15328
|
),
|
|
15249
15329
|
() => ensureSessionLifecycleColumns(sqlite),
|
|
15250
15330
|
() => ensureSessionProvenanceColumn(sqlite),
|
|
15331
|
+
() => sqlite.exec(sql(
|
|
15332
|
+
"CREATE TABLE IF NOT EXISTS [sporades_auth_reauthentication_proofs] ([id] TEXT PRIMARY KEY, [userId] TEXT NOT NULL, [sessionToken] TEXT NOT NULL, [purpose] TEXT NOT NULL, [createdAt] TEXT NOT NULL, [expiresAt] TEXT NOT NULL, UNIQUE ([sessionToken], [purpose]))"
|
|
15333
|
+
)),
|
|
15334
|
+
() => sqlite.exec(sql("CREATE TABLE IF NOT EXISTS [sporades_auth_reauthentication_throttle_fence] ([id] TEXT PRIMARY KEY, [version] INTEGER NOT NULL)")),
|
|
15335
|
+
() => sqlite.prepare(sql("INSERT INTO [sporades_auth_reauthentication_throttle_fence] ([id], [version]) VALUES ('email', 0) ON CONFLICT ([id]) DO NOTHING")).run(),
|
|
15336
|
+
() => sqlite.exec(sql("CREATE TABLE IF NOT EXISTS [sporades_auth_reauthentication_throttle] ([key] TEXT PRIMARY KEY, [count] INTEGER NOT NULL, [resetAt] TEXT NOT NULL)")),
|
|
15337
|
+
() => sqlite.prepare(sql("DELETE FROM [sporades_auth_reauthentication_proofs] WHERE [expiresAt] <= ? OR NOT EXISTS (SELECT 1 FROM [sporades_auth_sessions] [s] WHERE [s].[token] = [sporades_auth_reauthentication_proofs].[sessionToken])")).run((/* @__PURE__ */ new Date()).toISOString()),
|
|
15251
15338
|
() => createProviderIdentityTables(sqlite),
|
|
15252
15339
|
() => sqlite.exec(
|
|
15253
15340
|
sql(
|
|
@@ -15261,7 +15348,7 @@ function createAnonymousAuthTables(sqlite, _authConfig = null) {
|
|
|
15261
15348
|
),
|
|
15262
15349
|
() => sqlite.exec(
|
|
15263
15350
|
sql(
|
|
15264
|
-
"CREATE TABLE IF NOT EXISTS [sporades_auth_oauth_states] ([state] TEXT PRIMARY KEY, [provider] TEXT NOT NULL, [sessionToken] TEXT NOT NULL, [returnTo] TEXT NOT NULL, [redirectUri] TEXT NOT NULL, [createdAt] TEXT NOT NULL, [expiresAt] TEXT NOT NULL, [nonce] TEXT, [pkceVerifier] TEXT, [registrationCiphertext] TEXT)"
|
|
15351
|
+
"CREATE TABLE IF NOT EXISTS [sporades_auth_oauth_states] ([state] TEXT PRIMARY KEY, [provider] TEXT NOT NULL, [sessionToken] TEXT NOT NULL, [returnTo] TEXT NOT NULL, [redirectUri] TEXT NOT NULL, [createdAt] TEXT NOT NULL, [expiresAt] TEXT NOT NULL, [nonce] TEXT, [pkceVerifier] TEXT, [registrationCiphertext] TEXT, [reauthPurpose] TEXT, [reauthUserId] TEXT)"
|
|
15265
15352
|
)
|
|
15266
15353
|
),
|
|
15267
15354
|
() => ensureOAuthStateColumns(sqlite)
|
|
@@ -15275,7 +15362,9 @@ function ensureOAuthStateColumns(sqlite) {
|
|
|
15275
15362
|
["expiresAt", "TEXT"],
|
|
15276
15363
|
["nonce", "TEXT"],
|
|
15277
15364
|
["pkceVerifier", "TEXT"],
|
|
15278
|
-
["registrationCiphertext", "TEXT"]
|
|
15365
|
+
["registrationCiphertext", "TEXT"],
|
|
15366
|
+
["reauthPurpose", "TEXT"],
|
|
15367
|
+
["reauthUserId", "TEXT"]
|
|
15279
15368
|
].map(([name, type]) => () => sqlite.dialect.addMissingColumn(sqlite, "sporades_auth_oauth_states", name, type)),
|
|
15280
15369
|
() => sqlite.exec(sql("UPDATE [sporades_auth_oauth_states] SET [provider] = 'google' WHERE [provider] IS NULL")),
|
|
15281
15370
|
() => sqlite.exec(sql("UPDATE [sporades_auth_oauth_states] SET [expiresAt] = [createdAt] WHERE [expiresAt] IS NULL"))
|
|
@@ -18646,7 +18735,7 @@ function createSharedDatabaseAdapterMethods(dialect) {
|
|
|
18646
18735
|
);
|
|
18647
18736
|
},
|
|
18648
18737
|
deleteAuthSession(token) {
|
|
18649
|
-
return this.prepare(sql("DELETE FROM [sporades_auth_sessions] WHERE [token] = ?")).run(token);
|
|
18738
|
+
return thenIfPromise(this.prepare(sql("DELETE FROM [sporades_auth_reauthentication_proofs] WHERE [sessionToken] = ?")).run(token), () => this.prepare(sql("DELETE FROM [sporades_auth_sessions] WHERE [token] = ?")).run(token));
|
|
18650
18739
|
},
|
|
18651
18740
|
refreshAuthSession(token, expiresAt) {
|
|
18652
18741
|
return this.prepare(sql("UPDATE [sporades_auth_sessions] SET [expiresAt] = ? WHERE [token] = ?")).run(expiresAt, token);
|
|
@@ -18656,7 +18745,7 @@ function createSharedDatabaseAdapterMethods(dialect) {
|
|
|
18656
18745
|
},
|
|
18657
18746
|
rotateAuthSession(previousToken, row) {
|
|
18658
18747
|
assertNotReservedAuthUserId(row.userId);
|
|
18659
|
-
|
|
18748
|
+
const rotated = this.prepare(
|
|
18660
18749
|
sql(
|
|
18661
18750
|
"UPDATE [sporades_auth_sessions] SET [token] = ?, [userId] = ?, [provider] = ?, [createdAt] = ?, [expiresAt] = ? WHERE [token] = ?"
|
|
18662
18751
|
)
|
|
@@ -18668,6 +18757,10 @@ function createSharedDatabaseAdapterMethods(dialect) {
|
|
|
18668
18757
|
row.expiresAt,
|
|
18669
18758
|
previousToken
|
|
18670
18759
|
);
|
|
18760
|
+
return thenIfPromise(rotated, (result) => {
|
|
18761
|
+
if (result.changes !== 1 || previousToken === row.token) return result;
|
|
18762
|
+
return thenIfPromise(this.prepare(sql("DELETE FROM [sporades_auth_reauthentication_proofs] WHERE [sessionToken] = ?")).run(previousToken), () => result);
|
|
18763
|
+
});
|
|
18671
18764
|
},
|
|
18672
18765
|
readAuthSessionWithUser(token) {
|
|
18673
18766
|
return thenIfPromise(
|
|
@@ -18684,9 +18777,9 @@ function createSharedDatabaseAdapterMethods(dialect) {
|
|
|
18684
18777
|
const expiresAt = row.expiresAt ?? new Date(Date.parse(row.createdAt) + 10 * 60 * 1e3).toISOString();
|
|
18685
18778
|
return this.prepare(
|
|
18686
18779
|
sql(
|
|
18687
|
-
"INSERT INTO [sporades_auth_oauth_states] ([state], [provider], [sessionToken], [returnTo], [redirectUri], [createdAt], [expiresAt], [nonce], [pkceVerifier], [registrationCiphertext]) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
18780
|
+
"INSERT INTO [sporades_auth_oauth_states] ([state], [provider], [sessionToken], [returnTo], [redirectUri], [createdAt], [expiresAt], [nonce], [pkceVerifier], [registrationCiphertext], [reauthPurpose], [reauthUserId]) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
18688
18781
|
)
|
|
18689
|
-
).run(row.state, provider, row.sessionToken, row.returnTo, row.redirectUri, row.createdAt, expiresAt, row.nonce ?? null, row.pkceVerifier ?? null, row.registrationCiphertext ?? null);
|
|
18782
|
+
).run(row.state, provider, row.sessionToken, row.returnTo, row.redirectUri, row.createdAt, expiresAt, row.nonce ?? null, row.pkceVerifier ?? null, row.registrationCiphertext ?? null, row.reauthPurpose ?? null, row.reauthUserId ?? null);
|
|
18690
18783
|
},
|
|
18691
18784
|
// One statement, not a SELECT followed by a DELETE. The two-statement form was correct on
|
|
18692
18785
|
// SQLite and a race everywhere else: nothing ordered the delete after the read, so on an
|
|
@@ -18697,7 +18790,7 @@ function createSharedDatabaseAdapterMethods(dialect) {
|
|
|
18697
18790
|
return thenIfPromise(
|
|
18698
18791
|
this.prepare(
|
|
18699
18792
|
sql(
|
|
18700
|
-
"DELETE FROM [sporades_auth_oauth_states] WHERE [state] = ? RETURNING [state], [provider], [sessionToken], [returnTo], [redirectUri], [createdAt], [expiresAt], [nonce], [pkceVerifier], [registrationCiphertext]"
|
|
18793
|
+
"DELETE FROM [sporades_auth_oauth_states] WHERE [state] = ? RETURNING [state], [provider], [sessionToken], [returnTo], [redirectUri], [createdAt], [expiresAt], [nonce], [pkceVerifier], [registrationCiphertext], [reauthPurpose], [reauthUserId]"
|
|
18701
18794
|
)
|
|
18702
18795
|
).get(state),
|
|
18703
18796
|
(row) => row ?? null
|
|
@@ -18732,8 +18825,45 @@ function createSharedDatabaseAdapterMethods(dialect) {
|
|
|
18732
18825
|
(row) => isReservedAuthUserId(row?.userId) ? null : row ?? null
|
|
18733
18826
|
);
|
|
18734
18827
|
},
|
|
18828
|
+
replaceReauthenticationProof(row) {
|
|
18829
|
+
return chainMaybePromise([
|
|
18830
|
+
() => this.prepare(sql("DELETE FROM [sporades_auth_reauthentication_proofs] WHERE [sessionToken] = ? AND [purpose] = ?")).run(row.sessionToken, row.purpose),
|
|
18831
|
+
() => this.prepare(sql("INSERT INTO [sporades_auth_reauthentication_proofs] ([id], [userId], [sessionToken], [purpose], [createdAt], [expiresAt]) VALUES (?, ?, ?, ?, ?, ?)")).run(row.id, row.userId, row.sessionToken, row.purpose, row.createdAt, row.expiresAt)
|
|
18832
|
+
]);
|
|
18833
|
+
},
|
|
18834
|
+
async reserveEmailReauthenticationAttempt(input) {
|
|
18835
|
+
await this.prepare(sql("UPDATE [sporades_auth_reauthentication_throttle_fence] SET [version] = [version] + 1 WHERE [id] = 'email'")).run();
|
|
18836
|
+
await this.prepare(sql("DELETE FROM [sporades_auth_reauthentication_throttle] WHERE [resetAt] <= ?")).run(input.now);
|
|
18837
|
+
const rows = await Promise.all(input.keys.map((key) => this.prepare(sql("SELECT [count], [resetAt] FROM [sporades_auth_reauthentication_throttle] WHERE [key] = ?")).get(key)));
|
|
18838
|
+
if (rows.some((row) => row && row.count >= input.limit)) return false;
|
|
18839
|
+
const active = await this.prepare(sql("SELECT COUNT(*) AS [count] FROM [sporades_auth_reauthentication_throttle]")).get();
|
|
18840
|
+
const missing = rows.filter((row) => !row).length;
|
|
18841
|
+
if (Number(active?.count ?? 0) + missing > input.maxEntries) return false;
|
|
18842
|
+
for (let index = 0; index < input.keys.length; index += 1) {
|
|
18843
|
+
const key = input.keys[index];
|
|
18844
|
+
const row = rows[index];
|
|
18845
|
+
if (row) await this.prepare(sql("UPDATE [sporades_auth_reauthentication_throttle] SET [count] = [count] + 1 WHERE [key] = ?")).run(key);
|
|
18846
|
+
else await this.prepare(sql("INSERT INTO [sporades_auth_reauthentication_throttle] ([key], [count], [resetAt]) VALUES (?, 1, ?)")).run(key, input.resetAt);
|
|
18847
|
+
}
|
|
18848
|
+
return true;
|
|
18849
|
+
},
|
|
18850
|
+
async clearEmailReauthenticationAttempts(keys) {
|
|
18851
|
+
for (const key of keys) await this.prepare(sql("DELETE FROM [sporades_auth_reauthentication_throttle] WHERE [key] = ?")).run(key);
|
|
18852
|
+
},
|
|
18853
|
+
claimEmailCredentialVersion(email, passwordHash, passwordSalt) {
|
|
18854
|
+
return thenIfPromise(this.prepare(sql("UPDATE [sporades_auth_email_credentials] SET [passwordHash] = [passwordHash] WHERE [email] = ? AND [passwordHash] = ? AND [passwordSalt] = ?")).run(email, passwordHash, passwordSalt), (result) => result.changes === 1);
|
|
18855
|
+
},
|
|
18856
|
+
deleteExpiredReauthenticationProofs(now2) {
|
|
18857
|
+
return this.prepare(sql("DELETE FROM [sporades_auth_reauthentication_proofs] WHERE [expiresAt] <= ?")).run(now2);
|
|
18858
|
+
},
|
|
18859
|
+
consumeReauthenticationProof(input) {
|
|
18860
|
+
return thenIfPromise(this.prepare(sql("SELECT [p].[id] FROM [sporades_auth_reauthentication_proofs] [p] JOIN [sporades_auth_sessions] [s] ON [s].[token] = [p].[sessionToken] JOIN [sporades_auth_users] [u] ON [u].[id] = [s].[userId] WHERE [p].[sessionToken] = ? AND [p].[userId] = ? AND [p].[purpose] = ? AND [p].[expiresAt] > ? AND [s].[userId] = ? AND [s].[expiresAt] > ? AND [u].[isAuthenticated] = 1 AND [u].[isGuest] = 0")).get(input.sessionToken, input.userId, input.purpose, input.now, input.userId, input.now), (row) => {
|
|
18861
|
+
if (!row) return false;
|
|
18862
|
+
return thenIfPromise(this.prepare(sql("DELETE FROM [sporades_auth_reauthentication_proofs] WHERE [id] = ?")).run(row.id), (result) => result.changes === 1);
|
|
18863
|
+
});
|
|
18864
|
+
},
|
|
18735
18865
|
deleteAuthSessionsForUser(userId) {
|
|
18736
|
-
return this.prepare(sql("DELETE FROM [sporades_auth_sessions] WHERE [userId] = ?")).run(userId);
|
|
18866
|
+
return thenIfPromise(this.prepare(sql("DELETE FROM [sporades_auth_reauthentication_proofs] WHERE [userId] = ?")).run(userId), () => this.prepare(sql("DELETE FROM [sporades_auth_sessions] WHERE [userId] = ?")).run(userId));
|
|
18737
18867
|
},
|
|
18738
18868
|
insertPasswordResetCode(row) {
|
|
18739
18869
|
assertNotReservedAuthUserId(row.userId);
|
|
@@ -20991,6 +21121,23 @@ async function openDevDatabase(databasePath, serverSource, serverEnv = {}, confi
|
|
|
20991
21121
|
scheduleTeamBillingJobDispatch: () => scheduleCurrentUserJobWorker(database),
|
|
20992
21122
|
mail,
|
|
20993
21123
|
authConfig: authStatus2(config, serverEnv),
|
|
21124
|
+
reauthenticationPolicy: capsuleDefinition?.auth?.reauthentication?.purposes ?? null,
|
|
21125
|
+
authorizeReauthentication: typeof capsuleDefinition?.auth?.reauthentication?.authorize === "function" ? async function(transaction, auth, purpose) {
|
|
21126
|
+
const reauthDatabase = createTransactionDatabase(database, transaction);
|
|
21127
|
+
let active = true;
|
|
21128
|
+
const assertActive = () => {
|
|
21129
|
+
if (!active) throw commandError("Reauthentication access is no longer active.", "Start a new reauthentication attempt.", "REAUTHENTICATION_ACCESS_INACTIVE");
|
|
21130
|
+
};
|
|
21131
|
+
const reauthContext = { purpose: "auth.reauthentication", auth };
|
|
21132
|
+
grantPrivilegedDbAccess(reauthContext);
|
|
21133
|
+
const holder = createContextHolder(reauthContext);
|
|
21134
|
+
try {
|
|
21135
|
+
return await capsuleDefinition.auth.reauthentication.authorize({ db: createEndpointReadOnlyDatabaseApi(reauthDatabase, () => holder.current, assertActive), auth }, purpose) === true;
|
|
21136
|
+
} finally {
|
|
21137
|
+
active = false;
|
|
21138
|
+
revokePrivilegedDbAccess(reauthContext);
|
|
21139
|
+
}
|
|
21140
|
+
} : async () => true,
|
|
20994
21141
|
passwordResetConfig: resolvePasswordResetConfig(config),
|
|
20995
21142
|
teamJoinLinkConfig: resolveTeamJoinLinkConfig(config),
|
|
20996
21143
|
teamApplicationRoles,
|
|
@@ -24492,6 +24639,63 @@ function createWebSocketHub(getDatabase, trustedRefresh = null) {
|
|
|
24492
24639
|
});
|
|
24493
24640
|
return;
|
|
24494
24641
|
}
|
|
24642
|
+
if (message.type === "auth.reauthenticate") {
|
|
24643
|
+
const purpose = message.purpose;
|
|
24644
|
+
const policy = database.reauthenticationPolicy?.[purpose];
|
|
24645
|
+
const normalized = normalizeEmailCredentials(message.credentials ?? {});
|
|
24646
|
+
let ok = false;
|
|
24647
|
+
let expiresAt = null;
|
|
24648
|
+
const authorized = Boolean(policy && client.session.auth?.isAuthenticated && !client.session.auth?.isGuest);
|
|
24649
|
+
const emailProviderEnabled = database.authConfig.providers.email?.enabled === true;
|
|
24650
|
+
if (authorized && message.provider === "email" && emailProviderEnabled && normalized.ok && typeof normalized.password === "string") {
|
|
24651
|
+
const reauthenticationThrottleKeys = [
|
|
24652
|
+
`email:${createHash8("sha256").update(normalized.email).digest("base64url")}`,
|
|
24653
|
+
`session:${createHash8("sha256").update(client.session.token).digest("base64url")}`
|
|
24654
|
+
];
|
|
24655
|
+
const throttleNow = database.clock.now();
|
|
24656
|
+
let reserved = false;
|
|
24657
|
+
try {
|
|
24658
|
+
reserved = await database.adapter.withTransaction((tx) => tx.reserveEmailReauthenticationAttempt({ keys: reauthenticationThrottleKeys, now: throttleNow.toISOString(), resetAt: new Date(throttleNow.getTime() + EMAIL_SIGN_IN_THROTTLE_WINDOW_MS).toISOString(), limit: EMAIL_SIGN_IN_FAILURE_LIMIT, maxEntries: EMAIL_SIGN_IN_THROTTLE_MAX_ENTRIES }));
|
|
24659
|
+
} catch {
|
|
24660
|
+
try {
|
|
24661
|
+
await database.log?.emit?.({ category: "platform", event: "auth.reauthentication.throttle_failed", level: "error", message: "Reauthentication attempt reservation failed.", data: { provider: "email", purpose } });
|
|
24662
|
+
} catch {
|
|
24663
|
+
}
|
|
24664
|
+
}
|
|
24665
|
+
if (reserved) {
|
|
24666
|
+
const now2 = database.clock.now();
|
|
24667
|
+
expiresAt = new Date(now2.getTime() + policy.maxAgeSeconds * 1e3).toISOString();
|
|
24668
|
+
try {
|
|
24669
|
+
await database.adapter.withTransaction(async (tx) => {
|
|
24670
|
+
const current = await tx.readAuthSessionWithUser(client.session.token);
|
|
24671
|
+
const credential = await tx.findEmailCredentialWithUser(normalized.email);
|
|
24672
|
+
if (!current || current.token !== client.session.token || current.userId !== client.session.auth.userId || !current.isAuthenticated || current.isGuest || Date.parse(current.expiresAt) <= now2.getTime() || credential?.userId !== current.userId || !verifyEmailPassword(normalized.password, credential.passwordSalt, credential.passwordHash)) return;
|
|
24673
|
+
const currentAuth = { userId: current.userId, displayName: current.displayName, email: current.email, picture: current.picture, isAuthenticated: Boolean(current.isAuthenticated), isGuest: Boolean(current.isGuest), provider: current.provider };
|
|
24674
|
+
if (!await tx.claimEmailCredentialVersion(normalized.email, credential.passwordHash, credential.passwordSalt)) return;
|
|
24675
|
+
if (!await database.authorizeReauthentication(tx, currentAuth, purpose)) return;
|
|
24676
|
+
await tx.replaceReauthenticationProof({ id: randomUUID7(), userId: current.userId, sessionToken: current.token, purpose, createdAt: now2.toISOString(), expiresAt });
|
|
24677
|
+
ok = true;
|
|
24678
|
+
await tx.clearEmailReauthenticationAttempts(reauthenticationThrottleKeys);
|
|
24679
|
+
});
|
|
24680
|
+
} catch {
|
|
24681
|
+
try {
|
|
24682
|
+
await database.log?.emit?.({ category: "platform", event: "auth.reauthentication.authorization_failed", level: "error", message: "Reauthentication authorization policy failed.", data: { provider: "email", purpose } });
|
|
24683
|
+
} catch {
|
|
24684
|
+
}
|
|
24685
|
+
ok = false;
|
|
24686
|
+
}
|
|
24687
|
+
}
|
|
24688
|
+
}
|
|
24689
|
+
if (!ok && authorized && message.provider !== "email" && oauthProviderAdapter(database, message.provider)?.enabled) {
|
|
24690
|
+
const started = await beginOAuthSignIn(database, client.session, message.provider, { origin: client.origin, returnTo: message.returnTo, reauthentication: { purpose, userId: client.session.auth.userId } });
|
|
24691
|
+
if (started.ok) {
|
|
24692
|
+
sendJson(client, { id: message.id ?? null, type: "auth.redirect", data: { url: started.url }, error: null });
|
|
24693
|
+
return;
|
|
24694
|
+
}
|
|
24695
|
+
}
|
|
24696
|
+
sendJson(client, { id: message.id ?? null, type: ok ? "auth.reauthenticate.result" : "error", data: ok ? { ok: true, purpose, expiresAt } : null, error: ok ? null : { code: "REAUTHENTICATION_FAILED", message: "Reauthentication failed.", hint: "Verify the current linked identity and retry." } });
|
|
24697
|
+
return;
|
|
24698
|
+
}
|
|
24495
24699
|
if (message.type === "auth.signIn" || message.type === "auth.signInWithGoogle") {
|
|
24496
24700
|
const provider = message.type === "auth.signInWithGoogle" ? "google" : message.provider;
|
|
24497
24701
|
if (provider === "email") {
|
|
@@ -25122,7 +25326,7 @@ function createWebSocketHub(getDatabase, trustedRefresh = null) {
|
|
|
25122
25326
|
}
|
|
25123
25327
|
async function signOutSession(database, client) {
|
|
25124
25328
|
try {
|
|
25125
|
-
await database.adapter.deleteAuthSession(client.session.token);
|
|
25329
|
+
await database.adapter.withTransaction((tx) => tx.deleteAuthSession(client.session.token));
|
|
25126
25330
|
client.session = await resolveAnonymousSession(database, null);
|
|
25127
25331
|
return { ok: true };
|
|
25128
25332
|
} catch (error) {
|
|
@@ -25464,6 +25668,12 @@ async function runMutation(database, auth, mutationName, args, options = {}) {
|
|
|
25464
25668
|
let result;
|
|
25465
25669
|
const writeState = { didWrite: false };
|
|
25466
25670
|
try {
|
|
25671
|
+
const declaredHandler = database.mutations.find((candidate) => candidate.name === mutationName);
|
|
25672
|
+
const declaredMutationHandler = declaredHandler ? materializeHandler(declaredHandler) : null;
|
|
25673
|
+
if (readAuthRequirements(declaredMutationHandler)?.reauthentication) {
|
|
25674
|
+
const maintenanceNow = database.clock.now().toISOString();
|
|
25675
|
+
await database.adapter.withTransaction((maintenanceAdapter) => maintenanceAdapter.deleteExpiredReauthenticationProofs(maintenanceNow));
|
|
25676
|
+
}
|
|
25467
25677
|
const committed = await (database.adapter ?? database.adapter).withTransaction(async (transactionAdapter) => {
|
|
25468
25678
|
const transactionDatabase = createTransactionDatabase(database, transactionAdapter, writeState);
|
|
25469
25679
|
let handlerFailed = false;
|
|
@@ -25472,6 +25682,11 @@ async function runMutation(database, auth, mutationName, args, options = {}) {
|
|
|
25472
25682
|
const customHandler = transactionDatabase.mutations.find((candidate) => candidate.name === mutationName);
|
|
25473
25683
|
const mutationHandler = customHandler ? materializeHandler(customHandler) : null;
|
|
25474
25684
|
if (mutationHandler) admitCredentialHandler(mutationHandler, context, "mutation");
|
|
25685
|
+
const reauthenticationPurpose = readAuthRequirements(mutationHandler)?.reauthentication;
|
|
25686
|
+
if (reauthenticationPurpose) {
|
|
25687
|
+
const consumed = typeof options.sessionToken === "string" && await transactionAdapter.consumeReauthenticationProof({ sessionToken: options.sessionToken, userId: auth.userId, purpose: reauthenticationPurpose, now: database.clock.now().toISOString() });
|
|
25688
|
+
if (!consumed) throw commandError("Reauthentication required.", "Verify the current Session for this purpose and retry.", "REAUTHENTICATION_REQUIRED");
|
|
25689
|
+
}
|
|
25475
25690
|
context = await applyContextMiddleware(transactionDatabase, context, "mutation");
|
|
25476
25691
|
for (const hookSource of database.mutationHooks.beforeMutation) {
|
|
25477
25692
|
await runMutationHookAndDrainPendingAclWrites(hookSource, { name: mutationName, args, ctx: context }, context);
|
|
@@ -32480,7 +32695,7 @@ jobs:
|
|
|
32480
32695
|
}
|
|
32481
32696
|
|
|
32482
32697
|
// src/cli/cli-version.ts
|
|
32483
|
-
var CLI_VERSION = "0.9.
|
|
32698
|
+
var CLI_VERSION = "0.9.9";
|
|
32484
32699
|
|
|
32485
32700
|
// src/cli/sporades.ts
|
|
32486
32701
|
var SUPPORTED_TEMPLATES = new Set(CLIENT_TEMPLATES);
|
package/dist/auth-admission.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type AuthRequirements = Readonly<{
|
|
|
7
7
|
linked: boolean;
|
|
8
8
|
credentials: readonly CredentialKind[];
|
|
9
9
|
scopes: readonly string[];
|
|
10
|
+
reauthentication: string | null;
|
|
10
11
|
}>;
|
|
11
12
|
export declare function invalidAuthRequirements(hint: string): import("./runtime-errors.js").HelperError;
|
|
12
13
|
export declare function normalizeRequireUserAuthOptions(options?: unknown): Readonly<{
|