@cosmicdrift/kumiko-server-runtime 0.342.0 → 0.344.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/worker-boot-core.js +9 -3
- package/dist/build-prod-bundle.js +51 -16
- package/dist/run-prod-app-boot-context.d.ts +4 -0
- package/dist/run-prod-app-boot-context.js +16 -2
- package/dist/run-prod-app.d.ts +14 -6
- package/dist/run-prod-app.js +25 -16
- package/dist/run-worker-app.d.ts +3 -1
- package/package.json +5 -5
- package/src/changes.json +25 -0
|
@@ -16,7 +16,7 @@ import { Redis } from "ioredis";
|
|
|
16
16
|
import { composeFeatures } from "../compose-features.js";
|
|
17
17
|
import { assertPiiBootInvariants } from "../pii-boot-gate.js";
|
|
18
18
|
import { requireEnv } from "../run-prod-app.js";
|
|
19
|
-
import { addConfigAccessorFactory, buildBootExtraContext } from "../run-prod-app-boot-context.js";
|
|
19
|
+
import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, } from "../run-prod-app-boot-context.js";
|
|
20
20
|
import { resolveBootCrypto } from "./boot-crypto.js";
|
|
21
21
|
import { jobRunLoggerCallbacks } from "./job-run-logger.js";
|
|
22
22
|
import { assertWorkerMetricsOptions } from "./worker-metrics-server.js";
|
|
@@ -71,7 +71,12 @@ export async function bootWorkerProcess(options, envSource, profile) {
|
|
|
71
71
|
includeBundled,
|
|
72
72
|
...(profile.authOptions && { authOptions: profile.authOptions }),
|
|
73
73
|
});
|
|
74
|
-
|
|
74
|
+
const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
|
|
75
|
+
validateBoot(features, {
|
|
76
|
+
env: envSource,
|
|
77
|
+
...(bootCrypto.entityFieldCipher && { entityFieldCipher: bootCrypto.entityFieldCipher }),
|
|
78
|
+
...options.validateBootOptions,
|
|
79
|
+
});
|
|
75
80
|
warnIfNonUtcServerTimeZone();
|
|
76
81
|
assertWorkerMetricsOptions(options.metrics, options.observability, processName);
|
|
77
82
|
assertPiiBootInvariants(features, {
|
|
@@ -124,10 +129,10 @@ export async function bootWorkerProcess(options, envSource, profile) {
|
|
|
124
129
|
const resolvedExtraContext = typeof options.extraContext === "function"
|
|
125
130
|
? options.extraContext(deps)
|
|
126
131
|
: (options.extraContext ?? {});
|
|
127
|
-
const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
|
|
128
132
|
configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
|
|
129
133
|
configurePiiSubjectKms(options.kms);
|
|
130
134
|
configureBlindIndexKey(options.blindIndexKey);
|
|
135
|
+
const bootLogger = loggerFromExtraContext(resolvedExtraContext);
|
|
131
136
|
const autoExtraContext = buildBootExtraContext({
|
|
132
137
|
db,
|
|
133
138
|
features,
|
|
@@ -137,6 +142,7 @@ export async function bootWorkerProcess(options, envSource, profile) {
|
|
|
137
142
|
crypto: bootCrypto,
|
|
138
143
|
...(options.kms && { kms: options.kms }),
|
|
139
144
|
...(profile.deliverQueuedInline === true && { deliverQueuedInline: true }),
|
|
145
|
+
...(bootLogger && { log: bootLogger }),
|
|
140
146
|
});
|
|
141
147
|
const extraContext = addConfigAccessorFactory({ ...autoExtraContext, ...resolvedExtraContext }, registry);
|
|
142
148
|
const jobLogger = jobRunLoggerCallbacks(registry, db);
|
|
@@ -48,9 +48,8 @@
|
|
|
48
48
|
// alles andere (public/) → default (auto-cache)
|
|
49
49
|
import { createHash } from "node:crypto";
|
|
50
50
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
51
|
-
import { cp, mkdir,
|
|
52
|
-
import {
|
|
53
|
-
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
51
|
+
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
52
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
54
53
|
import { isPlainObject, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
55
54
|
import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
56
55
|
import { Temporal } from "temporal-polyfill";
|
|
@@ -530,21 +529,57 @@ export async function runTailwindOnce(entry, cwd) {
|
|
|
530
529
|
if (!canResolveTailwindStylesheet(entry, { bun: bunResolver, cwd })) {
|
|
531
530
|
throw new Error(`[kumiko build] tailwindcss nicht auflösbar für ${entry} — peer dependency fehlt am Stylesheet-Ort.`);
|
|
532
531
|
}
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
532
|
+
const { compile, optimize, Scanner } = await loadTailwindEngine(cliPath, bunResolver);
|
|
533
|
+
const input = resolve(cwd, entry);
|
|
534
|
+
if (!existsSync(input)) {
|
|
535
|
+
throw new Error(`[kumiko build] tailwind: stylesheet not found: ${input}`);
|
|
536
|
+
}
|
|
537
|
+
const css = await readFile(input, "utf8");
|
|
538
|
+
// Mirrors `@tailwindcss/cli -i <entry> -o <file> --minify` option for option, so the output
|
|
539
|
+
// stays byte-identical to the CLI without spawning a child process.
|
|
540
|
+
const compiler = await compile(css, {
|
|
541
|
+
from: input,
|
|
542
|
+
base: dirname(input),
|
|
543
|
+
onDependency: () => { },
|
|
540
544
|
});
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
545
|
+
const rootSources = compiler.root === "none"
|
|
546
|
+
? []
|
|
547
|
+
: compiler.root === null
|
|
548
|
+
? [{ base: resolve(cwd), pattern: "**/*", negated: false }]
|
|
549
|
+
: [{ ...compiler.root, negated: false }];
|
|
550
|
+
const scanner = new Scanner({
|
|
551
|
+
sources: [
|
|
552
|
+
...rootSources,
|
|
553
|
+
...compiler.sources,
|
|
554
|
+
{ base: dirname(process.execPath), pattern: basename(process.execPath), negated: true },
|
|
555
|
+
],
|
|
556
|
+
});
|
|
557
|
+
return optimize(compiler.build(scanner.scan()), { file: input, minify: true }).code;
|
|
558
|
+
}
|
|
559
|
+
function isRecord(value) {
|
|
560
|
+
return typeof value === "object" && value !== null;
|
|
561
|
+
}
|
|
562
|
+
// Dynamic import of an external package: only the shape is checkable, the signatures are the documented Tailwind v4 API.
|
|
563
|
+
function isTailwindEngine(candidate) {
|
|
564
|
+
return (typeof candidate.compile === "function" &&
|
|
565
|
+
typeof candidate.optimize === "function" &&
|
|
566
|
+
typeof candidate.Scanner === "function");
|
|
567
|
+
}
|
|
568
|
+
// Both packages are the CLI's own dependencies, so they resolve from the CLI package directory
|
|
569
|
+
// (same version the CLI would run), not from the app root.
|
|
570
|
+
async function loadTailwindEngine(cliPath, bun) {
|
|
571
|
+
const cliDir = dirname(bun.resolveSync("@tailwindcss/cli/package.json", dirname(cliPath)));
|
|
572
|
+
const node = await import(bun.resolveSync("@tailwindcss/node", cliDir));
|
|
573
|
+
const oxide = await import(bun.resolveSync("@tailwindcss/oxide", cliDir));
|
|
574
|
+
const engine = {
|
|
575
|
+
compile: isRecord(node) ? node["compile"] : undefined,
|
|
576
|
+
optimize: isRecord(node) ? node["optimize"] : undefined,
|
|
577
|
+
Scanner: isRecord(oxide) ? oxide["Scanner"] : undefined,
|
|
578
|
+
};
|
|
579
|
+
if (!isTailwindEngine(engine)) {
|
|
580
|
+
throw new Error("[kumiko build] @tailwindcss/node or @tailwindcss/oxide has an unexpected API.");
|
|
544
581
|
}
|
|
545
|
-
|
|
546
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
547
|
-
return css;
|
|
582
|
+
return engine;
|
|
548
583
|
}
|
|
549
584
|
// Exact `<base>-<hash>.js` match: a plain prefix check would let
|
|
550
585
|
// `client-admin-legacy-<hash>.js` satisfy the `client-admin` entry. The hash is
|
|
@@ -4,12 +4,14 @@ import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
|
4
4
|
import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
5
5
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
6
6
|
import type { ConfigResolver, FeatureDefinition, Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
7
8
|
import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
8
9
|
import { type BootCrypto } from "./boot/boot-crypto.js";
|
|
9
10
|
import type { AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup } from "./run-prod-app.js";
|
|
10
11
|
export declare function addConfigAccessorFactory<T extends {
|
|
11
12
|
readonly configResolver?: ConfigResolver;
|
|
12
13
|
}>(resolved: T, registry: Registry): T;
|
|
14
|
+
export declare function loggerFromExtraContext(extraContext: Record<string, unknown>): Logger | undefined;
|
|
13
15
|
export declare function buildBootExtraContext(opts: {
|
|
14
16
|
readonly db: DbConnection;
|
|
15
17
|
readonly features: readonly FeatureDefinition[];
|
|
@@ -23,6 +25,8 @@ export declare function buildBootExtraContext(opts: {
|
|
|
23
25
|
/** One-shot process (runBootstrap): nothing drains its job queue once it
|
|
24
26
|
* exits, so queued channels must send inline instead of enqueueing. */
|
|
25
27
|
readonly deliverQueuedInline?: boolean;
|
|
28
|
+
/** Receives redacted delivery failures; without it they go to the console. */
|
|
29
|
+
readonly log?: Logger;
|
|
26
30
|
}): Record<string, unknown>;
|
|
27
31
|
/** Die Auth-Felder die resolveAuthMail liest/normalisiert — beide
|
|
28
32
|
* App-Auth-Typen (prod + dev) erfüllen das strukturell. */
|
|
@@ -4,7 +4,7 @@ import { resolveSessionStore } from "@cosmicdrift/kumiko-bundled-features/auth-f
|
|
|
4
4
|
import { bindMfaRevokeAllOtherSessionsFromFeature, bindRevokeAllPatTokensFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
5
5
|
import { createSmtpTransportFromEnv } from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
6
6
|
import { buildEnvConfigOverrides, createConfigAccessorFactory, createConfigResolver, } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
7
|
-
import { collectChannels, createDeliveryService, DELIVERY_FEATURE, } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
7
|
+
import { collectChannels, createDeliveryNotifyFactory, createDeliveryService, DELIVERY_FEATURE, } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
8
8
|
import { bindPatAutoRevokeOnPasswordChangeFromFeature, revokeAllPatTokensForUser, } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
9
9
|
import { createSecretsContext, SECRETS_FEATURE_NAME, } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
10
10
|
import { bindAutoRevokeFromFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
@@ -52,8 +52,21 @@ function buildDeliveryNotifyFactory(opts) {
|
|
|
52
52
|
...(opts.secrets && { secrets: opts.secrets }),
|
|
53
53
|
...(opts.sseBroker && { sseBroker: opts.sseBroker }),
|
|
54
54
|
...(opts.escapeHatchAuditSink && { escapeHatchAuditSink: opts.escapeHatchAuditSink }),
|
|
55
|
+
...(opts.log && { log: opts.log }),
|
|
55
56
|
});
|
|
56
|
-
return (
|
|
57
|
+
return createDeliveryNotifyFactory(deliveryService, {
|
|
58
|
+
deliverQueuedInline: opts.deliverQueuedInline === true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function isLogger(value) {
|
|
62
|
+
if (typeof value !== "object" || value === null)
|
|
63
|
+
return false;
|
|
64
|
+
return ["info", "warn", "error", "debug", "child"].every((method) => method in value && typeof Reflect.get(value, method) === "function");
|
|
65
|
+
}
|
|
66
|
+
// Prod context.log only comes from the app's extraContext.
|
|
67
|
+
export function loggerFromExtraContext(extraContext) {
|
|
68
|
+
const candidate = extraContext["log"];
|
|
69
|
+
return isLogger(candidate) ? candidate : undefined;
|
|
57
70
|
}
|
|
58
71
|
function resolveEscapeHatchAuditSink(features, db) {
|
|
59
72
|
const hasAuditFeature = features.some((f) => f.name === AUDIT_FEATURE);
|
|
@@ -86,6 +99,7 @@ export function buildBootExtraContext(opts) {
|
|
|
86
99
|
...(secrets && { secrets }),
|
|
87
100
|
...(opts.sseBroker && { sseBroker: opts.sseBroker }),
|
|
88
101
|
...(escapeHatchAuditSink && { escapeHatchAuditSink }),
|
|
102
|
+
...(opts.log && { log: opts.log }),
|
|
89
103
|
deliverQueuedInline: opts.deliverQueuedInline,
|
|
90
104
|
}),
|
|
91
105
|
}),
|
package/dist/run-prod-app.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
|
6
6
|
import { type EffectiveFeaturesResolver, type FeatureDefinition, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { type AllInOneEntrypoint, type ApiEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
|
|
8
8
|
import { type ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
|
|
9
|
+
import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
|
|
9
10
|
import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
|
|
10
11
|
import type { ObservabilityOptions, ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
|
|
11
12
|
import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
@@ -14,7 +15,7 @@ import { Redis } from "ioredis";
|
|
|
14
15
|
import { type SystemWireDeps } from "./extra-routes-deps.js";
|
|
15
16
|
import { type SecurityHeadersOption } from "./security-headers.js";
|
|
16
17
|
export { buildBunServeOptions } from "./bun-serve-options.js";
|
|
17
|
-
export { addConfigAccessorFactory, buildBootExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
|
|
18
|
+
export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
|
|
18
19
|
export { staticCachePolicy } from "./run-prod-app-static-files.js";
|
|
19
20
|
export declare function requireEnv(name: string, src?: Record<string, string | undefined>, context?: string): string;
|
|
20
21
|
/** Wrapper API for the password-reset flow.
|
|
@@ -156,9 +157,13 @@ export type RunProdAppAuthOptions = {
|
|
|
156
157
|
};
|
|
157
158
|
/** Hook for app-specific seeding — runs after the admin (when auth is
|
|
158
159
|
* active). Each seed is responsible for its own idempotence (seeds are
|
|
159
|
-
* expected to check "is my row already there?" before inserting).
|
|
160
|
+
* expected to check "is my row already there?" before inserting). Write
|
|
161
|
+
* through `dispatcher.write(...)` rather than `db` so projections and
|
|
162
|
+
* hooks run; `registry` is the boot registry. */
|
|
160
163
|
export type ProdSeedFn = (deps: {
|
|
161
164
|
db: import("@cosmicdrift/kumiko-framework/db").DbConnection;
|
|
165
|
+
registry: import("@cosmicdrift/kumiko-framework/engine").Registry;
|
|
166
|
+
dispatcher: import("@cosmicdrift/kumiko-framework/pipeline").Dispatcher;
|
|
162
167
|
}) => Promise<void>;
|
|
163
168
|
/** Boot-Time-Deps die `extraContext` + `anonymousAccess` Factories als
|
|
164
169
|
* Argument bekommen. Closure dann in der returned Config (z.B. ein
|
|
@@ -306,10 +311,11 @@ export type RunProdAppOptions = {
|
|
|
306
311
|
* no locale signal. Merged into AppContext before extraContext (app wins). */
|
|
307
312
|
readonly defaultLocale?: string;
|
|
308
313
|
readonly extraContext?: ExtraContextOption;
|
|
309
|
-
/** MasterKeyProvider
|
|
310
|
-
* `
|
|
311
|
-
*
|
|
312
|
-
*
|
|
314
|
+
/** MasterKeyProvider replacing the env KEK (`KUMIKO_SECRETS_MASTER_KEY_V<n>`)
|
|
315
|
+
* for `ctx.secrets`, encrypted config keys and `encrypted: true` entity
|
|
316
|
+
* fields. Default: `createEnvMasterKeyProvider`. Use it for KMS backends
|
|
317
|
+
* (AWS/GCP/Azure); with it set, `KUMIKO_SECRETS_MASTER_KEY_V1` is not
|
|
318
|
+
* required in the env. */
|
|
313
319
|
readonly masterKey?: MasterKeyProvider;
|
|
314
320
|
/** Subject-Key-Adapter für Crypto-Shredding (DSGVO Art. 17). Wenn gesetzt,
|
|
315
321
|
* steht er Feature-Code als `ctx.kms` zur Verfügung und der Boot prüft
|
|
@@ -447,6 +453,8 @@ export type RunProdAppOptions = {
|
|
|
447
453
|
* `auth.trustedProxyHops` and `KUMIKO_TRUSTED_PROXY_HOPS`. Default 0 =
|
|
448
454
|
* trust no proxy header, only the socket address (or "unknown") counts. */
|
|
449
455
|
readonly trustedProxyHops?: number;
|
|
456
|
+
/** Links the app's own error reasons to its own docs (`docsUrl` in error responses); framework reasons keep the framework docs link. */
|
|
457
|
+
readonly errorDocs?: ErrorDocsConfig;
|
|
450
458
|
};
|
|
451
459
|
export type ProdAppHandle = {
|
|
452
460
|
/** The composed entrypoint — AllInOne (runSingleInstance, default) or
|
package/dist/run-prod-app.js
CHANGED
|
@@ -44,6 +44,7 @@ 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 } 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";
|
|
47
48
|
import { SESSIONS_FEATURE } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
48
49
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
49
50
|
import { UserQueries } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
@@ -52,7 +53,7 @@ import { configureBlindIndexKey, configurePiiSubjectKms, resolvePlatformKeks, }
|
|
|
52
53
|
import { configureEntityFieldEncryption, createDbConnection, } from "@cosmicdrift/kumiko-framework/db";
|
|
53
54
|
import { collectWriteHandlerQns, createRegistry, findTierResolverUsage, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
|
|
54
55
|
import { createAllInOneEntrypoint, createApiEntrypoint, } from "@cosmicdrift/kumiko-framework/entrypoint";
|
|
55
|
-
import { KumikoBootError, kmsSlotsOf, parseEnv, } from "@cosmicdrift/kumiko-framework/env";
|
|
56
|
+
import { KumikoBootError, kmsSlotsOf, parseEnv, withOptionalEnvKeys, } from "@cosmicdrift/kumiko-framework/env";
|
|
56
57
|
import { renderDryRun } from "@cosmicdrift/kumiko-framework/env/dry-run";
|
|
57
58
|
import { createEsOperationsTable, createSeedMigrationContext, runPendingSeedMigrations, } from "@cosmicdrift/kumiko-framework/es-ops";
|
|
58
59
|
import { assertKumikoSchemaCurrent, SchemaDriftError, } from "@cosmicdrift/kumiko-framework/migrations";
|
|
@@ -68,14 +69,14 @@ import { buildBunServeOptions, resolveDerivedMaxRequestBodySize } from "./bun-se
|
|
|
68
69
|
import { buildComposeAuthOptions, composeFeatures } from "./compose-features.js";
|
|
69
70
|
import { makeDispatchSystemWrite } from "./extra-routes-deps.js";
|
|
70
71
|
import { assertPiiBootInvariants } from "./pii-boot-gate.js";
|
|
71
|
-
import { addConfigAccessorFactory, buildBootExtraContext, buildProdSessionAuth, resolveAuthMail, wireProdPatAutoRevoke, } from "./run-prod-app-boot-context.js";
|
|
72
|
+
import { addConfigAccessorFactory, buildBootExtraContext, buildProdSessionAuth, loggerFromExtraContext, resolveAuthMail, wireProdPatAutoRevoke, } from "./run-prod-app-boot-context.js";
|
|
72
73
|
import { buildStaticFallback } from "./run-prod-app-static-files.js";
|
|
73
74
|
import { withSecurityHeaders } from "./security-headers.js";
|
|
74
75
|
import { assertSessionBootInvariants } from "./session-boot-gate.js";
|
|
75
76
|
import { shouldWireProdSessions } from "./session-wiring.js";
|
|
76
77
|
import { stripNoRouteMatchHeader } from "./try-hono-first.js";
|
|
77
78
|
export { buildBunServeOptions } from "./bun-serve-options.js";
|
|
78
|
-
export { addConfigAccessorFactory, buildBootExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
|
|
79
|
+
export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
|
|
79
80
|
export { staticCachePolicy } from "./run-prod-app-static-files.js";
|
|
80
81
|
// Strict env-var read. Throws with a clear hint when missing — better
|
|
81
82
|
// than discovering a Postgres-connection-refused 30s into the boot.
|
|
@@ -191,7 +192,10 @@ export async function runProdApp(options) {
|
|
|
191
192
|
// a real env-check (all required vars present + schema-valid) before
|
|
192
193
|
// it asserts feature-wiring works.
|
|
193
194
|
try {
|
|
194
|
-
|
|
195
|
+
const envSchema = options.masterKey
|
|
196
|
+
? withOptionalEnvKeys(options.envSchema.schema, SECRETS_MASTER_KEK_ENV_KEYS)
|
|
197
|
+
: options.envSchema.schema;
|
|
198
|
+
parseEnv(envSchema, rawEnvSource, {
|
|
195
199
|
sources: options.envSchema.sources,
|
|
196
200
|
...(options.pulumiPrefix ? { pulumiPrefix: options.pulumiPrefix } : {}),
|
|
197
201
|
});
|
|
@@ -269,7 +273,12 @@ export async function runProdApp(options) {
|
|
|
269
273
|
includeBundled: !!effectiveAuth,
|
|
270
274
|
...(composeAuthOptions && { authOptions: composeAuthOptions }),
|
|
271
275
|
});
|
|
272
|
-
|
|
276
|
+
const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
|
|
277
|
+
validateBoot(features, {
|
|
278
|
+
env: envSource,
|
|
279
|
+
...(bootCrypto.entityFieldCipher && { entityFieldCipher: bootCrypto.entityFieldCipher }),
|
|
280
|
+
...options.validateBootOptions,
|
|
281
|
+
});
|
|
273
282
|
warnIfNonUtcServerTimeZone();
|
|
274
283
|
validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
|
|
275
284
|
assertPiiBootInvariants(features, {
|
|
@@ -385,7 +394,6 @@ export async function runProdApp(options) {
|
|
|
385
394
|
: (options.extraContext ?? {});
|
|
386
395
|
// Framework-Default-Provider zuerst, App-Werte (resolvedExtraContext)
|
|
387
396
|
// gewinnen immer (z.B. money-horse's eigener configResolver).
|
|
388
|
-
const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
|
|
389
397
|
// App-wide cipher for `encrypted: true` entity fields — executors resolve
|
|
390
398
|
// it lazily, entities without encrypted fields never touch it.
|
|
391
399
|
configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
|
|
@@ -399,6 +407,7 @@ export async function runProdApp(options) {
|
|
|
399
407
|
// jeder Equality-Lookup (Login by email!) liefe ins Leere. Fail-fast statt
|
|
400
408
|
// silent-broken-auth.
|
|
401
409
|
configureBlindIndexKey(options.blindIndexKey);
|
|
410
|
+
const bootLogger = loggerFromExtraContext(resolvedExtraContext);
|
|
402
411
|
const autoExtraContext = buildBootExtraContext({
|
|
403
412
|
db,
|
|
404
413
|
features,
|
|
@@ -408,6 +417,7 @@ export async function runProdApp(options) {
|
|
|
408
417
|
sseBroker,
|
|
409
418
|
crypto: bootCrypto,
|
|
410
419
|
...(options.kms && { kms: options.kms }),
|
|
420
|
+
...(bootLogger && { log: bootLogger }),
|
|
411
421
|
});
|
|
412
422
|
const extraContext = {
|
|
413
423
|
...addConfigAccessorFactory({ ...autoExtraContext, ...resolvedExtraContext }, registry),
|
|
@@ -483,6 +493,7 @@ export async function runProdApp(options) {
|
|
|
483
493
|
...(options.metrics && { metrics: options.metrics }),
|
|
484
494
|
...(options.rateLimit && { rateLimit: options.rateLimit }),
|
|
485
495
|
...(trustedProxyHops !== undefined && { trustedProxyHops }),
|
|
496
|
+
...(options.errorDocs && { errorDocs: options.errorDocs }),
|
|
486
497
|
...(options.extraRoutes && { extraRoutes: options.extraRoutes }),
|
|
487
498
|
...(effectiveAuth && {
|
|
488
499
|
auth: {
|
|
@@ -643,23 +654,21 @@ export async function runProdApp(options) {
|
|
|
643
654
|
db,
|
|
644
655
|
...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
|
|
645
656
|
});
|
|
646
|
-
|
|
647
|
-
|
|
657
|
+
const seedDispatcher = (options.seeds?.length ?? 0) > 0 || options.seedsDir !== undefined
|
|
658
|
+
? createDispatcher(registry, { db, redis, entityCache, registry, ...extraContext })
|
|
659
|
+
: undefined;
|
|
660
|
+
if (seedDispatcher) {
|
|
661
|
+
for (const seed of options.seeds ?? []) {
|
|
662
|
+
await seed({ db, registry, dispatcher: seedDispatcher });
|
|
663
|
+
}
|
|
648
664
|
}
|
|
649
665
|
// ES-Operations / Seed-Migrations (Phase 1). Läuft NACH applyBootSeeds +
|
|
650
666
|
// existing seeds-array — die deklarativen Seeds sind die "always-insert-
|
|
651
667
|
// if-missing"-Schicht; seed-migrations sind die "diff-and-update"-
|
|
652
668
|
// Schicht für Drift den existing Seeds nicht erfassen können (z.B.
|
|
653
669
|
// Membership-Roles-Change nach initialer Seed-Erstellung).
|
|
654
|
-
if (options.seedsDir !== undefined && envSource["KUMIKO_SKIP_ES_OPS"] !== "1") {
|
|
670
|
+
if (seedDispatcher && options.seedsDir !== undefined && envSource["KUMIKO_SKIP_ES_OPS"] !== "1") {
|
|
655
671
|
await createEsOperationsTable(db);
|
|
656
|
-
const seedDispatcher = createDispatcher(registry, {
|
|
657
|
-
db,
|
|
658
|
-
redis,
|
|
659
|
-
entityCache,
|
|
660
|
-
registry,
|
|
661
|
-
...extraContext,
|
|
662
|
-
});
|
|
663
672
|
await runPendingSeedMigrations({
|
|
664
673
|
db,
|
|
665
674
|
seedsDir: options.seedsDir,
|
package/dist/run-worker-app.d.ts
CHANGED
|
@@ -49,7 +49,9 @@ export type RunWorkerAppOptions = {
|
|
|
49
49
|
* RunProdAppOptions["extraContext"], without sseBroker (the worker
|
|
50
50
|
* has none — see entrypoint/index.ts's documented SSE limitation). */
|
|
51
51
|
readonly extraContext?: WorkerContextOption;
|
|
52
|
-
/** MasterKeyProvider for ctx.secrets
|
|
52
|
+
/** MasterKeyProvider replacing the env KEK for ctx.secrets, encrypted
|
|
53
|
+
* config keys and `encrypted: true` entity fields; with it set,
|
|
54
|
+
* `KUMIKO_SECRETS_MASTER_KEY_V1` is not required (see
|
|
53
55
|
* RunProdAppOptions["masterKey"]). */
|
|
54
56
|
readonly masterKey?: MasterKeyProvider;
|
|
55
57
|
/** Subject-key adapter for crypto-shredding — boot checks health()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-server-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.344.0",
|
|
4
4
|
"description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold/codegen tooling.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -80,10 +80,10 @@
|
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
84
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
85
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
86
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
83
|
+
"@cosmicdrift/kumiko-bundled-features": "0.344.0",
|
|
84
|
+
"@cosmicdrift/kumiko-framework": "0.344.0",
|
|
85
|
+
"@cosmicdrift/kumiko-headless": "0.344.0",
|
|
86
|
+
"@cosmicdrift/kumiko-renderer-web": "0.344.0",
|
|
87
87
|
"bun-types": "^1.3.13",
|
|
88
88
|
"ioredis": "^5.10.1",
|
|
89
89
|
"temporal-polyfill": "^0.3.2"
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.343.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Delivery failures are logged to the app logger in prod",
|
|
6
|
+
"detail": "The prod and worker boot pass the app logger (`extraContext.log`) to the delivery service, so redacted delivery failures reach that logger instead of the console. `ctx.notify` is built with `createDeliveryNotifyFactory`. `buildBootExtraContext` accepts an optional `log`."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.343.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "masterKey on runProdApp and runWorkerApp replaces the env KEK for the boot checks",
|
|
12
|
+
"detail": "A `masterKey` provider on runProdApp and runWorkerApp is enough for the boot checks\nThe boot probe for `encrypted: true` entity fields now accepts the cipher the runner builds from `masterKey` (new `ValidateBootOptions.entityFieldCipher`), and the env-schema parse no longer requires `KUMIKO_SECRETS_MASTER_KEY_V1` when `masterKey` is set. The framework exports `withOptionalEnvKeys` for that, and the secrets feature exports `SECRETS_MASTER_KEK_ENV_KEYS`. Without `masterKey`, a missing env KEK still stops the boot; the message now names both ways to provide a key. A malformed env KEK now fails during boot validation, so `KUMIKO_DRY_RUN_ENV=boot` reports it too.",
|
|
13
|
+
"migration": "Apps that pass `masterKey` can drop `KUMIKO_SECRETS_MASTER_KEY_V1` from their env and deployment config. Apps without `masterKey` change nothing."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.343.0",
|
|
17
|
+
"type": "improvement",
|
|
18
|
+
"title": "Prod seeds get registry and dispatcher",
|
|
19
|
+
"detail": "Prod seeds receive the boot registry and a system dispatcher\n`ProdSeedFn` deps grow from `{ db }` to `{ db, registry, dispatcher }`. Seeds should write through `dispatcher.write(...)` so projections and hooks run. The dispatcher is built once when `seeds` or `seedsDir` is set and is shared with the seed migrations."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "0.343.0",
|
|
23
|
+
"type": "improvement",
|
|
24
|
+
"title": "Prod build runs Tailwind in-process instead of spawning the CLI",
|
|
25
|
+
"detail": "The prod build runs Tailwind in-process (`@tailwindcss/node` and `@tailwindcss/oxide`, resolved from the `@tailwindcss/cli` package) instead of spawning the CLI. Options mirror the CLI, and a parity test pins byte-identical output."
|
|
26
|
+
},
|
|
2
27
|
{
|
|
3
28
|
"version": "0.341.0",
|
|
4
29
|
"type": "improvement",
|