@cosmicdrift/kumiko-server-runtime 0.160.0 → 0.162.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/package.json +3 -3
- package/src/__tests__/run-prod-app.integration.test.ts +13 -4
- package/src/__tests__/session-boot-gate.test.ts +8 -40
- package/src/__tests__/session-wiring.test.ts +8 -43
- package/src/run-prod-app-boot-context.ts +16 -25
- package/src/run-prod-app.ts +24 -33
- package/src/session-boot-gate.ts +9 -18
- package/src/session-wiring.ts +7 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-server-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.162.0",
|
|
4
4
|
"description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold/codegen tooling.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
76
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
75
|
+
"@cosmicdrift/kumiko-bundled-features": "0.162.0",
|
|
76
|
+
"@cosmicdrift/kumiko-framework": "0.162.0",
|
|
77
77
|
"temporal-polyfill": "^0.3.2"
|
|
78
78
|
},
|
|
79
79
|
"publishConfig": {
|
|
@@ -823,7 +823,12 @@ describe("runProdApp — auth allowedOrigins forwarding", () => {
|
|
|
823
823
|
test("cookieDomain without allowedOrigins fails closed — guard is wired through runProdApp", async () => {
|
|
824
824
|
await expect(
|
|
825
825
|
boot(undefined, {
|
|
826
|
-
|
|
826
|
+
features: [
|
|
827
|
+
authFoundationFeature,
|
|
828
|
+
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
829
|
+
createSessionsFeature(),
|
|
830
|
+
],
|
|
831
|
+
auth: { admin: ADMIN, cookieDomain: "example.eu" },
|
|
827
832
|
allowPlaintextPii: "test: origin-guard focus, not crypto",
|
|
828
833
|
}),
|
|
829
834
|
).rejects.toThrow(/allowedOrigins is empty/);
|
|
@@ -836,11 +841,15 @@ describe("runProdApp — auth allowedOrigins forwarding", () => {
|
|
|
836
841
|
let bootError: unknown;
|
|
837
842
|
try {
|
|
838
843
|
const handle = await boot(undefined, {
|
|
844
|
+
features: [
|
|
845
|
+
authFoundationFeature,
|
|
846
|
+
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
847
|
+
createSessionsFeature(),
|
|
848
|
+
],
|
|
839
849
|
auth: {
|
|
840
850
|
admin: ADMIN,
|
|
841
851
|
cookieDomain: "example.eu",
|
|
842
852
|
allowedOrigins: ["https://app.example.eu"],
|
|
843
|
-
sessions: false,
|
|
844
853
|
},
|
|
845
854
|
});
|
|
846
855
|
expect(handle).toBeDefined();
|
|
@@ -861,7 +870,7 @@ describe("runProdApp — session boot gate (#1262/#1275)", () => {
|
|
|
861
870
|
memberships: [],
|
|
862
871
|
};
|
|
863
872
|
|
|
864
|
-
test("auth mounted, sessions feature missing
|
|
873
|
+
test("auth mounted, sessions feature missing → aborts boot", async () => {
|
|
865
874
|
await expect(
|
|
866
875
|
boot(undefined, {
|
|
867
876
|
auth: {
|
|
@@ -871,7 +880,7 @@ describe("runProdApp — session boot gate (#1262/#1275)", () => {
|
|
|
871
880
|
},
|
|
872
881
|
allowPlaintextPii: "test: session-gate focus, not crypto",
|
|
873
882
|
}),
|
|
874
|
-
).rejects.toThrow(/BOOT ABORTED.*
|
|
883
|
+
).rejects.toThrow(/BOOT ABORTED.*sessionStore/);
|
|
875
884
|
});
|
|
876
885
|
|
|
877
886
|
test("auth mounted, sessions feature mounted → boots cleanly (the happy path the gate guards)", async () => {
|
|
@@ -1,54 +1,22 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { assertSessionBootInvariants } from "../session-boot-gate";
|
|
3
3
|
|
|
4
|
-
describe("assertSessionBootInvariants", () => {
|
|
5
|
-
test("no auth
|
|
4
|
+
describe("assertSessionBootInvariants (#1372)", () => {
|
|
5
|
+
test("no auth → no throw", () => {
|
|
6
6
|
expect(() =>
|
|
7
|
-
assertSessionBootInvariants({
|
|
8
|
-
hasAuth: false,
|
|
9
|
-
sessionsFeatureMounted: false,
|
|
10
|
-
sessionsOption: undefined,
|
|
11
|
-
}),
|
|
7
|
+
assertSessionBootInvariants({ hasAuth: false, sessionStoreProviderMounted: false }),
|
|
12
8
|
).not.toThrow();
|
|
13
9
|
});
|
|
14
10
|
|
|
15
|
-
test("auth
|
|
11
|
+
test("auth + sessionStore → no throw", () => {
|
|
16
12
|
expect(() =>
|
|
17
|
-
assertSessionBootInvariants({
|
|
18
|
-
hasAuth: true,
|
|
19
|
-
sessionsFeatureMounted: false,
|
|
20
|
-
sessionsOption: undefined,
|
|
21
|
-
}),
|
|
22
|
-
).toThrow(/BOOT ABORTED.*sessions.*stateless/s);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("auth mounted, sessions feature missing, explicit sessions:false → boots", () => {
|
|
26
|
-
expect(() =>
|
|
27
|
-
assertSessionBootInvariants({
|
|
28
|
-
hasAuth: true,
|
|
29
|
-
sessionsFeatureMounted: false,
|
|
30
|
-
sessionsOption: false,
|
|
31
|
-
}),
|
|
32
|
-
).not.toThrow();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("auth mounted, sessions feature wired → boots", () => {
|
|
36
|
-
expect(() =>
|
|
37
|
-
assertSessionBootInvariants({
|
|
38
|
-
hasAuth: true,
|
|
39
|
-
sessionsFeatureMounted: true,
|
|
40
|
-
sessionsOption: undefined,
|
|
41
|
-
}),
|
|
13
|
+
assertSessionBootInvariants({ hasAuth: true, sessionStoreProviderMounted: true }),
|
|
42
14
|
).not.toThrow();
|
|
43
15
|
});
|
|
44
16
|
|
|
45
|
-
test("auth
|
|
17
|
+
test("auth without sessionStore → throws", () => {
|
|
46
18
|
expect(() =>
|
|
47
|
-
assertSessionBootInvariants({
|
|
48
|
-
|
|
49
|
-
sessionsFeatureMounted: true,
|
|
50
|
-
sessionsOption: { expiresInMs: 60_000 },
|
|
51
|
-
}),
|
|
52
|
-
).not.toThrow();
|
|
19
|
+
assertSessionBootInvariants({ hasAuth: true, sessionStoreProviderMounted: false }),
|
|
20
|
+
).toThrow(/BOOT ABORTED/);
|
|
53
21
|
});
|
|
54
22
|
});
|
|
@@ -1,51 +1,16 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
createSessionsFeature,
|
|
4
|
-
SESSIONS_FEATURE,
|
|
5
|
-
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
6
|
-
import { resolveProdSessionsConfig, shouldWireProdSessions } from "../session-wiring";
|
|
2
|
+
import { shouldWireProdSessions } from "../session-wiring";
|
|
7
3
|
|
|
8
|
-
describe("shouldWireProdSessions — secure-by-default
|
|
9
|
-
it("wires
|
|
10
|
-
|
|
11
|
-
// previously left stateless (no revocation). Now it wires automatically.
|
|
12
|
-
expect(shouldWireProdSessions(true, true, undefined)).toBe(true);
|
|
4
|
+
describe("shouldWireProdSessions — secure-by-default (#1372)", () => {
|
|
5
|
+
it("wires when auth + sessionStore provider mounted", () => {
|
|
6
|
+
expect(shouldWireProdSessions(true, true)).toBe(true);
|
|
13
7
|
});
|
|
14
8
|
|
|
15
|
-
it("
|
|
16
|
-
expect(shouldWireProdSessions(
|
|
9
|
+
it("does not wire without auth", () => {
|
|
10
|
+
expect(shouldWireProdSessions(false, true)).toBe(false);
|
|
17
11
|
});
|
|
18
12
|
|
|
19
|
-
it("does not wire
|
|
20
|
-
expect(shouldWireProdSessions(true,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("does not wire when the sessions feature is not mounted", () => {
|
|
24
|
-
expect(shouldWireProdSessions(true, false, undefined)).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("does not wire when the app has no auth at all", () => {
|
|
28
|
-
expect(shouldWireProdSessions(false, true, undefined)).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("SESSIONS_FEATURE constant matches the real feature name", () => {
|
|
33
|
-
it("createSessionsFeature()'s name equals SESSIONS_FEATURE", () => {
|
|
34
|
-
// shouldWireProdSessions's own arm only tests the pure boolean helper —
|
|
35
|
-
// the actual run-prod-app.ts integration seam
|
|
36
|
-
// (`features.some((f) => f.name === SESSIONS_FEATURE)`) drifts silently
|
|
37
|
-
// if the feature is ever renamed without updating this constant.
|
|
38
|
-
expect(createSessionsFeature().name).toBe(SESSIONS_FEATURE);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("resolveProdSessionsConfig", () => {
|
|
43
|
-
it("passes a config object through", () => {
|
|
44
|
-
expect(resolveProdSessionsConfig({ expiresInMs: 5000 })).toEqual({ expiresInMs: 5000 });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("collapses false / undefined to defaults", () => {
|
|
48
|
-
expect(resolveProdSessionsConfig(undefined)).toEqual({});
|
|
49
|
-
expect(resolveProdSessionsConfig(false)).toEqual({});
|
|
13
|
+
it("does not wire without sessionStore provider", () => {
|
|
14
|
+
expect(shouldWireProdSessions(true, false)).toBe(false);
|
|
50
15
|
});
|
|
51
16
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { makeAuthPaths } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
2
|
+
import { resolveSessionStore } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
2
3
|
import { bindMfaRevokeAllOtherSessionsFromFeature } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
3
4
|
import { createSmtpTransportFromEnv } from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
4
5
|
import {
|
|
@@ -15,10 +16,7 @@ import {
|
|
|
15
16
|
createSecretsContext,
|
|
16
17
|
SECRETS_FEATURE_NAME,
|
|
17
18
|
} from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
18
|
-
import {
|
|
19
|
-
bindAutoRevokeFromFeature,
|
|
20
|
-
createSessionCallbacks,
|
|
21
|
-
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
19
|
+
import { bindAutoRevokeFromFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
22
20
|
import { createTextContentApi } from "@cosmicdrift/kumiko-bundled-features/text-content";
|
|
23
21
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
24
22
|
import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
@@ -38,7 +36,6 @@ import type {
|
|
|
38
36
|
PasswordResetSetup,
|
|
39
37
|
SignupSetup,
|
|
40
38
|
} from "./run-prod-app";
|
|
41
|
-
import type { ProdSessionsConfig } from "./session-wiring";
|
|
42
39
|
|
|
43
40
|
// Boot-time context helpers for runProdApp: ctx-extra-context wiring
|
|
44
41
|
// (textContent/delivery/secrets/config-resolver), auth-mail convenience
|
|
@@ -214,33 +211,27 @@ export function resolveAuthMail<T extends AuthMailNormalizable>(
|
|
|
214
211
|
};
|
|
215
212
|
}
|
|
216
213
|
|
|
217
|
-
export function buildProdSessionAuth(
|
|
214
|
+
export async function buildProdSessionAuth(
|
|
218
215
|
db: DbConnection,
|
|
219
|
-
|
|
216
|
+
registry: Registry,
|
|
220
217
|
sessionsFeature: FeatureDefinition | undefined,
|
|
221
218
|
mfaFeature: FeatureDefinition | undefined,
|
|
222
|
-
): {
|
|
223
|
-
readonly sessionCreator: ReturnType<typeof
|
|
224
|
-
readonly sessionRevoker: ReturnType<typeof
|
|
225
|
-
readonly sessionChecker: ReturnType<typeof
|
|
226
|
-
} {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
...(opts.expiresInMs !== undefined && { expiresInMs: opts.expiresInMs }),
|
|
230
|
-
});
|
|
231
|
-
// Secure-by-default: password-change/-reset mass-revokes the user's live
|
|
232
|
-
// sessions without the app opting in via autoRevokeOnPasswordChange.
|
|
219
|
+
): Promise<{
|
|
220
|
+
readonly sessionCreator: Awaited<ReturnType<typeof resolveSessionStore>>["creator"];
|
|
221
|
+
readonly sessionRevoker: Awaited<ReturnType<typeof resolveSessionStore>>["revoker"];
|
|
222
|
+
readonly sessionChecker: Awaited<ReturnType<typeof resolveSessionStore>>["checker"];
|
|
223
|
+
}> {
|
|
224
|
+
// Resolve the sessions feature sessionStore provider (#1372).
|
|
225
|
+
const store = await resolveSessionStore({ db, registry });
|
|
233
226
|
if (sessionsFeature) {
|
|
234
|
-
bindAutoRevokeFromFeature(sessionsFeature)?.(
|
|
227
|
+
bindAutoRevokeFromFeature(sessionsFeature)?.(store.massRevoker);
|
|
235
228
|
}
|
|
236
|
-
// MFA enable/disable/regenerate mass-revokes every OTHER live session
|
|
237
|
-
// (stolen-session defense) — only wired when auth-mfa is mounted.
|
|
238
229
|
if (mfaFeature) {
|
|
239
|
-
bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(
|
|
230
|
+
bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(store.revokeAllOthers);
|
|
240
231
|
}
|
|
241
232
|
return {
|
|
242
|
-
sessionCreator:
|
|
243
|
-
sessionRevoker:
|
|
244
|
-
sessionChecker:
|
|
233
|
+
sessionCreator: store.creator,
|
|
234
|
+
sessionRevoker: store.revoker,
|
|
235
|
+
sessionChecker: store.checker,
|
|
245
236
|
};
|
|
246
237
|
}
|
package/src/run-prod-app.ts
CHANGED
|
@@ -46,7 +46,9 @@ import {
|
|
|
46
46
|
seedAdmin,
|
|
47
47
|
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
|
|
48
48
|
import {
|
|
49
|
+
EXT_SESSION_STORE,
|
|
49
50
|
EXT_TOKEN_VERIFIER,
|
|
51
|
+
resolveAnonymousAccessFromRegistry,
|
|
50
52
|
resolveTokenVerifier,
|
|
51
53
|
} from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
52
54
|
import { AUTH_MFA_FEATURE, AuthMfaHandlers } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
@@ -140,11 +142,7 @@ import {
|
|
|
140
142
|
import { buildStaticFallback } from "./run-prod-app-static-files";
|
|
141
143
|
import { type SecurityHeadersOption, withSecurityHeaders } from "./security-headers";
|
|
142
144
|
import { assertSessionBootInvariants } from "./session-boot-gate";
|
|
143
|
-
import {
|
|
144
|
-
type ProdSessionsOption,
|
|
145
|
-
resolveProdSessionsConfig,
|
|
146
|
-
shouldWireProdSessions,
|
|
147
|
-
} from "./session-wiring";
|
|
145
|
+
import { shouldWireProdSessions } from "./session-wiring";
|
|
148
146
|
|
|
149
147
|
export { buildBunServeOptions } from "./bun-serve-options";
|
|
150
148
|
export {
|
|
@@ -287,14 +285,6 @@ export type RunProdAppAuthOptions = {
|
|
|
287
285
|
readonly admin: SeedAdminOptions;
|
|
288
286
|
/** Optional override of the login error → HTTP status map. */
|
|
289
287
|
readonly loginErrorStatusMap?: Readonly<Record<string, number>>;
|
|
290
|
-
/** Opt-in: revocable server-side sessions. Caller MUSS
|
|
291
|
-
* `createSessionsFeature()` zu `features` adden — runProdApp wired
|
|
292
|
-
* hier nur die Auth-Callbacks (creator/revoker/checker) gegen die
|
|
293
|
-
* echte db-connection (sidless JWTs werden dann abgelehnt).
|
|
294
|
-
*
|
|
295
|
-
* Standardverhalten ohne diese Option: stateless JWTs ohne sid
|
|
296
|
-
* (legacy-Verhalten, Kartenhaus existing-Apps unangefasst). */
|
|
297
|
-
readonly sessions?: ProdSessionsOption;
|
|
298
288
|
/** Auth-Mail-Convenience: verdrahtet alle 4 Mail-Flows (passwordReset,
|
|
299
289
|
* emailVerification, signup, invite) aus `auth.mail.baseUrl` + Standard-
|
|
300
290
|
* Pfaden. Alle vier mailen via delivery (ctx.notify) — ersetzt das per-App
|
|
@@ -368,10 +358,8 @@ export type RunProdAppDeps = {
|
|
|
368
358
|
};
|
|
369
359
|
|
|
370
360
|
export type AnonymousAccessOption =
|
|
371
|
-
| import("@cosmicdrift/kumiko-framework/api").
|
|
372
|
-
| ((
|
|
373
|
-
deps: RunProdAppDeps,
|
|
374
|
-
) => import("@cosmicdrift/kumiko-framework/api").ServerOptions["anonymousAccess"]);
|
|
361
|
+
| import("@cosmicdrift/kumiko-framework/api").AnonymousAccessConfig
|
|
362
|
+
| ((deps: RunProdAppDeps) => import("@cosmicdrift/kumiko-framework/api").AnonymousAccessConfig);
|
|
375
363
|
|
|
376
364
|
export type ExtraContextOption =
|
|
377
365
|
| Record<string, unknown>
|
|
@@ -726,12 +714,11 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
726
714
|
mode: "prod",
|
|
727
715
|
});
|
|
728
716
|
const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
|
|
717
|
+
const registry = createRegistry(features);
|
|
729
718
|
assertSessionBootInvariants({
|
|
730
719
|
hasAuth: Boolean(effectiveAuth),
|
|
731
|
-
|
|
732
|
-
sessionsOption: effectiveAuth?.sessions,
|
|
720
|
+
sessionStoreProviderMounted: registry.getExtensionUsages(EXT_SESSION_STORE).length > 0,
|
|
733
721
|
});
|
|
734
|
-
const registry = createRegistry(features);
|
|
735
722
|
|
|
736
723
|
// C1 boot-mode exit: validators ran + registry built; no DB/Redis client
|
|
737
724
|
// is constructed at all in this branch (the eager `new Redis(...)` below
|
|
@@ -861,10 +848,15 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
861
848
|
{ ...autoExtraContext, ...resolvedExtraContext },
|
|
862
849
|
registry,
|
|
863
850
|
);
|
|
864
|
-
const
|
|
851
|
+
const baseAnonymousAccess =
|
|
865
852
|
typeof options.anonymousAccess === "function"
|
|
866
853
|
? options.anonymousAccess(deps)
|
|
867
854
|
: options.anonymousAccess;
|
|
855
|
+
// #1374: tenantResolver / tenantExists come from auth-foundation providers.
|
|
856
|
+
const resolvedAnonymousAccess = await resolveAnonymousAccessFromRegistry(baseAnonymousAccess, {
|
|
857
|
+
db,
|
|
858
|
+
registry,
|
|
859
|
+
});
|
|
868
860
|
|
|
869
861
|
// Sessions opt-in: db ist hier schon konkret (createDbConnection oben),
|
|
870
862
|
// also direkt verdrahten — kein late-bound nötig wie bei runDevApp.
|
|
@@ -874,22 +866,13 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
874
866
|
// AuthRoutesConfig-Surface — der geht via bindAutoRevokeFromFeature ans
|
|
875
867
|
// sessions-Feature (Password-Change/-Reset revoked alle Sessions), nicht
|
|
876
868
|
// über die auth-routes.
|
|
877
|
-
// Secure-by-default:
|
|
878
|
-
// auto-revoke-on-password-change are wired automatically;
|
|
879
|
-
// `auth.sessions` only overrides the config, and `auth.sessions: false` is the
|
|
880
|
-
// explicit opt-out (back to stateless JWTs).
|
|
869
|
+
// Secure-by-default (#1372): sessionStore provider → resolveSessionStore.
|
|
881
870
|
const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
|
|
882
871
|
const sessionAuthFragment = shouldWireProdSessions(
|
|
883
872
|
Boolean(effectiveAuth),
|
|
884
|
-
|
|
885
|
-
effectiveAuth?.sessions,
|
|
873
|
+
registry.getExtensionUsages(EXT_SESSION_STORE).length > 0,
|
|
886
874
|
)
|
|
887
|
-
? buildProdSessionAuth(
|
|
888
|
-
db,
|
|
889
|
-
resolveProdSessionsConfig(effectiveAuth?.sessions),
|
|
890
|
-
sessionsFeature,
|
|
891
|
-
mfaFeature,
|
|
892
|
-
)
|
|
875
|
+
? await buildProdSessionAuth(db, registry, sessionsFeature, mfaFeature)
|
|
893
876
|
: undefined;
|
|
894
877
|
|
|
895
878
|
// Token-verifier opt-in: any provider feature (personal-access-tokens, a
|
|
@@ -975,12 +958,20 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
975
958
|
...tenantLifecycleAuthFragment,
|
|
976
959
|
...(mfaFeature && {
|
|
977
960
|
mfaVerifyHandler: AuthMfaHandlers.verify,
|
|
961
|
+
mfaPreauthEnableStartHandler: AuthMfaHandlers.enableStartPreauth,
|
|
962
|
+
mfaPreauthConfirmHandler: AuthMfaHandlers.enableConfirmPreauth,
|
|
978
963
|
mfaVerifyRateLimit: createRedisLoginRateLimiter(
|
|
979
964
|
redis,
|
|
980
965
|
undefined,
|
|
981
966
|
undefined,
|
|
982
967
|
"mfa-verify",
|
|
983
968
|
),
|
|
969
|
+
mfaPreauthConfirmRateLimit: createRedisLoginRateLimiter(
|
|
970
|
+
redis,
|
|
971
|
+
undefined,
|
|
972
|
+
undefined,
|
|
973
|
+
"mfa-preauth-confirm",
|
|
974
|
+
),
|
|
984
975
|
}),
|
|
985
976
|
...(effectiveAuth.passwordReset && {
|
|
986
977
|
passwordReset: {
|
package/src/session-boot-gate.ts
CHANGED
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import type { ProdSessionsOption } from "./session-wiring";
|
|
2
|
-
|
|
3
1
|
export type SessionBootGateOptions = {
|
|
4
2
|
readonly hasAuth: boolean;
|
|
5
|
-
readonly
|
|
6
|
-
readonly sessionsOption: ProdSessionsOption | undefined;
|
|
3
|
+
readonly sessionStoreProviderMounted: boolean;
|
|
7
4
|
};
|
|
8
5
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// already the sanctioned opt-out (see session-wiring.ts) — reusing it here
|
|
13
|
-
// instead of inventing a second acknowledgment param.
|
|
6
|
+
// Catch a forgotten sessions mount at boot instead of silently degrading
|
|
7
|
+
// into stateless JWTs (#1372). Mount createSessionsFeature() for revocable
|
|
8
|
+
// sessions; there is no auth.sessions opt-out anymore.
|
|
14
9
|
export function assertSessionBootInvariants(opts: SessionBootGateOptions): void {
|
|
15
10
|
// skip: no auth mounted — nothing to gate.
|
|
16
11
|
if (!opts.hasAuth) return;
|
|
17
|
-
// skip:
|
|
18
|
-
if (opts.
|
|
19
|
-
// skip: sessions feature is wired.
|
|
20
|
-
if (opts.sessionsFeatureMounted) return;
|
|
12
|
+
// skip: sessionStore provider is wired (sessions feature).
|
|
13
|
+
if (opts.sessionStoreProviderMounted) return;
|
|
21
14
|
|
|
22
15
|
throw new Error(
|
|
23
|
-
"[runProdApp] BOOT ABORTED — auth is mounted but
|
|
24
|
-
"JWTs would be stateless (no server-side revocation
|
|
25
|
-
"
|
|
26
|
-
"(@cosmicdrift/kumiko-bundled-features/sessions) for revocable sessions, or pass " +
|
|
27
|
-
"{ auth: { sessions: false } } to acknowledge stateless JWTs are intentional.",
|
|
16
|
+
"[runProdApp] BOOT ABORTED — auth is mounted but no sessionStore provider is registered. " +
|
|
17
|
+
"JWTs would be stateless (no server-side revocation). Mount createSessionsFeature() " +
|
|
18
|
+
"(@cosmicdrift/kumiko-bundled-features/sessions) alongside auth-foundation.",
|
|
28
19
|
);
|
|
29
20
|
}
|
package/src/session-wiring.ts
CHANGED
|
@@ -2,28 +2,16 @@
|
|
|
2
2
|
* runProdApp session-wiring decision (extracted pure so it is testable without a
|
|
3
3
|
* full prod boot).
|
|
4
4
|
*
|
|
5
|
-
* Secure-by-default: mounting
|
|
6
|
-
* revocation ON automatically
|
|
7
|
-
* `auth.sessions`
|
|
8
|
-
*
|
|
5
|
+
* Secure-by-default (#1372): mounting a sessionStore provider (sessions feature)
|
|
6
|
+
* turns server-side session revocation ON automatically. There is no
|
|
7
|
+
* `auth.sessions` opt-in/opt-out — mount sessions for revocable JWTs, omit it
|
|
8
|
+
* for intentional stateless JWTs (boot-gate warns / aborts unless acknowledged
|
|
9
|
+
* via a future flag if needed; today auth without sessions fails the gate).
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
export type ProdSessionsConfig = { readonly expiresInMs?: number };
|
|
12
|
-
|
|
13
|
-
/** Config object to override defaults, or `false` to disable session wiring entirely. */
|
|
14
|
-
export type ProdSessionsOption = ProdSessionsConfig | false;
|
|
15
|
-
|
|
16
12
|
export function shouldWireProdSessions(
|
|
17
13
|
hasAuth: boolean,
|
|
18
|
-
|
|
19
|
-
sessionsOption: ProdSessionsOption | undefined,
|
|
14
|
+
sessionStoreProviderMounted: boolean,
|
|
20
15
|
): boolean {
|
|
21
|
-
return hasAuth &&
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** The config passed to buildProdSessionAuth — `false`/absent collapse to defaults. */
|
|
25
|
-
export function resolveProdSessionsConfig(
|
|
26
|
-
sessionsOption: ProdSessionsOption | undefined,
|
|
27
|
-
): ProdSessionsConfig {
|
|
28
|
-
return sessionsOption || {};
|
|
16
|
+
return hasAuth && sessionStoreProviderMounted;
|
|
29
17
|
}
|