@cosmicdrift/kumiko-dev-server 0.387.0 → 0.388.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.
Files changed (42) hide show
  1. package/dist/build-server-bundle.d.ts +1 -1
  2. package/dist/build-server-bundle.js +2 -2
  3. package/dist/create-kumiko-server-client-bundles.d.ts +21 -0
  4. package/dist/create-kumiko-server-client-bundles.js +97 -0
  5. package/dist/create-kumiko-server-html.d.ts +21 -0
  6. package/dist/create-kumiko-server-html.js +132 -0
  7. package/dist/create-kumiko-server-public-files.d.ts +6 -0
  8. package/dist/create-kumiko-server-public-files.js +96 -0
  9. package/dist/create-kumiko-server-reload.d.ts +14 -0
  10. package/dist/create-kumiko-server-reload.js +73 -0
  11. package/dist/create-kumiko-server-routes.d.ts +16 -0
  12. package/dist/create-kumiko-server-routes.js +206 -0
  13. package/dist/create-kumiko-server-shutdown.d.ts +14 -0
  14. package/dist/create-kumiko-server-shutdown.js +72 -0
  15. package/dist/create-kumiko-server-stack.d.ts +12 -0
  16. package/dist/create-kumiko-server-stack.js +101 -0
  17. package/dist/create-kumiko-server-stylesheet.d.ts +10 -0
  18. package/dist/create-kumiko-server-stylesheet.js +137 -0
  19. package/dist/create-kumiko-server-watcher.d.ts +22 -0
  20. package/dist/create-kumiko-server-watcher.js +155 -0
  21. package/dist/create-kumiko-server.d.ts +6 -20
  22. package/dist/create-kumiko-server.js +59 -1046
  23. package/dist/dev-server-runtime.d.ts +5 -0
  24. package/dist/dev-server-runtime.js +11 -0
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.js +3 -1
  27. package/dist/managed-files/app-jobs.js +1 -1
  28. package/dist/managed-files/check.js +1 -1
  29. package/dist/run-dev-app-auth.d.ts +38 -0
  30. package/dist/run-dev-app-auth.js +155 -0
  31. package/dist/run-dev-app-config.d.ts +26 -0
  32. package/dist/run-dev-app-config.js +185 -0
  33. package/dist/run-dev-app-server-options.d.ts +13 -0
  34. package/dist/run-dev-app-server-options.js +49 -0
  35. package/dist/run-dev-app.d.ts +4 -4
  36. package/dist/run-dev-app.js +86 -459
  37. package/dist/schema-apply.d.ts +1 -1
  38. package/dist/schema-apply.js +4 -4
  39. package/dist/write-server-lockfile.js +23 -16
  40. package/package.json +9 -9
  41. package/src/changes.json +30 -0
  42. package/templates/managed/ci-public.yml.template +4 -0
@@ -11,222 +11,103 @@
11
11
  // For maximum control, such as custom auth wiring, a different membership
12
12
  // query, or a custom LoginRateLimiter, use `createKumikoServer` directly from
13
13
  // @cosmicdrift/kumiko-dev-server.
