@cosmicdrift/kumiko-dev-server 0.109.0 → 0.111.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/boot/apply-boot-seeds.ts +4 -3
- package/src/boot/boot-crypto.ts +82 -0
- package/src/run-dev-app.ts +29 -4
- package/src/run-prod-app.ts +52 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.111.0",
|
|
4
4
|
"description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
54
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
53
|
+
"@cosmicdrift/kumiko-bundled-features": "0.111.0",
|
|
54
|
+
"@cosmicdrift/kumiko-framework": "0.111.0",
|
|
55
55
|
"ts-morph": "^28.0.0"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { seedAllConfigValues } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
2
|
-
import type { DbConnection
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
4
5
|
|
|
5
6
|
// Single boot-seed entry-point. runDevApp + runProdApp both call this
|
|
6
7
|
// from their post-stack hook, so the wiring lives in exactly one place
|
|
@@ -12,7 +13,7 @@ import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
12
13
|
export async function applyBootSeeds(deps: {
|
|
13
14
|
registry: Registry;
|
|
14
15
|
db: DbConnection;
|
|
15
|
-
|
|
16
|
+
cipher?: EnvelopeCipher;
|
|
16
17
|
}): Promise<void> {
|
|
17
|
-
await seedAllConfigValues(deps.registry, deps.db, deps.
|
|
18
|
+
await seedAllConfigValues(deps.registry, deps.db, deps.cipher);
|
|
18
19
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// One place that turns env/options into the app-wide crypto objects:
|
|
2
|
+
// the MasterKeyProvider (KUMIKO_SECRETS_MASTER_KEY_V<n> keyring) and the
|
|
3
|
+
// EnvelopeCipher for `encrypted: true` config keys. runProdApp and
|
|
4
|
+
// runDevApp both resolve this once and thread it into
|
|
5
|
+
// buildBootExtraContext + applyBootSeeds so resolver, set-handler and
|
|
6
|
+
// seeds share the same cipher instance (and DEK cache).
|
|
7
|
+
|
|
8
|
+
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
9
|
+
import {
|
|
10
|
+
createDekCache,
|
|
11
|
+
createEnvelopeCipher,
|
|
12
|
+
createEnvMasterKeyProvider,
|
|
13
|
+
type DekCache,
|
|
14
|
+
type EnvelopeCipher,
|
|
15
|
+
type MasterKeyProvider,
|
|
16
|
+
} from "@cosmicdrift/kumiko-framework/secrets";
|
|
17
|
+
|
|
18
|
+
// Keyring detection without throwing: only build the env provider when at
|
|
19
|
+
// least one versioned KEK is actually set. Covers (a) apps without any
|
|
20
|
+
// encryption — no KEK requirement, and (b) dev with an app-supplied
|
|
21
|
+
// provider in options.masterKey — env stays irrelevant.
|
|
22
|
+
const MASTER_KEK_VAR = /^KUMIKO_SECRETS_MASTER_KEY_V\d+$/;
|
|
23
|
+
export function envHasMasterKek(env: Record<string, string | undefined>): boolean {
|
|
24
|
+
return Object.entries(env).some(([k, v]) => MASTER_KEK_VAR.test(k) && !!v);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type BootCrypto = {
|
|
28
|
+
readonly masterKeyProvider?: MasterKeyProvider;
|
|
29
|
+
// Cipher for encrypted config keys. Present exactly when a master key is
|
|
30
|
+
// available. Decrypts legacy CONFIG_ENCRYPTION_KEY values as fallback
|
|
31
|
+
// until the config re-encrypt job migrated them.
|
|
32
|
+
readonly configCipher?: EnvelopeCipher;
|
|
33
|
+
// Cipher for `encrypted: true` entity fields — same master key, but the
|
|
34
|
+
// legacy fallback reads the pre-envelope ENCRYPTION_KEY format. Identical
|
|
35
|
+
// to configCipher when no ENCRYPTION_KEY is set.
|
|
36
|
+
readonly entityFieldCipher?: EnvelopeCipher;
|
|
37
|
+
readonly dekCache: DekCache;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function resolveBootCrypto(
|
|
41
|
+
envSource: Record<string, string | undefined>,
|
|
42
|
+
masterKeyOverride?: MasterKeyProvider,
|
|
43
|
+
): BootCrypto {
|
|
44
|
+
const masterKeyProvider =
|
|
45
|
+
masterKeyOverride ??
|
|
46
|
+
(envHasMasterKek(envSource)
|
|
47
|
+
? createEnvMasterKeyProvider({
|
|
48
|
+
// CURRENT_VERSION default "1" spiegelt secretsEnvSchema — ohne
|
|
49
|
+
// ihn wirft der raw-env-Provider, obwohl V1 gesetzt ist.
|
|
50
|
+
env: {
|
|
51
|
+
...envSource,
|
|
52
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION:
|
|
53
|
+
envSource["KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION"] ?? "1",
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
: undefined);
|
|
57
|
+
|
|
58
|
+
const dekCache = createDekCache();
|
|
59
|
+
const legacyConfigKey = envSource["CONFIG_ENCRYPTION_KEY"];
|
|
60
|
+
const configCipher = masterKeyProvider
|
|
61
|
+
? createEnvelopeCipher(masterKeyProvider, {
|
|
62
|
+
dekCache,
|
|
63
|
+
...(legacyConfigKey ? { legacy: createEncryptionProvider(legacyConfigKey) } : {}),
|
|
64
|
+
})
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
67
|
+
const legacyEntityKey = envSource["ENCRYPTION_KEY"];
|
|
68
|
+
const entityFieldCipher =
|
|
69
|
+
masterKeyProvider && legacyEntityKey
|
|
70
|
+
? createEnvelopeCipher(masterKeyProvider, {
|
|
71
|
+
dekCache,
|
|
72
|
+
legacy: createEncryptionProvider(legacyEntityKey),
|
|
73
|
+
})
|
|
74
|
+
: configCipher;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...(masterKeyProvider && { masterKeyProvider }),
|
|
78
|
+
...(configCipher && { configCipher }),
|
|
79
|
+
...(entityFieldCipher && { entityFieldCipher }),
|
|
80
|
+
dekCache,
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/run-dev-app.ts
CHANGED
|
@@ -29,12 +29,15 @@ import {
|
|
|
29
29
|
patScopesFromFeature,
|
|
30
30
|
} from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
31
31
|
import {
|
|
32
|
+
bindAutoRevokeFromFeature,
|
|
32
33
|
createSessionCallbacks,
|
|
34
|
+
SESSIONS_FEATURE,
|
|
33
35
|
type SessionCallbacks,
|
|
34
36
|
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
35
37
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
36
38
|
import type { PatResolver, SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
|
|
37
39
|
import { createInMemoryLoginRateLimiter } from "@cosmicdrift/kumiko-framework/api";
|
|
40
|
+
import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
38
41
|
import {
|
|
39
42
|
collectWriteHandlerQns,
|
|
40
43
|
createRegistry,
|
|
@@ -48,10 +51,11 @@ import {
|
|
|
48
51
|
validateAppCustomScreenWriteQns,
|
|
49
52
|
validateBoot,
|
|
50
53
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
51
|
-
import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
54
|
+
import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
52
55
|
import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
53
56
|
import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
54
57
|
import { applyBootSeeds } from "./boot/apply-boot-seeds";
|
|
58
|
+
import { resolveBootCrypto } from "./boot/boot-crypto";
|
|
55
59
|
|
|
56
60
|
import { watchAndRegenerate } from "./codegen";
|
|
57
61
|
import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
|
|
@@ -309,8 +313,17 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
309
313
|
// throwaway-Registry hier extrahiert nur die config-Keys, weil der
|
|
310
314
|
// configResolver vor dem Server-Boot konstruiert wird (stack.registry
|
|
311
315
|
// gibt's erst onAfterSetup) — createKumikoServer baut intern seine eigene.
|
|
316
|
+
const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
|
|
317
|
+
// App-wide cipher for `encrypted: true` entity fields (symmetrisch zu
|
|
318
|
+
// runProdApp) — executors resolve it lazily.
|
|
319
|
+
configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
|
|
312
320
|
const cfgExtra = effectiveAuth
|
|
313
|
-
? mergeConfigResolverDefault(
|
|
321
|
+
? mergeConfigResolverDefault(
|
|
322
|
+
options.extraContext,
|
|
323
|
+
createRegistry(features),
|
|
324
|
+
envSource,
|
|
325
|
+
bootCrypto.configCipher,
|
|
326
|
+
)
|
|
314
327
|
: options.extraContext;
|
|
315
328
|
// Auto-wire textContent (immer) + secrets (feature-gated), symmetrisch zu
|
|
316
329
|
// runProdApp. Anders als prod existiert die db hier erst im Factory-deps
|
|
@@ -325,7 +338,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
325
338
|
registry: deps.registry,
|
|
326
339
|
hasAuth: false,
|
|
327
340
|
sseBroker: deps.sseBroker,
|
|
328
|
-
|
|
341
|
+
crypto: bootCrypto,
|
|
329
342
|
});
|
|
330
343
|
const base = typeof cfgExtra === "function" ? cfgExtra(deps) : (cfgExtra ?? {});
|
|
331
344
|
return { ...boot, ...base };
|
|
@@ -463,6 +476,12 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
463
476
|
db: stack.db,
|
|
464
477
|
...(expiresInMs !== undefined && { expiresInMs }),
|
|
465
478
|
});
|
|
479
|
+
// Secure-by-default (symmetrisch zu runProdApp): Password-Change/
|
|
480
|
+
// -Reset mass-revoked die Sessions des Users ohne App-Opt-in.
|
|
481
|
+
const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
|
|
482
|
+
if (sessionsFeature) {
|
|
483
|
+
bindAutoRevokeFromFeature(sessionsFeature)?.(sessionCallbacks.sessionMassRevoker);
|
|
484
|
+
}
|
|
466
485
|
}
|
|
467
486
|
if (patFeature) {
|
|
468
487
|
patResolver = createPatResolver({ db: stack.db, scopes: patScopesFromFeature(patFeature) });
|
|
@@ -474,7 +493,11 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
474
493
|
// Runs before user-supplied seed callbacks so those can read /
|
|
475
494
|
// override the deploy-defaults. The helper indirection is what
|
|
476
495
|
// config-seed-boot.integration.ts pins — keep it as a single call.
|
|
477
|
-
await applyBootSeeds({
|
|
496
|
+
await applyBootSeeds({
|
|
497
|
+
registry: stack.registry,
|
|
498
|
+
db: stack.db,
|
|
499
|
+
...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
|
|
500
|
+
});
|
|
478
501
|
for (const seed of options.seeds ?? []) {
|
|
479
502
|
await seed(stack);
|
|
480
503
|
}
|
|
@@ -510,10 +533,12 @@ export function mergeConfigResolverDefault(
|
|
|
510
533
|
ctx: CreateKumikoServerOptions["extraContext"],
|
|
511
534
|
registry: Registry,
|
|
512
535
|
envSource: Record<string, string | undefined>,
|
|
536
|
+
cipher?: EnvelopeCipher,
|
|
513
537
|
): CreateKumikoServerOptions["extraContext"] {
|
|
514
538
|
const defaults = {
|
|
515
539
|
configResolver: createConfigResolver({
|
|
516
540
|
appOverrides: buildEnvConfigOverrides(registry, envSource),
|
|
541
|
+
...(cipher && { cipher }),
|
|
517
542
|
}),
|
|
518
543
|
};
|
|
519
544
|
// ctx.config wird per-Request aus _configAccessorFactory geminted (siehe
|
package/src/run-prod-app.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
SECRETS_FEATURE_NAME,
|
|
62
62
|
} from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
63
63
|
import {
|
|
64
|
+
bindAutoRevokeFromFeature,
|
|
64
65
|
createSessionCallbacks,
|
|
65
66
|
SESSIONS_FEATURE,
|
|
66
67
|
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
@@ -77,6 +78,7 @@ import {
|
|
|
77
78
|
type SseBroker,
|
|
78
79
|
} from "@cosmicdrift/kumiko-framework/api";
|
|
79
80
|
import {
|
|
81
|
+
configureEntityFieldEncryption,
|
|
80
82
|
createDbConnection,
|
|
81
83
|
type DbConnection,
|
|
82
84
|
type DbRunner,
|
|
@@ -122,13 +124,11 @@ import {
|
|
|
122
124
|
createEventDedup,
|
|
123
125
|
createIdempotencyGuard,
|
|
124
126
|
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
125
|
-
import {
|
|
126
|
-
createEnvMasterKeyProvider,
|
|
127
|
-
type MasterKeyProvider,
|
|
128
|
-
} from "@cosmicdrift/kumiko-framework/secrets";
|
|
127
|
+
import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
129
128
|
import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
130
129
|
import Redis from "ioredis";
|
|
131
130
|
import { applyBootSeeds } from "./boot/apply-boot-seeds";
|
|
131
|
+
import { type BootCrypto, resolveBootCrypto } from "./boot/boot-crypto";
|
|
132
132
|
import { ASSETS_DIR } from "./build-prod-bundle";
|
|
133
133
|
import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
|
|
134
134
|
import { type ExtraRoutesSystemDeps, makeDispatchSystemWrite } from "./extra-routes-deps";
|
|
@@ -596,18 +596,13 @@ export function addConfigAccessorFactory<T extends { readonly configResolver?: C
|
|
|
596
596
|
// nur einen db-gebundenen Accessor). secrets wird nur auto-verdrahtet wenn
|
|
597
597
|
// das secrets-Feature gemountet ist UND ein KEK tatsächlich verfügbar ist
|
|
598
598
|
// (masterKey-Override ODER env-KEK present) — sonst skip, damit der eager
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
// (kein env-KEK) → kein Boot-Crash, die App-explizite secrets-Wiring gewinnt.
|
|
599
|
+
// KEK-Detection + Provider/Cipher-Aufbau leben in boot/boot-crypto.ts
|
|
600
|
+
// (envHasMasterKek, resolveBootCrypto) — gemeinsam mit runDevApp.
|
|
602
601
|
// Prod-Misconfig (secrets gemountet, kein KEK) fängt schon secretsEnvSchema
|
|
603
602
|
// beim Boot; fehlt der env-Schema-Pfad, wirft requireSecretsContext beim
|
|
604
603
|
// ersten ctx.secrets-Zugriff mit Wiring-Hinweis. configResolver nur im
|
|
605
604
|
// Auth-Mode. Exportiert + pure für Unit-Tests; der merge mit App-Werten
|
|
606
605
|
// passiert beim Caller (App gewinnt).
|
|
607
|
-
const MASTER_KEK_VAR = /^KUMIKO_SECRETS_MASTER_KEY_V\d+$/;
|
|
608
|
-
function envHasMasterKek(env: Record<string, string | undefined>): boolean {
|
|
609
|
-
return Object.entries(env).some(([k, v]) => MASTER_KEK_VAR.test(k) && !!v);
|
|
610
|
-
}
|
|
611
606
|
|
|
612
607
|
// Prod/dev parity for ctx.notify: without this `_notifyFactory` is only wired
|
|
613
608
|
// in tests (createDeliveryTestContext), so ctx.notify is undefined at runtime
|
|
@@ -634,12 +629,16 @@ export function buildBootExtraContext(opts: {
|
|
|
634
629
|
readonly envSource: Record<string, string | undefined>;
|
|
635
630
|
readonly registry: Registry;
|
|
636
631
|
readonly hasAuth: boolean;
|
|
632
|
+
// Resolved once per boot via resolveBootCrypto — shared by secrets,
|
|
633
|
+
// config-resolver, config-set-handler and boot-seeds. Absent (tests
|
|
634
|
+
// that don't care about encryption) ⇒ resolved from envSource here.
|
|
635
|
+
readonly crypto?: BootCrypto;
|
|
637
636
|
readonly masterKey?: MasterKeyProvider;
|
|
638
637
|
readonly sseBroker?: SseBroker;
|
|
639
638
|
}): Record<string, unknown> {
|
|
639
|
+
const crypto = opts.crypto ?? resolveBootCrypto(opts.envSource, opts.masterKey);
|
|
640
640
|
const hasSecretsFeature = opts.features.some((f) => f.name === SECRETS_FEATURE_NAME);
|
|
641
|
-
const wireSecrets =
|
|
642
|
-
hasSecretsFeature && (opts.masterKey !== undefined || envHasMasterKek(opts.envSource));
|
|
641
|
+
const wireSecrets = hasSecretsFeature && crypto.masterKeyProvider !== undefined;
|
|
643
642
|
const hasDeliveryFeature = opts.features.some((f) => f.name === DELIVERY_FEATURE);
|
|
644
643
|
return {
|
|
645
644
|
textContent: createTextContentApi(opts.db),
|
|
@@ -650,25 +649,25 @@ export function buildBootExtraContext(opts: {
|
|
|
650
649
|
...(opts.sseBroker && { sseBroker: opts.sseBroker }),
|
|
651
650
|
}),
|
|
652
651
|
}),
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
652
|
+
// Top-level provider so feature jobs (secrets rotate, config reencrypt)
|
|
653
|
+
// reach it via ctx — previously only test-stack wired it.
|
|
654
|
+
...(crypto.masterKeyProvider && { masterKeyProvider: crypto.masterKeyProvider }),
|
|
655
|
+
// Encrypt/decrypt partner for `encrypted: true` config keys. Wired
|
|
656
|
+
// whenever a master key exists — NOT gated on the secrets feature,
|
|
657
|
+
// config encryption must work without mounting ctx.secrets.
|
|
658
|
+
...(crypto.configCipher && { configEncryption: crypto.configCipher }),
|
|
659
|
+
...(wireSecrets &&
|
|
660
|
+
crypto.masterKeyProvider && {
|
|
661
|
+
secrets: createSecretsContext({
|
|
662
|
+
db: opts.db,
|
|
663
|
+
masterKeyProvider: crypto.masterKeyProvider,
|
|
664
|
+
dekCache: crypto.dekCache,
|
|
665
|
+
}),
|
|
667
666
|
}),
|
|
668
|
-
}),
|
|
669
667
|
...(opts.hasAuth && {
|
|
670
668
|
configResolver: createConfigResolver({
|
|
671
669
|
appOverrides: buildEnvConfigOverrides(opts.registry, opts.envSource),
|
|
670
|
+
...(crypto.configCipher && { cipher: crypto.configCipher }),
|
|
672
671
|
}),
|
|
673
672
|
}),
|
|
674
673
|
};
|
|
@@ -919,6 +918,10 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
919
918
|
|
|
920
919
|
// Framework-Default-Provider zuerst, App-Werte (resolvedExtraContext)
|
|
921
920
|
// gewinnen immer (z.B. money-horse's eigener configResolver).
|
|
921
|
+
const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
|
|
922
|
+
// App-wide cipher for `encrypted: true` entity fields — executors resolve
|
|
923
|
+
// it lazily, entities without encrypted fields never touch it.
|
|
924
|
+
configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
|
|
922
925
|
const autoExtraContext = buildBootExtraContext({
|
|
923
926
|
db,
|
|
924
927
|
features,
|
|
@@ -926,7 +929,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
926
929
|
registry,
|
|
927
930
|
hasAuth: !!effectiveAuth,
|
|
928
931
|
sseBroker,
|
|
929
|
-
|
|
932
|
+
crypto: bootCrypto,
|
|
930
933
|
});
|
|
931
934
|
const extraContext = addConfigAccessorFactory(
|
|
932
935
|
{ ...autoExtraContext, ...resolvedExtraContext },
|
|
@@ -942,19 +945,20 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
942
945
|
// sessionStrictMode=true: Prod-Sessions sollen nicht stillschweigend
|
|
943
946
|
// von einem JWT-ohne-sid umgangen werden können. sessionMassRevoker
|
|
944
947
|
// (4. callback aus createSessionCallbacks) ist nicht Teil der
|
|
945
|
-
// AuthRoutesConfig-Surface — der
|
|
946
|
-
//
|
|
947
|
-
// auth-routes.
|
|
948
|
+
// AuthRoutesConfig-Surface — der geht via bindAutoRevokeFromFeature ans
|
|
949
|
+
// sessions-Feature (Password-Change/-Reset revoked alle Sessions), nicht
|
|
950
|
+
// über die auth-routes.
|
|
948
951
|
// Secure-by-default: if the sessions feature is mounted, server-side revocation +
|
|
949
|
-
// sessionStrictMode are wired automatically;
|
|
950
|
-
// and `auth.sessions: false` is the
|
|
951
|
-
|
|
952
|
+
// sessionStrictMode + auto-revoke-on-password-change are wired automatically;
|
|
953
|
+
// `auth.sessions` only overrides the config, and `auth.sessions: false` is the
|
|
954
|
+
// explicit opt-out (back to stateless JWTs).
|
|
955
|
+
const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
|
|
952
956
|
const sessionAuthFragment = shouldWireProdSessions(
|
|
953
957
|
Boolean(effectiveAuth),
|
|
954
|
-
|
|
958
|
+
sessionsFeature !== undefined,
|
|
955
959
|
effectiveAuth?.sessions,
|
|
956
960
|
)
|
|
957
|
-
? buildProdSessionAuth(db, resolveProdSessionsConfig(effectiveAuth?.sessions))
|
|
961
|
+
? buildProdSessionAuth(db, resolveProdSessionsConfig(effectiveAuth?.sessions), sessionsFeature)
|
|
958
962
|
: undefined;
|
|
959
963
|
|
|
960
964
|
// PAT opt-in: if the personal-access-tokens feature is mounted, wire its
|
|
@@ -1104,7 +1108,11 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
1104
1108
|
if (effectiveAuth) {
|
|
1105
1109
|
await seedAdmin(db, effectiveAuth.admin);
|
|
1106
1110
|
}
|
|
1107
|
-
await applyBootSeeds({
|
|
1111
|
+
await applyBootSeeds({
|
|
1112
|
+
registry,
|
|
1113
|
+
db,
|
|
1114
|
+
...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
|
|
1115
|
+
});
|
|
1108
1116
|
for (const seed of options.seeds ?? []) {
|
|
1109
1117
|
await seed({ db });
|
|
1110
1118
|
}
|
|
@@ -1492,6 +1500,7 @@ export function staticCachePolicy(pathname: string): CachePolicy {
|
|
|
1492
1500
|
function buildProdSessionAuth(
|
|
1493
1501
|
db: import("@cosmicdrift/kumiko-framework/db").DbConnection,
|
|
1494
1502
|
opts: ProdSessionsConfig,
|
|
1503
|
+
sessionsFeature: import("@cosmicdrift/kumiko-framework/engine").FeatureDefinition | undefined,
|
|
1495
1504
|
): {
|
|
1496
1505
|
readonly sessionCreator: ReturnType<typeof createSessionCallbacks>["sessionCreator"];
|
|
1497
1506
|
readonly sessionRevoker: ReturnType<typeof createSessionCallbacks>["sessionRevoker"];
|
|
@@ -1502,6 +1511,11 @@ function buildProdSessionAuth(
|
|
|
1502
1511
|
db,
|
|
1503
1512
|
...(opts.expiresInMs !== undefined && { expiresInMs: opts.expiresInMs }),
|
|
1504
1513
|
});
|
|
1514
|
+
// Secure-by-default: password-change/-reset mass-revokes the user's live
|
|
1515
|
+
// sessions without the app opting in via autoRevokeOnPasswordChange.
|
|
1516
|
+
if (sessionsFeature) {
|
|
1517
|
+
bindAutoRevokeFromFeature(sessionsFeature)?.(cbs.sessionMassRevoker);
|
|
1518
|
+
}
|
|
1505
1519
|
return {
|
|
1506
1520
|
sessionCreator: cbs.sessionCreator,
|
|
1507
1521
|
sessionRevoker: cbs.sessionRevoker,
|