@cosmicdrift/kumiko-server-runtime 0.373.0 → 0.377.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/dist/boot/env-gate.d.ts +23 -0
- package/dist/boot/env-gate.js +136 -0
- package/dist/boot/worker-boot-core.d.ts +3 -0
- package/dist/boot/worker-boot-core.js +10 -2
- package/dist/boot-options/demo-mode.d.ts +7 -1
- package/dist/boot-options/demo-mode.js +5 -2
- package/dist/boot-options/feature-toggles-option.d.ts +19 -0
- package/dist/boot-options/feature-toggles-option.js +25 -0
- package/dist/boot-options/file-storage-option.d.ts +11 -2
- package/dist/boot-options/file-storage-option.js +48 -9
- package/dist/boot-options/mail-option.d.ts +8 -3
- package/dist/boot-options/mail-option.js +21 -6
- package/dist/boot-options/option-env-schema.js +30 -9
- package/dist/boot-options/resolve-boot-options.d.ts +9 -1
- package/dist/boot-options/resolve-boot-options.js +37 -4
- package/dist/boot-options/stack-boot-requirements.d.ts +5 -2
- package/dist/boot-options/stack-boot-requirements.js +23 -8
- package/dist/build-prod-bundle.d.ts +3 -3
- package/dist/build-prod-bundle.js +5 -5
- package/dist/extra-routes-deps.d.ts +5 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -0
- package/dist/renderer-web-fonts.js +1 -1
- package/dist/request-bound-system-query.d.ts +1 -1
- package/dist/resolve-tailwind-cli.d.ts +1 -1
- package/dist/resolve-tailwind-cli.js +1 -1
- package/dist/run-prod-app-boot-context.d.ts +4 -0
- package/dist/run-prod-app-boot-context.js +12 -5
- package/dist/run-prod-app-static-files.js +1 -1
- package/dist/run-prod-app.d.ts +22 -12
- package/dist/run-prod-app.js +45 -144
- package/dist/run-worker-app.d.ts +23 -4
- package/dist/run-worker-app.js +51 -5
- package/package.json +7 -8
- package/src/changes.json +51 -0
package/dist/run-prod-app.js
CHANGED
|
@@ -44,17 +44,14 @@ import { seedAdminGuarded, } from "@cosmicdrift/kumiko-bundled-features/auth-ema
|
|
|
44
44
|
import { EXT_SESSION_STORE, EXT_TOKEN_VERIFIER, resolveAnonymousAccessFromRegistry, resolveTokenVerifier, } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
45
45
|
import { AUTH_MFA_FEATURE, AuthMfaHandlers, bindMfaTokenSecretsFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
46
46
|
import { PAT_FEATURE, patRateLimitFromFeature, } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
47
|
-
import { SECRETS_MASTER_KEK_ENV_KEYS } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
48
47
|
import { SESSIONS_FEATURE } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
49
48
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
50
49
|
import { UserQueries } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
51
50
|
import { createDefaultSseBroker, createRedisLoginRateLimiter, loadJwtSecretOrKeyring, parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, withFileProviderResolver, } from "@cosmicdrift/kumiko-framework/api";
|
|
52
|
-
import { configureBlindIndexKey, configurePiiSubjectKms,
|
|
51
|
+
import { configureBlindIndexKey, configurePiiSubjectKms, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
53
52
|
import { configureEntityFieldEncryption, createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/db";
|
|
54
53
|
import { collectWriteHandlerQns, createRegistry, findTierResolverUsage, requireTierResolverPlugin, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
|
|
55
54
|
import { createAllInOneEntrypoint, createApiEntrypoint, } from "@cosmicdrift/kumiko-framework/entrypoint";
|
|
56
|
-
import { inspectEnv, KumikoBootError, kmsSlotsOf, parseEnv, withOptionalEnvKeys, } from "@cosmicdrift/kumiko-framework/env";
|
|
57
|
-
import { DRY_RUN_ENV_VAR, parseDryRunEnvMode, renderDryRun, renderEnvInspection, } from "@cosmicdrift/kumiko-framework/env/dry-run";
|
|
58
55
|
import { createEsOperationsTable, createSeedMigrationContext, runPendingSeedMigrations, } from "@cosmicdrift/kumiko-framework/es-ops";
|
|
59
56
|
import { assertSeedModeNotInProduction } from "@cosmicdrift/kumiko-framework/event-store";
|
|
60
57
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
@@ -66,11 +63,11 @@ import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
|
66
63
|
import { Redis } from "ioredis";
|
|
67
64
|
import { applyBootSeeds } from "./boot/apply-boot-seeds.js";
|
|
68
65
|
import { resolveBootCrypto } from "./boot/boot-crypto.js";
|
|
66
|
+
import { runEnvGate } from "./boot/env-gate.js";
|
|
69
67
|
import { jobRunLoggerCallbacks } from "./boot/job-run-logger.js";
|
|
70
68
|
import { startPiiEventBackfillOnBoot } from "./boot/pii-event-backfill-on-boot.js";
|
|
71
69
|
import { recordAppStartedOnBoot } from "./boot/record-app-started-on-boot.js";
|
|
72
70
|
import { waitForDatabase, waitUntilDatabaseReachable } from "./boot/wait-for-database.js";
|
|
73
|
-
import { withBootOptionEnvBlocks } from "./boot-options/option-env-schema.js";
|
|
74
71
|
import { installShutdownSignals } from "./boot-options/process-signals.js";
|
|
75
72
|
import { resolveBootOptionWiring } from "./boot-options/resolve-boot-options.js";
|
|
76
73
|
import { assertStackBootRequirements } from "./boot-options/stack-boot-requirements.js";
|
|
@@ -88,21 +85,11 @@ import { makeSeedCreateFileRef, withSeedErrorContext, } from "./seed-dispatcher.
|
|
|
88
85
|
import { assertSessionBootInvariants } from "./session-boot-gate.js";
|
|
89
86
|
import { shouldWireProdSessions } from "./session-wiring.js";
|
|
90
87
|
import { stripNoRouteMatchHeader } from "./try-hono-first.js";
|
|
88
|
+
export { parseRunMode } from "./boot/env-gate.js";
|
|
91
89
|
export { buildBunServeOptions } from "./bun-serve-options.js";
|
|
92
90
|
export { resolveUnsubscribeWiring, } from "./resolve-unsubscribe-wiring.js";
|
|
93
91
|
export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
|
|
94
92
|
export { staticCachePolicy } from "./run-prod-app-static-files.js";
|
|
95
|
-
// runProdApp reads these itself, so the boot dry run lists them even when
|
|
96
|
-
// the app schema leaves them out (JWT_SECRET usually comes from auth-email-password).
|
|
97
|
-
const RUNTIME_REQUIRED_ENV_KEYS = ["DATABASE_URL", "REDIS_URL", "JWT_SECRET"];
|
|
98
|
-
function inspectUndeclaredRuntimeEnv(declaredShape, env) {
|
|
99
|
-
return RUNTIME_REQUIRED_ENV_KEYS.filter((name) => !(name in declaredShape)).map((name) => ({
|
|
100
|
-
name,
|
|
101
|
-
klass: "required",
|
|
102
|
-
source: "server-runtime",
|
|
103
|
-
status: env[name] ? "set" : "missing",
|
|
104
|
-
}));
|
|
105
|
-
}
|
|
106
93
|
// Strict env-var read. Throws with a clear hint when missing — better
|
|
107
94
|
// than discovering a Postgres-connection-refused 30s into the boot.
|
|
108
95
|
// `src` defaults to process.env but is threaded from the caller's envSource
|
|
@@ -124,25 +111,6 @@ function readEnv(name, src = process.env) {
|
|
|
124
111
|
const value = src[name];
|
|
125
112
|
return value === undefined || value === "" ? undefined : value;
|
|
126
113
|
}
|
|
127
|
-
export function parseRunMode(raw) {
|
|
128
|
-
const mode = parseDryRunEnvMode(raw);
|
|
129
|
-
if (mode !== undefined)
|
|
130
|
-
return mode;
|
|
131
|
-
if (raw) {
|
|
132
|
-
// biome-ignore lint/suspicious/noConsole: boot-time warn for typo discovery
|
|
133
|
-
console.warn(`[runProdApp] ${DRY_RUN_ENV_VAR}="${raw}" unrecognized ` +
|
|
134
|
-
`(expected 1|human|json|pulumi|k8s|boot); continuing with normal boot.`);
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
function isRenderMode(mode) {
|
|
139
|
-
return mode !== null && mode !== "boot";
|
|
140
|
-
}
|
|
141
|
-
function defaultBootErrorReporter(err) {
|
|
142
|
-
// biome-ignore lint/suspicious/noConsole: boot-time error, no logger configured yet
|
|
143
|
-
console.error(err.format());
|
|
144
|
-
process.exit(1);
|
|
145
|
-
}
|
|
146
114
|
// Returned from runProdApp when KUMIKO_DRY_RUN_ENV is set AND envSource
|
|
147
115
|
// was passed (= test-mode). The handle is intentionally inert — listen()
|
|
148
116
|
// and stop() are no-ops; tests inspect the dry-run console output and
|
|
@@ -176,19 +144,6 @@ function warnLegacyJwtSecretOnce() {
|
|
|
176
144
|
"that is no longer present in any in-flight token (see resolveAuthMail / " +
|
|
177
145
|
"loadJwtSecretOrKeyring).");
|
|
178
146
|
}
|
|
179
|
-
// JS callers and apps built against older typings can still omit it.
|
|
180
|
-
function requireEnvSchema(envSchema) {
|
|
181
|
-
if (envSchema === undefined) {
|
|
182
|
-
throw new KumikoBootError([
|
|
183
|
-
{
|
|
184
|
-
name: "envSchema",
|
|
185
|
-
kind: "missing",
|
|
186
|
-
message: "runProdApp needs envSchema. Pass composeEnvSchema({ core: frameworkCoreEnvSchema, features, extend }): composeEnvSchema from @cosmicdrift/kumiko-framework/env, frameworkCoreEnvSchema from @cosmicdrift/kumiko-dev-server/env-schema, features the same list as runProdApp's.",
|
|
187
|
-
},
|
|
188
|
-
]);
|
|
189
|
-
}
|
|
190
|
-
return envSchema;
|
|
191
|
-
}
|
|
192
147
|
export async function runProdApp(options) {
|
|
193
148
|
assertSeedModeNotInProduction();
|
|
194
149
|
// 0. Env-Schema validation + dry-run modes. Runs FIRST so:
|
|
@@ -208,88 +163,10 @@ export async function runProdApp(options) {
|
|
|
208
163
|
// at parse-time before the polyfill loads. Plain strings + .regex /
|
|
209
164
|
// .min / .email / .url cover every env-var shape we've actually
|
|
210
165
|
// needed in 9.1's audit (37 references, 25 distinct vars).
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
const composedEnvSchema = withBootOptionEnvBlocks(requireEnvSchema(options.envSchema), options);
|
|
214
|
-
const effectiveEnvSchema = options.masterKey
|
|
215
|
-
? withOptionalEnvKeys(composedEnvSchema.schema, SECRETS_MASTER_KEK_ENV_KEYS)
|
|
216
|
-
: composedEnvSchema.schema;
|
|
217
|
-
const reportBootError = (err) => {
|
|
218
|
-
const reporter = options.bootErrorReporter ?? defaultBootErrorReporter;
|
|
219
|
-
reporter(err);
|
|
220
|
-
};
|
|
221
|
-
const validateEnv = (source) => {
|
|
222
|
-
try {
|
|
223
|
-
parseEnv(effectiveEnvSchema, source, {
|
|
224
|
-
sources: composedEnvSchema.sources,
|
|
225
|
-
...(options.pulumiPrefix ? { pulumiPrefix: options.pulumiPrefix } : {}),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
catch (err) {
|
|
229
|
-
if (err instanceof KumikoBootError)
|
|
230
|
-
reportBootError(err);
|
|
231
|
-
throw err;
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
if (isRenderMode(runMode)) {
|
|
235
|
-
// biome-ignore lint/suspicious/noConsole: dry-run output IS the deliverable
|
|
236
|
-
console.log(renderDryRun(composedEnvSchema, runMode, {
|
|
237
|
-
...(options.pulumiPrefix ? { pulumiPrefix: options.pulumiPrefix } : {}),
|
|
238
|
-
sources: composedEnvSchema.sources,
|
|
239
|
-
}));
|
|
240
|
-
// Tests inject envSource and want a return-value, not exit. Detecting
|
|
241
|
-
// "this is a test" via envSource is brittle; instead exit when running
|
|
242
|
-
// against the real process.env (the deploy-flow), return otherwise.
|
|
243
|
-
if (options.envSource === undefined) {
|
|
244
|
-
process.exit(0);
|
|
245
|
-
}
|
|
166
|
+
const envGate = await runEnvGate(options, "runProdApp");
|
|
167
|
+
if (envGate.kind === "stopped")
|
|
246
168
|
return makeDryRunHandle();
|
|
247
|
-
}
|
|
248
|
-
if (runMode === "boot") {
|
|
249
|
-
const inspection = [
|
|
250
|
-
...inspectEnv(effectiveEnvSchema, rawEnvSource, { sources: composedEnvSchema.sources }),
|
|
251
|
-
...inspectUndeclaredRuntimeEnv(effectiveEnvSchema.shape, rawEnvSource),
|
|
252
|
-
];
|
|
253
|
-
// biome-ignore lint/suspicious/noConsole: boot-mode output IS the deliverable
|
|
254
|
-
console.log(renderEnvInspection(inspection));
|
|
255
|
-
if (inspection.some((row) => row.status === "invalid")) {
|
|
256
|
-
// Missing rows go into the same report: the run fails anyway, so the
|
|
257
|
-
// operator sees every variable to fix in one pass.
|
|
258
|
-
reportBootError(new KumikoBootError(inspection.flatMap((row) => row.status === "invalid" || row.status === "missing"
|
|
259
|
-
? [
|
|
260
|
-
{
|
|
261
|
-
name: row.name,
|
|
262
|
-
kind: row.status,
|
|
263
|
-
message: row.message ?? (row.status === "missing" ? "missing" : "invalid value"),
|
|
264
|
-
source: row.source,
|
|
265
|
-
},
|
|
266
|
-
]
|
|
267
|
-
: [])));
|
|
268
|
-
return makeDryRunHandle();
|
|
269
|
-
}
|
|
270
|
-
// A build pipeline has no deploy secrets: the inventory is the result,
|
|
271
|
-
// and the wiring check below needs every value, so it stops here.
|
|
272
|
-
if (inspection.some((row) => row.status === "missing")) {
|
|
273
|
-
if (options.envSource === undefined) {
|
|
274
|
-
process.exit(0);
|
|
275
|
-
}
|
|
276
|
-
return makeDryRunHandle();
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
// boot-mode with a complete env AND normal boot run the full validation,
|
|
280
|
-
// including object-level refinements the per-field check skips.
|
|
281
|
-
validateEnv(rawEnvSource);
|
|
282
|
-
// Slots the schema marks `kms` are decrypted once here so every consumer
|
|
283
|
-
// below (boot probe, master-key keyring, config) sees the same resolved env.
|
|
284
|
-
const decryptedEnvSource = await resolvePlatformKeks(rawEnvSource, {
|
|
285
|
-
slots: kmsSlotsOf(composedEnvSchema.schema),
|
|
286
|
-
logPrefix: "[runProdApp]",
|
|
287
|
-
});
|
|
288
|
-
// The first parse relaxes ciphertext-only slots, so their decrypted
|
|
289
|
-
// plaintext never met the slot's own validators (.regex/.min/.refine).
|
|
290
|
-
if (kmsSlotsOf(composedEnvSchema.schema).some((slot) => rawEnvSource[slot] === undefined && decryptedEnvSource[slot] !== undefined)) {
|
|
291
|
-
validateEnv(decryptedEnvSource);
|
|
292
|
-
}
|
|
169
|
+
const { runMode, decryptedEnvSource } = envGate;
|
|
293
170
|
// Stacks whose boot options are missing and the declarative boot options
|
|
294
171
|
// (mail, kmsMode, fileStorage, billing, demoMode, seed shortcuts) fail here, before any connection. Their defaults sit under
|
|
295
172
|
// the env so an explicitly set variable wins.
|
|
@@ -432,9 +309,8 @@ export async function runProdApp(options) {
|
|
|
432
309
|
const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
|
|
433
310
|
await waitForDatabase(db, { processName: "runProdApp" });
|
|
434
311
|
// Sprint-8a Tier-Composition auto-wire: scan features for a
|
|
435
|
-
// tenantTierResolver-extension. If found
|
|
436
|
-
//
|
|
437
|
-
// available) before the dispatcher is constructed. App-Author sees
|
|
312
|
+
// tenantTierResolver-extension. If found, build the resolver here (db +
|
|
313
|
+
// registry are available) before the dispatcher is constructed. App-Author sees
|
|
438
314
|
// nothing — `createTierEngineFeature(opts)` mounts + framework auto-wires.
|
|
439
315
|
// One bus per process: the tier resolver, the dispatcher and the file-provider
|
|
440
316
|
// resolver all subscribe to it (invalidation across the API pods).
|
|
@@ -442,16 +318,23 @@ export async function runProdApp(options) {
|
|
|
442
318
|
redisUrl,
|
|
443
319
|
clientOptions: redisClientOptionsFromEnv(envSource),
|
|
444
320
|
});
|
|
445
|
-
let resolvedEffectiveFeatures
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
321
|
+
let resolvedEffectiveFeatures;
|
|
322
|
+
const tierResolverUsage = findTierResolverUsage(features);
|
|
323
|
+
if (tierResolverUsage) {
|
|
324
|
+
resolvedEffectiveFeatures = await requireTierResolverPlugin(tierResolverUsage).build({
|
|
325
|
+
db,
|
|
326
|
+
registry,
|
|
327
|
+
cacheSync,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (bootOptionWiring.featureToggles) {
|
|
331
|
+
const bound = await bootOptionWiring.featureToggles.bind({
|
|
332
|
+
db,
|
|
333
|
+
registry,
|
|
334
|
+
cacheSync,
|
|
335
|
+
tierResolver: resolvedEffectiveFeatures,
|
|
336
|
+
});
|
|
337
|
+
resolvedEffectiveFeatures = bound.resolver;
|
|
455
338
|
}
|
|
456
339
|
// 5. Schema-Drift-Gate (drizzle-frei, kumiko/migrations). `kumiko schema
|
|
457
340
|
// apply` läuft als Deploy-Step VOR dem Container-Rollout. Boot prüft nur:
|
|
@@ -502,7 +385,15 @@ export async function runProdApp(options) {
|
|
|
502
385
|
// wins), which is exactly how fw#2630's cross-replica fanout fix shipped
|
|
503
386
|
// without ever taking effect in production.
|
|
504
387
|
const { sseBroker, ownedRedisSseBroker } = createDefaultSseBroker(envSource);
|
|
505
|
-
const deps = {
|
|
388
|
+
const deps = {
|
|
389
|
+
db,
|
|
390
|
+
redis,
|
|
391
|
+
registry,
|
|
392
|
+
sseBroker,
|
|
393
|
+
env: envSource,
|
|
394
|
+
...(bootCrypto.masterKeyProvider && { masterKeyProvider: bootCrypto.masterKeyProvider }),
|
|
395
|
+
...(bootCrypto.configCipher && { configEncryption: bootCrypto.configCipher }),
|
|
396
|
+
};
|
|
506
397
|
const resolvedExtraContext = typeof options.extraContext === "function"
|
|
507
398
|
? options.extraContext(deps)
|
|
508
399
|
: (options.extraContext ?? {});
|
|
@@ -534,6 +425,7 @@ export async function runProdApp(options) {
|
|
|
534
425
|
...(bootKms.kms && { kms: bootKms.kms }),
|
|
535
426
|
...(bootLogger && { log: bootLogger }),
|
|
536
427
|
...(unsubscribeLinks && { unsubscribeLinks }),
|
|
428
|
+
...(options.appOverrides && { appOverrides: options.appOverrides }),
|
|
537
429
|
});
|
|
538
430
|
const extraContext = {
|
|
539
431
|
...addConfigAccessorFactory({ ...autoExtraContext, ...resolvedExtraContext }, registry),
|
|
@@ -776,6 +668,8 @@ export async function runProdApp(options) {
|
|
|
776
668
|
db,
|
|
777
669
|
redis,
|
|
778
670
|
registry,
|
|
671
|
+
...(bootCrypto.masterKeyProvider && { masterKeyProvider: bootCrypto.masterKeyProvider }),
|
|
672
|
+
...(bootCrypto.configCipher && { configEncryption: bootCrypto.configCipher }),
|
|
779
673
|
metricsFor: createMetricsForFactory(entrypoint.observability.meter),
|
|
780
674
|
dispatchSystemWrite: makeDispatchSystemWrite(entrypoint.dispatcher),
|
|
781
675
|
dispatchSystemQuery: makeDispatchSystemQuery(entrypoint.dispatcher),
|
|
@@ -820,7 +714,14 @@ export async function runProdApp(options) {
|
|
|
820
714
|
if (seedDispatcher && seedContext) {
|
|
821
715
|
const createFileRef = makeSeedCreateFileRef({ db, registry, context: seedContext });
|
|
822
716
|
for (const seed of customSeeds) {
|
|
823
|
-
await seed({
|
|
717
|
+
await seed({
|
|
718
|
+
db,
|
|
719
|
+
registry,
|
|
720
|
+
dispatcher: seedDispatcher,
|
|
721
|
+
createFileRef,
|
|
722
|
+
...(bootCrypto.masterKeyProvider && { masterKeyProvider: bootCrypto.masterKeyProvider }),
|
|
723
|
+
...(bootCrypto.configCipher && { configEncryption: bootCrypto.configCipher }),
|
|
724
|
+
});
|
|
824
725
|
}
|
|
825
726
|
}
|
|
826
727
|
// ES-Operations / Seed-Migrations (Phase 1). Läuft NACH applyBootSeeds +
|
package/dist/run-worker-app.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { ServerOptions } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
3
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
4
|
-
import type {
|
|
4
|
+
import type { FeatureDefinition, Registry, ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
5
|
import type { WorkerEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
|
|
6
|
+
import type { ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
|
|
6
7
|
import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
|
|
7
8
|
import { type ObservabilityOptions, type ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
|
|
8
9
|
import type { MasterKeyProvider, SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
9
10
|
import type { Redis } from "ioredis";
|
|
10
11
|
import { type WorkerMetricsOptions, type WorkerMetricsServer } from "./boot/worker-metrics-server.ts";
|
|
12
|
+
import type { BillingBootOption } from "./boot-options/billing-option.ts";
|
|
13
|
+
import type { FileStorageBootOption } from "./boot-options/file-storage-option.ts";
|
|
14
|
+
import type { KmsMode } from "./boot-options/kms-mode.ts";
|
|
15
|
+
import type { MailBootOption } from "./boot-options/mail-option.ts";
|
|
11
16
|
import { type SystemWireDeps } from "./extra-routes-deps.ts";
|
|
12
17
|
export type WorkerContextOption = Record<string, unknown> | ((deps: WorkerDeps) => Record<string, unknown>);
|
|
13
18
|
export type WorkerDeps = {
|
|
@@ -24,7 +29,7 @@ export type WorkerWireDeps = SystemWireDeps & {
|
|
|
24
29
|
readonly lifecycle: WorkerEntrypoint["lifecycle"];
|
|
25
30
|
/** The worker's ctx.secrets — what `createInboundMailSupervisor` needs as
|
|
26
31
|
* `providerCtx.secrets` (the IMAP provider reads credentials through it,
|
|
27
|
-
* with `
|
|
32
|
+
* with `auditUserId` set to a system identity for the audit trail). Undefined
|
|
28
33
|
* when the secrets feature is not mounted or no master key is available. */
|
|
29
34
|
readonly secrets?: SecretsContext;
|
|
30
35
|
};
|
|
@@ -79,8 +84,22 @@ export type RunWorkerAppOptions = {
|
|
|
79
84
|
* `entrypoint.start()` — the hook itself is responsible for starting
|
|
80
85
|
* its component and registering a shutdown hook on `lifecycle`. */
|
|
81
86
|
readonly wireComponents?: (deps: WorkerWireDeps) => Promise<void> | void;
|
|
82
|
-
/**
|
|
83
|
-
readonly
|
|
87
|
+
/** Same as RunProdAppOptions["mail"]: mounts channel-email so the worker delivers what the API queues. */
|
|
88
|
+
readonly mail?: MailBootOption;
|
|
89
|
+
/** Same as RunProdAppOptions["fileStorage"]: mounts the file providers the worker's file jobs use. */
|
|
90
|
+
readonly fileStorage?: FileStorageBootOption;
|
|
91
|
+
/** Same as RunProdAppOptions["kmsMode"]; cannot be combined with `kms` / `blindIndexKey` / `allowPlaintextPii`. */
|
|
92
|
+
readonly kmsMode?: KmsMode;
|
|
93
|
+
/** Same as RunProdAppOptions["billing"]: mounts subscription-stripe so the registry matches the API. The webhook route belongs to the API process. */
|
|
94
|
+
readonly billing?: BillingBootOption;
|
|
95
|
+
/** Same as RunProdAppOptions["featureToggles"]: mounts feature-toggles and layers the operator switches over the tier resolver. */
|
|
96
|
+
readonly featureToggles?: boolean;
|
|
97
|
+
/** Composed env schema (see RunProdAppOptions["envSchema"]). With it the worker validates its env,
|
|
98
|
+
* derives the KMS slots from it (`kmsSlots` is ignored) and understands every `KUMIKO_DRY_RUN_ENV`
|
|
99
|
+
* mode. Without it only `KUMIKO_DRY_RUN_ENV=boot` is available. */
|
|
100
|
+
readonly envSchema?: ComposedEnvSchema;
|
|
101
|
+
readonly pulumiPrefix?: string;
|
|
102
|
+
readonly bootErrorReporter?: (err: KumikoBootError) => void;
|
|
84
103
|
/** Override `process.env` for env-validation (see
|
|
85
104
|
* RunProdAppOptions["envSource"]). */
|
|
86
105
|
readonly envSource?: Record<string, string | undefined>;
|
package/dist/run-worker-app.js
CHANGED
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
// DATABASE_URL, REDIS_URL, JWT_SECRET — PORT is not needed.
|
|
17
17
|
import { assertSeedModeNotInProduction } from "@cosmicdrift/kumiko-framework/event-store";
|
|
18
18
|
import { createMetricsForFactory, } from "@cosmicdrift/kumiko-framework/observability";
|
|
19
|
+
import { runEnvGate } from "./boot/env-gate.js";
|
|
19
20
|
import { startPiiEventBackfillOnBoot } from "./boot/pii-event-backfill-on-boot.js";
|
|
20
21
|
import { recordAppStartedOnBoot } from "./boot/record-app-started-on-boot.js";
|
|
21
|
-
import { bootWorkerProcess, resolveWorkerEnvSource } from "./boot/worker-boot-core.js";
|
|
22
|
+
import { bootWorkerProcess, resolveWorkerEnvSource, } from "./boot/worker-boot-core.js";
|
|
22
23
|
import { startWorkerMetricsServer, } from "./boot/worker-metrics-server.js";
|
|
24
|
+
import { resolveBootOptionWiring } from "./boot-options/resolve-boot-options.js";
|
|
25
|
+
import { assertStackBootRequirements } from "./boot-options/stack-boot-requirements.js";
|
|
23
26
|
import { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "./extra-routes-deps.js";
|
|
24
27
|
function makeBootModeHandle() {
|
|
25
28
|
return {
|
|
@@ -29,14 +32,53 @@ function makeBootModeHandle() {
|
|
|
29
32
|
stop: async () => { },
|
|
30
33
|
};
|
|
31
34
|
}
|
|
35
|
+
// undefined: a dry-run mode already produced its output and the worker must not boot.
|
|
36
|
+
async function resolveGatedEnvSource(options) {
|
|
37
|
+
if (!options.envSchema)
|
|
38
|
+
return resolveWorkerEnvSource(options, "runWorkerApp");
|
|
39
|
+
const envGate = await runEnvGate({ ...options, envSchema: options.envSchema }, "runWorkerApp");
|
|
40
|
+
return envGate.kind === "stopped" ? undefined : envGate.decryptedEnvSource;
|
|
41
|
+
}
|
|
42
|
+
// The API enforces the same requirement; a worker that skipped it would keep running
|
|
43
|
+
// jobs and event handlers of features an operator switched off.
|
|
44
|
+
function assertWorkerStackRequirements(options) {
|
|
45
|
+
assertStackBootRequirements({ features: options.features, ...(options.featureToggles && { featureToggles: true }) }, ["featureToggles"]);
|
|
46
|
+
}
|
|
32
47
|
export async function runWorkerApp(options) {
|
|
33
48
|
assertSeedModeNotInProduction();
|
|
34
|
-
const
|
|
49
|
+
const resolvedEnvSource = await resolveGatedEnvSource(options);
|
|
50
|
+
if (resolvedEnvSource === undefined)
|
|
51
|
+
return makeBootModeHandle();
|
|
35
52
|
// biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
|
|
36
53
|
console.log("[runWorkerApp] booting Kumiko worker…");
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
assertWorkerStackRequirements(options);
|
|
55
|
+
const wiring = await resolveBootOptionWiring(options, resolvedEnvSource);
|
|
56
|
+
const envSource = Object.keys(wiring.envDefaults).length > 0
|
|
57
|
+
? { ...wiring.envDefaults, ...resolvedEnvSource }
|
|
58
|
+
: resolvedEnvSource;
|
|
59
|
+
let boot;
|
|
60
|
+
try {
|
|
61
|
+
boot = await bootWorkerProcess({
|
|
62
|
+
...options,
|
|
63
|
+
features: [...options.features, ...wiring.features],
|
|
64
|
+
...(wiring.kms.kms && { kms: wiring.kms.kms }),
|
|
65
|
+
...(wiring.kms.blindIndexKey !== undefined && { blindIndexKey: wiring.kms.blindIndexKey }),
|
|
66
|
+
...(wiring.kms.allowPlaintextPii !== undefined && {
|
|
67
|
+
allowPlaintextPii: wiring.kms.allowPlaintextPii,
|
|
68
|
+
}),
|
|
69
|
+
}, envSource, {
|
|
70
|
+
processName: "runWorkerApp",
|
|
71
|
+
...(wiring.featureToggles && { featureToggles: wiring.featureToggles }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
await wiring.kms.close().catch(() => { });
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
if (boot.kind === "dry-run") {
|
|
79
|
+
await wiring.kms.close();
|
|
39
80
|
return makeBootModeHandle();
|
|
81
|
+
}
|
|
40
82
|
const { db, redis, registry, entrypoint } = boot;
|
|
41
83
|
// Metrics first: a port clash must fail the boot before start() begins
|
|
42
84
|
// consuming jobs/events. boot.close() runs the "metrics-server" shutdown
|
|
@@ -52,11 +94,15 @@ export async function runWorkerApp(options) {
|
|
|
52
94
|
// The boot failure is the actionable error; a close failure on a
|
|
53
95
|
// half-started worker must not replace it.
|
|
54
96
|
await boot.close().catch(() => { });
|
|
97
|
+
await wiring.kms.close().catch(() => { });
|
|
55
98
|
throw error;
|
|
56
99
|
}
|
|
57
100
|
const handle = {
|
|
58
101
|
entrypoint,
|
|
59
|
-
stop:
|
|
102
|
+
stop: async () => {
|
|
103
|
+
await boot.close();
|
|
104
|
+
await wiring.kms.close();
|
|
105
|
+
},
|
|
60
106
|
...(metricsServer && { metricsServer }),
|
|
61
107
|
};
|
|
62
108
|
await recordAppStartedOnBoot({ db, envSource });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-server-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.377.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 tooling.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -88,19 +88,18 @@
|
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
92
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
93
|
-
"@cosmicdrift/kumiko-
|
|
94
|
-
"@cosmicdrift/kumiko-renderer-mail-html": "0.
|
|
95
|
-
"@cosmicdrift/kumiko-
|
|
96
|
-
"@cosmicdrift/kumiko-types": "0.373.0",
|
|
91
|
+
"@cosmicdrift/kumiko-bundled-features": "0.377.0",
|
|
92
|
+
"@cosmicdrift/kumiko-framework": "0.377.0",
|
|
93
|
+
"@cosmicdrift/kumiko-renderer": "0.377.0",
|
|
94
|
+
"@cosmicdrift/kumiko-renderer-mail-html": "0.377.0",
|
|
95
|
+
"@cosmicdrift/kumiko-types": "0.377.0",
|
|
97
96
|
"bun-types": "^1.3.13",
|
|
98
97
|
"ioredis": "^5.10.1",
|
|
99
98
|
"temporal-polyfill": "^0.3.2",
|
|
100
99
|
"zod": "^4.4.3"
|
|
101
100
|
},
|
|
102
101
|
"devDependencies": {
|
|
103
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
102
|
+
"@cosmicdrift/kumiko-testing": "0.377.0"
|
|
104
103
|
},
|
|
105
104
|
"publishConfig": {
|
|
106
105
|
"registry": "https://registry.npmjs.org",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,55 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.376.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "effectiveFeatures is gone from runProdApp, runDevApp and runWorkerApp",
|
|
6
|
+
"detail": "The effectiveFeatures option of the boot functions is removed. Global operator switches come from the featureToggles option, tier gating from the tier-engine feature, which the boot functions wire themselves.",
|
|
7
|
+
"migration": "Replace a hand-built effectiveFeatures resolver with featureToggles true (global switches) and mount createTierEngineFeature for tier gating. A custom resolver that did neither has no replacement on the boot functions; wire it through createKumikoServer or setupTestStack in tests. The boot functions now abort when the option is still passed, because an ignored option would drop an operator kill-switch silently; the same goes for a feature-toggles feature in features without the featureToggles option, or with it set (the option mounts the feature)."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.376.0",
|
|
11
|
+
"type": "breaking",
|
|
12
|
+
"title": "The default mail resolver only reaches members of the notifying tenant",
|
|
13
|
+
"detail": "The default resolveEmail of the mail boot option returns an address for a notification from a tenant other than the system tenant only if the user is a member of that tenant or holds the SystemAdmin role in the stored user row. Everyone else resolves to null and the mail is skipped. The new option mail.recipientScope takes \"membership\" (default) or \"any\", which restores the old behaviour of reaching every user. EmailChannelOptions resolveEmail now receives the tenant database as TenantDb instead of unknown.",
|
|
14
|
+
"migration": "Apps that notify users outside the notifying tenant on purpose, for example merchants reached from a SystemAdmin context, set mail recipientScope to \"any\" or keep their own resolveEmail. Apps whose notifications go to members of the active tenant need no change. Known consumers on the default: offlot-app (main.ts) reaches merchants and SystemAdmins across tenants and needs recipientScope any; publicstatus (main.ts), show-pony, solon (main.ts) and kumiko-studio (main.ts) take the default, which fits notifications to members of the active tenant. money-horse and phronexsis keep their own resolveEmail. Per-app resolvers that only copied the membership check can be deleted. A custom resolveEmail that cast ctx.db can drop the cast."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.376.0",
|
|
18
|
+
"type": "improvement",
|
|
19
|
+
"title": "runDevApp and runWorkerApp take the declarative boot options",
|
|
20
|
+
"detail": "mail, fileStorage, kmsMode, billing, seeds, demoMode, envSchema and featureToggles work in runDevApp, and mail, fileStorage, kmsMode, billing, featureToggles and envSchema in runWorkerApp, through the same resolve functions as runProdApp. The worker honours KUMIKO_DRY_RUN_ENV with an env schema. A stack that sets runSingleInstance false only warns when no worker mounts the features.",
|
|
21
|
+
"migration": "RunDevAppOptions.seeds is now `readonly SeedFn[] | RunDevAppSeedsConfig`. Code that spreads `seeds` from an object typed as RunDevAppOptions no longer type-checks; declare that object with `satisfies RunDevAppOptions` so the array type survives, or build the seed list separately. An app that lists createFeatureTogglesFeature() in the features passed to runDevApp or runProdApp removes it there and sets `featureToggles: true`."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"version": "0.376.0",
|
|
25
|
+
"type": "improvement",
|
|
26
|
+
"title": "featureToggles, appOverrides and secrets in boot deps",
|
|
27
|
+
"detail": "featureToggles true mounts feature-toggles and layers the global switches over the tier resolver in runProdApp, runDevApp and runWorkerApp. appOverrides sets tenant-model, branding and seo defaults. masterKeyProvider and configEncryption reach extraContext factories, system wiring and seed functions, so an app no longer resolves its KEK a second time."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"version": "0.376.0",
|
|
31
|
+
"type": "improvement",
|
|
32
|
+
"title": "Boot errors for removed and half-wired feature toggles",
|
|
33
|
+
"detail": "runProdApp, runDevApp and runWorkerApp abort when the removed effectiveFeatures option is still passed, and when feature-toggles sits in features without the featureToggles option. runWorkerApp now enforces a stack that expects featureToggles, so a worker cannot keep running jobs and event handlers of features an operator switched off."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"version": "0.376.0",
|
|
37
|
+
"type": "improvement",
|
|
38
|
+
"title": "Named S3 provider names are validated at boot",
|
|
39
|
+
"detail": "fileStorage.providers refuses the names s3-env, s3 and inmemory, which the bundled providers register, and a name listed twice. A tenant admin can still select any mounted provider through the file-foundation provider config, so name a provider only if every tenant may use its bucket."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"version": "0.376.0",
|
|
43
|
+
"type": "improvement",
|
|
44
|
+
"title": "Named S3 providers and a corrected SMTP_HOST description",
|
|
45
|
+
"detail": "fileStorage.providers takes entries with name, envPrefix and lazy, each validated through createS3EnvSchema with its own prefix. The SMTP_HOST description in the env schema of mail smtp-env now says the variable is required."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"version": "0.376.0",
|
|
49
|
+
"type": "improvement",
|
|
50
|
+
"title": "Demo-mode accounts can carry a label",
|
|
51
|
+
"detail": "demoMode.notice.accounts entries may be { role, label? } next to a bare role; the GET /demo-mode payload then lists { role, label?, email, password } so the banner can name each login. Bare roles behave as before."
|
|
52
|
+
},
|
|
2
53
|
{
|
|
3
54
|
"version": "0.372.0",
|
|
4
55
|
"type": "fix",
|