14
- import { AuthErrors, AuthHandlers, AuthQueries, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
15
- import { seedAdminGuarded, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
16
- import { EXT_SESSION_STORE, resolveSessionStore, resolveTokenVerifier, } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
17
- import { AUTH_MFA_FEATURE, AuthMfaHandlers, bindMfaRevokeAllOtherSessionsFromFeature, bindMfaTokenSecretsFromFeature, bindRevokeAllPatTokensFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
18
14
  import { buildEnvConfigOverrides, createConfigResolver, } from "@cosmicdrift/kumiko-bundled-features/config";
19
- import { bindPatAutoRevokeOnPasswordChangeFromFeature, PAT_FEATURE, patRateLimitFromFeature, revokeAllPatTokensForUser, } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
20
- import { bindAutoRevokeFromFeature, SESSIONS_FEATURE, } from "@cosmicdrift/kumiko-bundled-features/sessions";
21
- import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
22
- import { createInMemoryLoginRateLimiter, parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, } from "@cosmicdrift/kumiko-framework/api";
23
- import { configureBlindIndexKey, configurePiiSubjectKms, } from "@cosmicdrift/kumiko-framework/crypto";
24
- import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
25
- import { collectWriteHandlerQns, createRegistry, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, findTierResolverUsage, requireTierResolverPlugin, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
26
- import { parseEnv } from "@cosmicdrift/kumiko-framework/env";
27
- import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
28
- import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
29
- import { assertStackBootRequirements, isSeedFnList, rejectRemovedEffectiveFeatures, resolveBootOptionWiring, withBootOptionEnvBlocks, } from "@cosmicdrift/kumiko-server-runtime";
30
- import { applyBootSeeds } from "@cosmicdrift/kumiko-server-runtime/boot/apply-boot-seeds";
31
- import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
32
- import { buildComposeAuthOptions, composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
33
- import { assertPiiBootInvariants } from "@cosmicdrift/kumiko-server-runtime/pii-boot-gate";
34
- import { assertSessionBootInvariants } from "@cosmicdrift/kumiko-server-runtime/session-boot-gate";
35
- import { createKumikoServer, logInfo, } from "./create-kumiko-server.js";
36
- import { captureEphemeralDevPiiKms, resolveDevPiiKms, } from "./resolve-dev-pii-kms.js";
15
+ import { parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, } from "@cosmicdrift/kumiko-framework/api";
16
+ import { findTierResolverUsage, } from "@cosmicdrift/kumiko-framework/engine";
17
+ import { createKumikoServer, } from "./create-kumiko-server.js";
18
+ import { createDevLateBindings, createLateBoundEffectiveFeatures, runDevAfterSetup, wireDevAuth, } from "./run-dev-app-auth.js";
19
+ import { configureDevBootCrypto, resolveDevAuthRegistry, resolveDevBootConfig, validateDevBoot, } from "./run-dev-app-config.js";
20
+ import { buildDevServerOptions } from "./run-dev-app-server-options.js";
37
21
  import { renderWelcomeBanner } from "./welcome-banner.js";
38
- import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, requireEnv, resolveAuthMail, resolveUnsubscribeWiring, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
22
+ import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
39
23
  export async function runDevApp(options) {
40
- // Automatically mixes standard features in auth mode through composeFeatures.
41
- // runProdApp and the per-app Drizzle schema generator use the same helper,
42
- // keeping migrations and runtime feature composition aligned.
43
- const rawEnvSource = options.envSource ?? process.env;
44
- const seedsConfig = options.seeds !== undefined && !isSeedFnList(options.seeds) ? options.seeds : undefined;
45
- const stackSeeds = options.seeds === undefined
46
- ? []
47
- : isSeedFnList(options.seeds)
48
- ? options.seeds
49
- : (options.seeds.run ?? []);
50
- if (options.envSchema) {
51
- // Free dev seeds take the test stack; the env blocks only read the declarative part.
52
- const { seeds: _devSeeds, kms: devKms, ...bootFields } = options;
53
- const composed = withBootOptionEnvBlocks(options.envSchema, {
54
- ...bootFields,
55
- ...(devKms && { kms: devKms }),
56
- ...(seedsConfig && { seeds: { ...seedsConfig, run: [] } }),
57
- features: options.features,
58
- });
59
- parseEnv(composed.schema, rawEnvSource, { sources: composed.sources });
24
+ const bootFailure = { release: undefined };
25
+ try {
26
+ return await bootDevApp(options, bootFailure);
60
27
  }
61
- rejectRemovedEffectiveFeatures(options);
62
- const wiring = await resolveBootOptionWiring({
63
- features: options.features,
64
- ...(options.mail && { mail: options.mail }),
65
- ...(options.fileStorage && { fileStorage: options.fileStorage }),
66
- ...(options.kmsMode !== undefined && { kmsMode: options.kmsMode }),
67
- ...(options.billing && { billing: options.billing }),
68
- ...(options.featureToggles && { featureToggles: true }),
69
- ...(options.demoMode && { demoMode: options.demoMode }),
70
- ...(seedsConfig && { seeds: { ...seedsConfig, run: [] } }),
71
- ...(options.kms && { kms: options.kms }),
72
- ...(options.blindIndexKey !== undefined && { blindIndexKey: options.blindIndexKey }),
73
- ...(options.auth && { auth: options.auth }),
74
- }, rawEnvSource);
75
- const envSource = Object.keys(wiring.envDefaults).length > 0
76
- ? { ...wiring.envDefaults, ...rawEnvSource }
77
- : rawEnvSource;
78
- assertStackBootRequirements({
79
- features: options.features,
80
- ...(options.featureToggles && { featureToggles: true }),
81
- }, ["featureToggles"]);
82
- // Normalizes auth.mail into the explicit flow fields, symmetrically with
83
- // runProdApp. hmacSecret is read from the JWT_SECRET environment value and
84
- // fails fast when unavailable. From this point on, use effectiveAuth rather
85
- // than options.auth.
86
- const effectiveAuth = options.auth
87
- ? resolveAuthMail(options.auth, requireEnv("JWT_SECRET", envSource, "runDevApp"), envSource, {
88
- mailConfigured: wiring.mailConfigured,
89
- })
90
- : undefined;
91
- const composeAuthOptions = buildComposeAuthOptions(effectiveAuth);
92
- const { unsubscribeLinks, unsubscribeRoutes } = resolveUnsubscribeWiring({
93
- unsubscribe: options.unsubscribe,
94
- publicOrigin: undefined,
95
- mailBaseUrl: effectiveAuth?.mail?.baseUrl,
96
- envSource,
97
- warn: (message) => createFallbackLogger("runDevApp").warn(message),
98
- });
99
- const extraRoutes = unsubscribeRoutes.length === 0 && wiring.extraRoutes.length === 0
100
- ? options.extraRoutes
101
- : [...(options.extraRoutes ?? []), ...unsubscribeRoutes, ...wiring.extraRoutes];
102
- const features = composeFeatures([...options.features, ...wiring.features], {
103
- includeBundled: !!effectiveAuth,
104
- ...(composeAuthOptions && { authOptions: composeAuthOptions }),
105
- });
106
- // An explicitly wired file provider (options.files) satisfies the
107
- // FILE_STORAGE_PROVIDER boot gate. Passed via the validateBoot env option
108
- // instead of mutating process.env, so nothing leaks into later runDevApp
109
- // calls in the same process.
110
- const baseBootEnv = {
111
- ...wiring.envDefaults,
112
- ...(options.validateBootOptions?.env ?? process.env),
113
- };
114
- const hasBootEnvDefaults = Object.keys(wiring.envDefaults).length > 0;
115
- const bootOptions = options.files !== undefined && baseBootEnv[FILE_STORAGE_PROVIDER_ENV] === undefined
116
- ? {
117
- ...options.validateBootOptions,
118
- env: { ...baseBootEnv, [FILE_STORAGE_PROVIDER_ENV]: FILE_STORAGE_PROVIDER_BOOT_SENTINEL },
119
- }
120
- : hasBootEnvDefaults
121
- ? { ...options.validateBootOptions, env: baseBootEnv }
122
- : options.validateBootOptions;
123
- // Run boot validation before creating the server. This catches the same
124
- // errors runProdApp catches (unqualified navigation or handler QNs,
125
- // screen-access violations), which would otherwise only show up as a prod pod
126
- // in CrashLoopBackOff. It throws
127
- // synchronously before a socket is opened.
128
- // Resolve this before validateBoot: an app-supplied masterKey provider must
129
- // satisfy the encrypted-field gate exactly as it does in runProdApp.
130
- const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
131
- validateBoot(features, {
132
- ...(bootCrypto.entityFieldCipher && { entityFieldCipher: bootCrypto.entityFieldCipher }),
133
- ...bootOptions,
28
+ catch (error) {
29
+ // Anything that throws after the KMS wiring is open (boot gates, server creation) must close it.
30
+ await bootFailure.release?.().catch(() => { });
31
+ throw error;
32
+ }
33
+ }
34
+ async function bootDevApp(options, bootFailure) {
35
+ const config = await resolveDevBootConfig(options, bootFailure);
36
+ const bootCrypto = validateDevBoot(options, config);
37
+ const lateBindings = createDevLateBindings();
38
+ const tierResolverUsage = findTierResolverUsage(config.features);
39
+ const effectiveFeatures = createLateBoundEffectiveFeatures(lateBindings, Boolean(tierResolverUsage || config.wiring.featureToggles));
40
+ const { releasePiiKms } = configureDevBootCrypto(options, config, bootCrypto, lateBindings.resets, bootFailure);
41
+ const { registry, sessionStoreProviderMounted } = resolveDevAuthRegistry(config);
42
+ const extraContext = buildDevExtraContext(options, config, registry, bootCrypto);
43
+ const auth = wireDevAuth(config, registry, sessionStoreProviderMounted, lateBindings);
44
+ const trustedProxyHops = options.trustedProxyHops ??
45
+ config.effectiveAuth?.trustedProxyHops ??
46
+ parseTrustedProxyHopsEnv(config.envSource[TRUSTED_PROXY_HOPS_ENV], "runDevApp");
47
+ const handle = await createKumikoServer({
48
+ ...buildDevServerOptions({
49
+ options,
50
+ config,
51
+ auth,
52
+ extraContext,
53
+ trustedProxyHops,
54
+ effectiveFeatures,
55
+ }),
56
+ onAfterSetup: (stack) => runDevAfterSetup(stack, { config, bootCrypto, auth, lateBindings }),
134
57
  });
135
- warnIfNonUtcServerTimeZone();
136
- validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
137
- // Tier-Composition auto-wire: scan features for a
138
- // tenantTierResolver-extension and wire a late-bound wrapper here; it is
139
- // filled in onAfterSetup (where stack.db + stack.registry are available).
140
- // The global operator toggles layer over the same wrapper.
141
- // App-Author sees nothing — `createTierEngineFeature(opts)` mounts +
142
- // framework auto-wires.
143
- const tierResolverUsage = findTierResolverUsage(features);
144
- const tierResolverHolder = {
145
- resolver: undefined,
146
- };
147
- const finalEffectiveFeatures = tierResolverUsage || wiring.featureToggles
148
- ? Object.assign((tenantId) => {
149
- // Defensive: Server starts AFTER onAfterSetup completes, so the
150
- // resolver is filled before any request comes in. Throwing here
151
- // means a programming error (boot order) rather than silent
152
- // "all-features-on" misbehavior.
153
- if (!tierResolverHolder.resolver) {
154
- throw new Error("tier-resolver: extension found but resolver not yet built — boot order issue?");
58
+ bootFailure.release = undefined;
59
+ if (options.welcomeBanner) {
60
+ const banner = renderDevWelcomeBanner(options, config, handle);
61
+ // biome-ignore lint/suspicious/noConsole: boot-time UX print, opt-in via welcomeBanner.
62
+ console.log(`\n${banner}\n`);
63
+ }
64
+ return {
65
+ ...handle,
66
+ stop: async () => {
67
+ try {
68
+ await handle.stop();
155
69
  }
156
- return tierResolverHolder.resolver(tenantId);
157
- }, {
158
- // The real trialGate lands on holder.resolver only after plugin.build(). Delegate rather than
159
- // copy it because this wrapper closure is created before build().
160
- // Return false when no trial gate is available.
161
- trialGate: (tenantId, featureName) => tierResolverHolder.resolver?.trialGate?.(tenantId, featureName) ??
162
- Promise.resolve(false),
163
- })
164
- : undefined;
165
- // Provide the configResolver default for the config feature in auth mode;
166
- // in no-auth mode, leave that choice to the caller. Wrap the factory so the
167
- // spread applies to its result; spreading the function itself is a no-op. The
168
- // ENV-to-config bridge supplies app-override defaults from `env:` keys.
169
- // The temporary registry extracts only config keys because the resolver is
170
- // built before server boot, and stack.registry only exists from onAfterSetup;
171
- // createKumikoServer builds its own registry internally.
172
- // The app-wide cipher for `encrypted: true` entity fields is resolved lazily
173
- // by executors, symmetrically with runProdApp.
174
- configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
175
- // Configure Subject-KMS and blind-index handling, symmetrically with
176
- // runProdApp. Dev warns instead of failing for plaintext PII, but kms with
177
- // lookupable fields still requires blindIndexKey because lookups would fail
178
- // in every mode.
179
- const piiKms = options.kmsMode !== undefined
180
- ? {
181
- kms: wiring.kms.kms,
182
- blindIndexKey: wiring.kms.blindIndexKey,
183
- generatedBlindIndexKey: false,
184
- mode: wiring.kms.kms ? "app-kms" : "plaintext",
185
- }
186
- : resolveDevPiiKms(options);
187
- configurePiiSubjectKms(piiKms.kms);
188
- configureBlindIndexKey(piiKms.blindIndexKey);
189
- const releaseEphemeralPiiKms = captureEphemeralDevPiiKms(piiKms);
190
- // The auth features bind database-backed revokers onto their (possibly shared) instances;
191
- // stop() swaps them for inert ones so a later stack in the same process never reaches this
192
- // app's closed connection.
193
- const resetLateBindings = [];
194
- const releasePiiKms = async () => {
195
- for (const reset of resetLateBindings)
196
- reset();
197
- releaseEphemeralPiiKms();
198
- await wiring.kms.close();
70
+ finally {
71
+ await releasePiiKms();
72
+ }
73
+ },
199
74
  };
200
- assertPiiBootInvariants(features, {
201
- kms: piiKms.kms,
202
- blindIndexKey: piiKms.blindIndexKey,
203
- mode: "dev",
204
- });
205
- if (piiKms.mode === "ephemeral-default") {
206
- logInfo("[runDevApp] PII encrypted with in-memory KMS (ephemeral DB); pass kms: false to opt out");
207
- }
208
- // Throwaway registry, built once and reused below — mirrors runProdApp's
209
- // `createRegistry(features)` right after validateBoot. Symmetric
210
- // sessionStoreProviderMounted check to runProdApp's (registry.getExtension-
211
- // Usages, not a raw features.some(...) scan) so dev and prod never ask the
212
- // same question two different ways again.
213
- const registry = effectiveAuth ? createRegistry(features) : undefined;
214
- const sessionStoreProviderMounted = registry !== undefined && registry.getExtensionUsages(EXT_SESSION_STORE).length > 0;
215
- assertSessionBootInvariants({
216
- hasAuth: Boolean(effectiveAuth),
217
- sessionStoreProviderMounted,
218
- mode: "dev",
75
+ }
76
+ function renderDevWelcomeBanner(options, config, handle) {
77
+ const { effectiveAuth } = config;
78
+ const overrides = typeof options.welcomeBanner === "object" ? options.welcomeBanner : {};
79
+ const port = handle.server?.port ?? options.port ?? 3000;
80
+ return renderWelcomeBanner({
81
+ url: `http://localhost:${port}`,
82
+ ...(effectiveAuth?.admin && {
83
+ admin: {
84
+ email: effectiveAuth.admin.email,
85
+ password: effectiveAuth.admin.password,
86
+ },
87
+ }),
88
+ ...(overrides.featuresDir !== undefined && { featuresDir: overrides.featuresDir }),
89
+ ...(overrides.docsUrl !== undefined && { docsUrl: overrides.docsUrl }),
219
90
  });
91
+ }
92
+ // Provide the configResolver default for the config feature in auth mode;
93
+ // in no-auth mode, leave that choice to the caller. Wrap the factory so the
94
+ // spread applies to its result; spreading the function itself is a no-op. The
95
+ // ENV-to-config bridge supplies app-override defaults from `env:` keys.
96
+ // The temporary registry extracts only config keys because the resolver is
97
+ // built before server boot, and stack.registry only exists from onAfterSetup;
98
+ // createKumikoServer builds its own registry internally.
99
+ // Automatically wire templateResolver and feature-gated secrets, symmetrically
100
+ // with runProdApp. Unlike prod, the database is available here only in the
101
+ // factory dependencies because createKumikoServer builds the stack. Therefore
102
+ // use a factory: buildBootExtraContext runs with hasAuth:false because
103
+ // configResolver already comes from cfgExtra, and cfgExtra values override
104
+ // the boot defaults.
105
+ function buildDevExtraContext(options, config, registry, bootCrypto) {
106
+ const { features, envSource, unsubscribeLinks } = config;
220
107
  const cfgExtra = registry
221
108
  ? mergeConfigResolverDefault(options.extraContext, registry, envSource, bootCrypto.configCipher)
222
109
  : options.extraContext;
223
- // Automatically wire templateResolver and feature-gated secrets, symmetrically
224
- // with runProdApp. Unlike prod, the database is available here only in the
225
- // factory dependencies because createKumikoServer builds the stack. Therefore
226
- // use a factory: buildBootExtraContext runs with hasAuth:false because
227
- // configResolver already comes from cfgExtra, and cfgExtra values override
228
- // the boot defaults.
229
- const extraContext = (deps) => {
110
+ return (deps) => {
230
111
  const base = typeof cfgExtra === "function" ? cfgExtra(deps) : (cfgExtra ?? {});
231
112
  const bootLogger = loggerFromExtraContext(base);
232
113
  const boot = buildBootExtraContext({
@@ -247,260 +128,6 @@ export async function runDevApp(options) {
247
128
  ...base,
248
129
  };
249
130
  };
250
- // Sessions: late-bound holder — resolveSessionStore needs concrete db+
251
- // registry (only after setupTestStack). Wired when sessions feature is
252
- // mounted; no auth.sessions opt-in anymore.
253
- let sessionStore;
254
- const requireSessionStore = () => {
255
- if (!sessionStore) {
256
- throw new Error("[runDevApp] sessionStore accessed before onAfterSetup");
257
- }
258
- return sessionStore;
259
- };
260
- // Token-verifier opt-in: same late-bound holder pattern — resolveTokenVerifier
261
- // needs the real db+registry (only concrete after setupTestStack). Wired
262
- // whenever a provider feature (personal-access-tokens today) is mounted.
263
- let tokenVerifier;
264
- const patFeature = features.find((f) => f.name === PAT_FEATURE);
265
- const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
266
- // Same master secret as runProdApp. Without JWT_SECRET the feature keeps
267
- // its explicit secrets, or its handlers report the missing binding.
268
- const devMasterSecret = envSource["JWT_SECRET"];
269
- if (mfaFeature && devMasterSecret) {
270
- bindMfaTokenSecretsFromFeature(mfaFeature)?.(devMasterSecret, envSource);
271
- }
272
- if (effectiveAuth &&
273
- mfaFeature &&
274
- registry?.getWriteHandler(AuthHandlers.switchTenantMfaGate) === undefined) {
275
- throw new Error(`[runDevApp] auth-mfa is mounted but ${AuthHandlers.switchTenantMfaGate} is not registered: ` +
276
- "createAuthEmailPasswordFeature registers it only when mfaStatusChecker is set, and without it " +
277
- "POST /auth/switch-tenant would mint sessions without the second factor");
278
- }
279
- const patAuthFragment = patFeature
280
- ? {
281
- tokenVerifier: (rawToken) => {
282
- if (!tokenVerifier) {
283
- throw new Error("[runDevApp] token-verifier accessed before onAfterSetup");
284
- }
285
- return tokenVerifier(rawToken);
286
- },
287
- patRateLimiter: (() => {
288
- const rl = patRateLimitFromFeature(patFeature);
289
- return createInMemoryLoginRateLimiter(rl.maxRequests, rl.windowMs);
290
- })(),
291
- }
292
- : {};
293
- // Parity with runProdApp: gate on sessionStore provider, not
294
- // the sessions feature name — a custom provider must also wire auth
295
- // callbacks. sessionStoreProviderMounted computed above via the registry
296
- // (same check as runProdApp) — not recomputed here.
297
- const sessionAuthFragment = effectiveAuth && sessionStoreProviderMounted
298
- ? {
299
- sessionCreator: (user, meta) => requireSessionStore().creator(user, meta),
300
- sessionRevoker: (sid) => requireSessionStore().revoker(sid),
301
- sessionChecker: (sid, userId) => requireSessionStore().checker(sid, userId),
302
- }
303
- : {};
304
- const trustedProxyHops = options.trustedProxyHops ??
305
- effectiveAuth?.trustedProxyHops ??
306
- parseTrustedProxyHopsEnv(envSource[TRUSTED_PROXY_HOPS_ENV], "runDevApp");
307
- const handle = await createKumikoServer({
308
- features,
309
- ...(options.clientEntry !== undefined && { clientEntry: options.clientEntry }),
310
- ...(options.errorDocs && { errorDocs: options.errorDocs }),
311
- ...(options.clientEntries !== undefined && { clientEntries: options.clientEntries }),
312
- ...(options.hostDispatch !== undefined && { hostDispatch: options.hostDispatch }),
313
- ...(options.resolvePageHead !== undefined && { resolvePageHead: options.resolvePageHead }),
314
- ...(options.stylesheet !== undefined && { stylesheet: options.stylesheet }),
315
- ...(options.stylesheetWatch !== undefined && { stylesheetWatch: options.stylesheetWatch }),
316
- ...(options.htmlPath !== undefined && { htmlPath: options.htmlPath }),
317
- ...(options.port !== undefined && { port: options.port }),
318
- ...(options.watchDirs !== undefined && { watchDirs: options.watchDirs }),
319
- ...(options.installSignalHandlers !== undefined && {
320
- installSignalHandlers: options.installSignalHandlers,
321
- }),
322
- ...(extraContext !== undefined && { extraContext }),
323
- ...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
324
- ...(options.files !== undefined && { files: options.files }),
325
- ...(extraRoutes !== undefined && { extraRoutes }),
326
- ...(options.wire !== undefined && { wire: options.wire }),
327
- ...(trustedProxyHops !== undefined && { trustedProxyHops }),
328
- ...(options.demoMode && { wrapFetch: wiring.wrapFetch }),
329
- ...(finalEffectiveFeatures !== undefined && {
330
- effectiveFeatures: finalEffectiveFeatures,
331
- }),
332
- ...(effectiveAuth && {
333
- auth: {
334
- membershipQuery: TenantQueries.memberships,
335
- loginHandler: AuthHandlers.login,
336
- loginErrorStatusMap: effectiveAuth.loginErrorStatusMap ?? {
337
- [AuthErrors.invalidCredentials]: 401,
338
- [AuthErrors.noMembership]: 403,
339
- },
340
- ...(effectiveAuth.cookieDomain !== undefined && {
341
- cookieDomain: effectiveAuth.cookieDomain,
342
- }),
343
- ...(effectiveAuth.retiredCookieDomains !== undefined && {
344
- retiredCookieDomains: effectiveAuth.retiredCookieDomains,
345
- }),
346
- ...(effectiveAuth.allowedOrigins !== undefined && {
347
- allowedOrigins: effectiveAuth.allowedOrigins,
348
- }),
349
- ...(effectiveAuth.unsafeSkipOriginCheck !== undefined && {
350
- unsafeSkipOriginCheck: effectiveAuth.unsafeSkipOriginCheck,
351
- }),
352
- ...(effectiveAuth.trustedProxyHops !== undefined && {
353
- trustedProxyHops: effectiveAuth.trustedProxyHops,
354
- }),
355
- ...(effectiveAuth.postAuthLanding !== undefined && {
356
- postAuthLanding: effectiveAuth.postAuthLanding,
357
- }),
358
- ...sessionAuthFragment,
359
- ...patAuthFragment,
360
- ...(mfaFeature && {
361
- mfaVerifyHandler: AuthMfaHandlers.verify,
362
- switchTenantMfaGateHandler: AuthHandlers.switchTenantMfaGate,
363
- mfaPreauthEnableStartHandler: AuthMfaHandlers.enableStartPreauth,
364
- mfaPreauthConfirmHandler: AuthMfaHandlers.enableConfirmPreauth,
365
- }),
366
- ...(effectiveAuth.passwordReset && {
367
- passwordReset: {
368
- requestHandler: AuthHandlers.requestPasswordReset,
369
- confirmHandler: AuthHandlers.resetPassword,
370
- },
371
- }),
372
- ...(effectiveAuth.emailVerification && {
373
- emailVerification: {
374
- requestHandler: AuthHandlers.requestEmailVerification,
375
- confirmHandler: AuthHandlers.verifyEmail,
376
- },
377
- }),
378
- ...(effectiveAuth.accountUnlock && {
379
- accountUnlock: {
380
- requestHandler: AuthHandlers.requestAccountUnlock,
381
- confirmHandler: AuthHandlers.confirmAccountUnlock,
382
- },
383
- }),
384
- ...(effectiveAuth.signup && {
385
- signup: {
386
- requestHandler: AuthHandlers.signupRequest,
387
- confirmHandler: AuthHandlers.signupConfirm,
388
- },
389
- }),
390
- ...(effectiveAuth.invite && {
391
- invite: {
392
- acceptHandler: AuthHandlers.inviteAccept,
393
- acceptWithLoginHandler: AuthHandlers.inviteAcceptWithLogin,
394
- signupCompleteHandler: AuthHandlers.inviteSignupComplete,
395
- infoHandler: AuthQueries.inviteInfo,
396
- },
397
- }),
398
- },
399
- }),
400
- onAfterSetup: async (stack) => {
401
- // Build tier-resolver BEFORE any seeds so seeds can rely
402
- // on the resolver being live (e.g. seed that writes a SystemAdmin's
403
- // tier-assignment can immediately read tier-cuts).
404
- if (tierResolverUsage) {
405
- tierResolverHolder.resolver = await requireTierResolverPlugin(tierResolverUsage).build({
406
- db: stack.db,
407
- registry: stack.registry,
408
- cacheSync: stack.cacheSync,
409
- });
410
- }
411
- if (wiring.featureToggles) {
412
- const bound = await wiring.featureToggles.bind({
413
- db: stack.db,
414
- registry: stack.registry,
415
- cacheSync: stack.cacheSync,
416
- tierResolver: tierResolverHolder.resolver,
417
- });
418
- tierResolverHolder.resolver = bound.resolver;
419
- }
420
- if (effectiveAuth && stack.registry.getExtensionUsages(EXT_SESSION_STORE).length > 0) {
421
- // Secure-by-default: resolve sessionStore provider; bind
422
- // password-change mass-revoke + MFA revokeAllOthers.
423
- const store = await resolveSessionStore({
424
- db: stack.db,
425
- registry: stack.registry,
426
- });
427
- const sessionsFeature = features.find((f) => f.name === SESSIONS_FEATURE);
428
- if (sessionsFeature) {
429
- bindReleasable(bindAutoRevokeFromFeature(sessionsFeature), store.massRevoker, INERT_REVOKER, resetLateBindings);
430
- }
431
- if (mfaFeature) {
432
- bindReleasable(bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature), store.revokeAllOthers, INERT_REVOKER, resetLateBindings);
433
- }
434
- if (mfaFeature && patFeature) {
435
- bindReleasable(bindRevokeAllPatTokensFromFeature(mfaFeature), (userId) => revokeAllPatTokensForUser(stack.db, userId), INERT_REVOKER, resetLateBindings);
436
- }
437
- sessionStore = store;
438
- }
439
- if (patFeature) {
440
- tokenVerifier = (rawToken) => resolveTokenVerifier({ db: stack.db, registry: stack.registry }, rawToken);
441
- // Password-change → PAT-revoke: independent of sessions/MFA being
442
- // mounted, so wired unconditionally here whenever PAT itself is.
443
- bindReleasable(bindPatAutoRevokeOnPasswordChangeFromFeature(patFeature), (userId) => revokeAllPatTokensForUser(stack.db, userId), INERT_REVOKER, resetLateBindings);
444
- }
445
- if (effectiveAuth) {
446
- await seedAdminGuarded(stack.db, effectiveAuth.admin);
447
- }
448
- // Apply r.config({ seeds }) declared by any registered feature.
449
- // Runs before user-supplied seed callbacks so those can read /
450
- // override the deploy-defaults. The helper indirection is what
451
- // config-seed-boot.integration.ts pins — keep it as a single call.
452
- for (const seed of wiring.shortcutSeeds) {
453
- await seed(stack.db);
454
- }
455
- await applyBootSeeds({
456
- registry: stack.registry,
457
- db: stack.db,
458
- ...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
459
- });
460
- for (const seed of stackSeeds) {
461
- await seed(stack);
462
- }
463
- },
464
- }).catch(async (error) => {
465
- await releasePiiKms().catch(() => { });
466
- throw error;
467
- });
468
- if (options.welcomeBanner) {
469
- const overrides = typeof options.welcomeBanner === "object" ? options.welcomeBanner : {};
470
- const port = handle.server?.port ?? options.port ?? 3000;
471
- const banner = renderWelcomeBanner({
472
- url: `http://localhost:${port}`,
473
- ...(effectiveAuth?.admin && {
474
- admin: {
475
- email: effectiveAuth.admin.email,
476
- password: effectiveAuth.admin.password,
477
- },
478
- }),
479
- ...(overrides.featuresDir !== undefined && { featuresDir: overrides.featuresDir }),
480
- ...(overrides.docsUrl !== undefined && { docsUrl: overrides.docsUrl }),
481
- });
482
- // biome-ignore lint/suspicious/noConsole: boot-time UX print, opt-in via welcomeBanner.
483
- console.log(`\n${banner}\n`);
484
- }
485
- return {
486
- ...handle,
487
- stop: async () => {
488
- try {
489
- await handle.stop();
490
- }
491
- finally {
492
- await releasePiiKms();
493
- }
494
- },
495
- };
496
- }
497
- const INERT_REVOKER = async () => 0;
498
- // Binds a late-bound revoker and records how to swap it for an inert one when the app stops.
499
- function bindReleasable(bind, revoker, inert, resets) {
500
- if (bind) {
501
- bind(revoker);
502
- resets.push(() => bind(inert));
503
- }
504
131
  }
505
132
  // Exported for the wiring-contract test (config-resolver-default.integration):
506
133
  // pins that the dev configResolver-default carries the ENV→app-override bridge
@@ -4,7 +4,7 @@ export type SchemaApplyOptions = ComposeFeaturesOptions & {
4
4
  /** App features (e.g. APP_FEATURES from run-config), composed with the
5
5
  * bundled features for the projection rebuild registry. */
6
6
  readonly features: readonly FeatureDefinition[];
7
- /** Default INIT_CWD ?? process.cwd(); Migrations unter <appCwd>/kumiko/migrations. */
7
+ /** Default INIT_CWD ?? process.cwd(); migrations live under <appCwd>/kumiko/migrations. */
8
8
  readonly appCwd?: string;
9
9
  };
10
10
  export declare function runSchemaApply(opts: SchemaApplyOptions): Promise<number>;
@@ -22,7 +22,7 @@ export async function runSchemaApply(opts) {
22
22
  const appCwd = opts.appCwd ?? process.env["INIT_CWD"] ?? process.cwd();
23
23
  const migrationsDir = join(appCwd, "kumiko/migrations");
24
24
  if (!existsSync(migrationsDir)) {
25
- console.error(`\n ${migrationsDir} fehlt — kumiko/migrations/ muss im Image liegen.\n`);
25
+ console.error(`\n ${migrationsDir} is missing — kumiko/migrations/ must be part of the image.\n`);
26
26
  return 1;
27
27
  }
28
28
  const { db, close } = createDbConnection({ url: dbUrl });
@@ -32,11 +32,11 @@ export async function runSchemaApply(opts) {
32
32
  // on an empty Greenfield DB they create kumiko_events/_consumers/_projections
33
33
  // before the app migrations run. Without this, an empty DB (e.g. cashcolt
34
34
  // on a fresh CNPG) fails with `relation "kumiko_events" does not exist`.
35
- console.log("\n Lege Framework-Infra-Tabellen an (idempotent)…");
35
+ console.log("\n Creating framework infrastructure tables (idempotent)…");
36
36
  await createEventsTable(db);
37
37
  await createEventConsumerStateTable(db);
38
38
  await createProjectionStateTable(db);
39
- console.log(` Wende kumiko-Migrations an (${migrationsDir})…`);
39
+ console.log(` Applying kumiko migrations (${migrationsDir})…`);
40
40
  const result = await runMigrationsFromDir(db, migrationsDir);
41
41
  if (result.applied.length === 0) {
42
42
  console.log(`\n ✓ All ${result.skipped.length} migrations already applied.\n`);
@@ -79,6 +79,6 @@ export async function runStandaloneSchemaCli(opts) {
79
79
  if (cmd === "schema" && sub === "apply") {
80
80
  process.exit(await runSchemaApply(opts));
81
81
  }
82
- console.error(`\n Unbekannt: kumiko ${cmd ?? ""} ${sub ?? ""}\n Nur 'kumiko schema apply' im Standalone-Bundle.\n`);
82
+ console.error(`\n Unknown command: kumiko ${cmd ?? ""} ${sub ?? ""}\n The standalone bundle only supports 'kumiko schema apply'.\n`);
83
83
  process.exit(1);
84
84
  }