@cosmicdrift/kumiko-server-runtime 0.369.0 → 0.371.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/boot/boot-crypto.js +17 -11
  2. package/dist/boot/worker-boot-core.d.ts +1 -1
  3. package/dist/boot-options/billing-option.d.ts +37 -0
  4. package/dist/boot-options/billing-option.js +36 -0
  5. package/dist/boot-options/demo-mode.d.ts +51 -0
  6. package/dist/boot-options/demo-mode.js +64 -0
  7. package/dist/boot-options/file-storage-option.d.ts +30 -0
  8. package/dist/boot-options/file-storage-option.js +52 -0
  9. package/dist/boot-options/kms-mode.d.ts +27 -0
  10. package/dist/boot-options/kms-mode.js +40 -0
  11. package/dist/boot-options/mail-option.d.ts +31 -0
  12. package/dist/boot-options/mail-option.js +43 -0
  13. package/dist/boot-options/option-env-schema.d.ts +7 -0
  14. package/dist/boot-options/option-env-schema.js +113 -0
  15. package/dist/boot-options/process-signals.d.ts +23 -0
  16. package/dist/boot-options/process-signals.js +58 -0
  17. package/dist/boot-options/resolve-boot-options.d.ts +44 -0
  18. package/dist/boot-options/resolve-boot-options.js +88 -0
  19. package/dist/boot-options/seed-shortcuts.d.ts +49 -0
  20. package/dist/boot-options/seed-shortcuts.js +92 -0
  21. package/dist/boot-options/stack-boot-requirements.d.ts +6 -0
  22. package/dist/boot-options/stack-boot-requirements.js +63 -0
  23. package/dist/build-prod-bundle.d.ts +4 -0
  24. package/dist/build-prod-bundle.js +41 -2
  25. package/dist/csp-hashes.d.ts +15 -0
  26. package/dist/csp-hashes.js +46 -0
  27. package/dist/index.d.ts +19 -9
  28. package/dist/index.js +4 -1
  29. package/dist/rate-limit-defaults.d.ts +19 -0
  30. package/dist/rate-limit-defaults.js +13 -0
  31. package/dist/run-bootstrap.d.ts +2 -2
  32. package/dist/run-prod-app-boot-context.d.ts +5 -3
  33. package/dist/run-prod-app-boot-context.js +4 -2
  34. package/dist/run-prod-app-static-files.d.ts +6 -2
  35. package/dist/run-prod-app-static-files.js +22 -1
  36. package/dist/run-prod-app.d.ts +81 -29
  37. package/dist/run-prod-app.js +164 -98
  38. package/dist/run-worker-app.d.ts +2 -2
  39. package/dist/security-headers.d.ts +16 -5
  40. package/dist/security-headers.js +56 -5
  41. package/package.json +18 -9
  42. package/src/changes.json +55 -0
@@ -8,8 +8,8 @@ import { type ConfigResolver, type FeatureDefinition, type Registry } from "@cos
8
8
  import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
9
9
  import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
10
10
  import type { Redis } from "ioredis";
11
- import { type BootCrypto } from "./boot/boot-crypto.js";
12
- import type { AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup } from "./run-prod-app.js";
11
+ import { type BootCrypto } from "./boot/boot-crypto.ts";
12
+ import type { AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup } from "./run-prod-app.ts";
13
13
  export declare function addConfigAccessorFactory<T extends {
14
14
  readonly configResolver?: ConfigResolver;
15
15
  }>(resolved: T, registry: Registry): T;
@@ -43,7 +43,9 @@ type AuthMailNormalizable = {
43
43
  readonly signup?: SignupSetup;
44
44
  readonly invite?: InviteSetup;
45
45
  };
46
- export declare function resolveAuthMail<T extends AuthMailNormalizable>(auth: T, hmacSecret: string, envSource: Record<string, string | undefined>): T & {
46
+ export declare function resolveAuthMail<T extends AuthMailNormalizable>(auth: T, hmacSecret: string, envSource: Record<string, string | undefined>, mailWiring?: {
47
+ readonly mailConfigured?: boolean;
48
+ }): T & {
47
49
  readonly passwordReset?: PasswordResetOptions;
48
50
  readonly emailVerification?: EmailVerificationOptions;
49
51
  };
@@ -185,7 +185,7 @@ function backfillTokenSecrets(auth, tokenSecret) {
185
185
  }),
186
186
  };
187
187
  }
188
- export function resolveAuthMail(auth, hmacSecret, envSource) {
188
+ export function resolveAuthMail(auth, hmacSecret, envSource, mailWiring = {}) {
189
189
  // Runs before the mail/SMTP guards below so an app-supplied block without
190
190
  // hmacSecret is backfilled even without a `mail` block; an explicit secret still wins.
191
191
  const tokenSecret = auth.mail?.hmacSecret ?? hmacSecret;
@@ -196,7 +196,9 @@ export function resolveAuthMail(auth, hmacSecret, envSource) {
196
196
  // blieben sonst 500). Der eigentliche Mail-Versand läuft über delivery
197
197
  // (channel-email), nicht über diesen Transport — er ist nur der Detektor
198
198
  // "ist Mail konfiguriert?".
199
- if (!createSmtpTransportFromEnv(envSource, { fallbackFrom: auth.mail.from ?? "noreply@localhost" })) {
199
+ // An app transport from runProdApp's `mail` option replaces the probe.
200
+ if (mailWiring.mailConfigured !== true &&
201
+ !createSmtpTransportFromEnv(envSource, { fallbackFrom: auth.mail.from ?? "noreply@localhost" })) {
200
202
  return withResolvedSecrets;
201
203
  }
202
204
  const paths = makeAuthPaths(auth.mail.paths);
@@ -1,11 +1,15 @@
1
1
  import { type CachePolicy } from "@cosmicdrift/kumiko-framework/api";
2
- import { type QueryDispatcher } from "./request-bound-system-query.js";
3
- import type { HostDispatchFn, PageHeadResolver } from "./run-prod-app.js";
2
+ import { type QueryDispatcher } from "./request-bound-system-query.ts";
3
+ import type { HostDispatchFn, PageHeadResolver } from "./run-prod-app.ts";
4
+ import type { SecurityHeadersOption } from "./security-headers.ts";
4
5
  export declare function readStaticFile(filePath: string): Promise<{
5
6
  readonly bytes: Uint8Array;
6
7
  readonly mime: string;
7
8
  readonly mtimeMs: number;
8
9
  } | undefined>;
10
+ /** Hashes for the default CSP. Only an app that keeps the default CSP and serves a
11
+ * staticDir needs them; a missing or broken file leaves inline scripts blocked, so boot says so. */
12
+ export declare function resolveCspScriptHashes(staticDir: string | undefined, securityHeaders: SecurityHeadersOption | undefined): Promise<readonly string[]>;
9
13
  export declare function serveDiskFile(req: Request, pathname: string, file: {
10
14
  readonly bytes: Uint8Array;
11
15
  readonly mime: string;
@@ -1,9 +1,10 @@
1
- import { resolve, sep } from "node:path";
1
+ import { join, resolve, sep } from "node:path";
2
2
  import { cachedResponse, computeStrongEtag, computeWeakEtag, createClientIpResolver, } from "@cosmicdrift/kumiko-framework/api";
3
3
  import { resolveAndInjectPageHead } from "@cosmicdrift/kumiko-headless/apex";
4
4
  import { errorCodeOf } from "@cosmicdrift/kumiko-types/type-guards";
5
5
  import { ASSETS_DIR } from "./build-prod-bundle.js";
6
6
  import { BUNDLED_ASSETS_DIST_DIR } from "./bundled-assets.js";
7
+ import { CSP_HASHES_FILE, parseCspHashes } from "./csp-hashes.js";
7
8
  import { buildRequestBoundSystemQuery, } from "./request-bound-system-query.js";
8
9
  import { stripNoRouteMatchHeader, tryHonoFirst } from "./try-hono-first.js";
9
10
  // Static-asset + SPA-fallback serving for runProdApp's HTTP handler. Split
@@ -48,6 +49,26 @@ export async function readStaticFile(filePath) {
48
49
  throw err;
49
50
  }
50
51
  }
52
+ /** Hashes for the default CSP. Only an app that keeps the default CSP and serves a
53
+ * staticDir needs them; a missing or broken file leaves inline scripts blocked, so boot says so. */
54
+ export async function resolveCspScriptHashes(staticDir, securityHeaders) {
55
+ if (staticDir === undefined || securityHeaders === false)
56
+ return [];
57
+ const csp = securityHeaders?.csp;
58
+ if (csp === false || typeof csp === "string")
59
+ return [];
60
+ const file = await readStaticFile(join(staticDir, CSP_HASHES_FILE));
61
+ const result = file ? parseCspHashes(new TextDecoder().decode(file.bytes)) : undefined;
62
+ if (result?.kind === "found")
63
+ return result.scriptSrc;
64
+ const problem = result === undefined
65
+ ? `${CSP_HASHES_FILE} not found in ${staticDir} (written by kumiko build)`
66
+ : `${CSP_HASHES_FILE} is invalid: ${result.kind === "invalid" ? result.problem : ""}`;
67
+ // biome-ignore lint/suspicious/noConsole: boot-time warn, no logger configured this early
68
+ console.warn(`[runProdApp] ${problem}; the default CSP carries no inline-script hashes, so inline scripts in the served HTML are blocked. ` +
69
+ "Rebuild with kumiko build, or set securityHeaders.csp.");
70
+ return [];
71
+ }
51
72
  // Bundled assets are server-side inputs (readBundledAsset), never public URLs.
52
73
  // Compared on the resolved path, not the URL prefix: "//kumiko-bundled-assets/x"
53
74
  // or a case variant on a case-insensitive disk would slip past a prefix check.
@@ -6,21 +6,32 @@ 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 DryRunEnvMode } from "@cosmicdrift/kumiko-framework/env/dry-run";
9
10
  import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
10
11
  import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
11
12
  import { type ObservabilityOptions, type ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
12
13
  import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
13
14
  import type { PageHeadMeta, PageHeadResolver, PageHeadSystemQuery } from "@cosmicdrift/kumiko-headless/apex";
14
15
  import { Redis } from "ioredis";
15
- import { type SystemWireDeps } from "./extra-routes-deps.js";
16
- import { type UnsubscribeOption } from "./resolve-unsubscribe-wiring.js";
17
- import { type SecurityHeadersOption } from "./security-headers.js";
18
- import { type SeedCreateFileRef } from "./seed-dispatcher.js";
19
- export { buildBunServeOptions } from "./bun-serve-options.js";
20
- export { resolveUnsubscribeWiring, type UnsubscribeOption, } from "./resolve-unsubscribe-wiring.js";
21
- export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
22
- export { staticCachePolicy } from "./run-prod-app-static-files.js";
16
+ import type { BillingBootOption } from "./boot-options/billing-option.ts";
17
+ import type { DemoModeOption } from "./boot-options/demo-mode.ts";
18
+ import type { FileStorageBootOption } from "./boot-options/file-storage-option.ts";
19
+ import type { KmsMode } from "./boot-options/kms-mode.ts";
20
+ import type { MailBootOption } from "./boot-options/mail-option.ts";
21
+ import { type SignalsOption } from "./boot-options/process-signals.ts";
22
+ import type { ProdSeedsConfig } from "./boot-options/seed-shortcuts.ts";
23
+ import { type SystemWireDeps } from "./extra-routes-deps.ts";
24
+ import { type RateLimitOption } from "./rate-limit-defaults.ts";
25
+ import { type UnsubscribeOption } from "./resolve-unsubscribe-wiring.ts";
26
+ import { type SecurityHeadersOption } from "./security-headers.ts";
27
+ import { type SeedCreateFileRef } from "./seed-dispatcher.ts";
28
+ export { buildBunServeOptions } from "./bun-serve-options.ts";
29
+ export { resolveUnsubscribeWiring, type UnsubscribeOption, } from "./resolve-unsubscribe-wiring.ts";
30
+ export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.ts";
31
+ export { staticCachePolicy } from "./run-prod-app-static-files.ts";
23
32
  export declare function requireEnv(name: string, src?: Record<string, string | undefined>, context?: string): string;
33
+ type RunMode = DryRunEnvMode;
34
+ export declare function parseRunMode(raw: string | undefined): RunMode | null;
24
35
  /** Wrapper API for the password-reset flow.
25
36
  *
26
37
  * Since the delivery migration PasswordResetOptions carries `appUrl`
@@ -193,6 +204,8 @@ export type RunProdAppDeps = {
193
204
  readonly redis: Redis;
194
205
  readonly registry: import("@cosmicdrift/kumiko-framework/engine").Registry;
195
206
  readonly sseBroker: SseBroker;
207
+ /** The env the boot runs on: process env or `envSource`, with the boot-option defaults (e.g. `fileStorage`) underneath. */
208
+ readonly env: Readonly<Record<string, string | undefined>>;
196
209
  };
197
210
  export type AnonymousAccessOption = import("@cosmicdrift/kumiko-framework/api").AnonymousAccessConfig | ((deps: RunProdAppDeps) => import("@cosmicdrift/kumiko-framework/api").AnonymousAccessConfig);
198
211
  export type ExtraContextOption = Record<string, unknown> | ((deps: RunProdAppDeps) => Record<string, unknown>);
@@ -258,8 +271,13 @@ export type RunProdAppOptions = {
258
271
  readonly maxRequestBodySize?: number;
259
272
  /** Auth-mode: standard features + routes wired, admin seeded. */
260
273
  readonly auth?: RunProdAppAuthOptions;
261
- /** Custom seed functions, run after the admin seed (when auth-mode). */
262
- readonly seeds?: readonly ProdSeedFn[];
274
+ /** Custom seed functions, run after the admin seed (when auth-mode). The
275
+ * object form adds the declarative demo-tenant / demo-admin / sysadmin
276
+ * seeds from the env and takes the functions under `run`; see
277
+ * `ProdSeedsConfig`.
278
+ * @example
279
+ * seeds: { demoTenant: { key: "demo", name: "Demo" }, sysadminFromEnv: true, run: [seedDemoData] } */
280
+ readonly seeds?: readonly ProdSeedFn[] | ProdSeedsConfig;
263
281
  /** Pfad zum seeds-Directory für ES-Operations / Seed-Migrations
264
282
  * (file-basiert wie drizzle-migrate). Wenn gesetzt + KUMIKO_SKIP_ES_OPS
265
283
  * != "1": runProdApp scannt das Verzeichnis nach `<id>.ts` Files,
@@ -354,6 +372,39 @@ export type RunProdAppOptions = {
354
372
  * ist eine Begründung für den Audit-Trail (z.B. "kms rollout pending,
355
373
  * infra#188"). Ohne Flag und ohne `kms` bricht der Boot ab. */
356
374
  readonly allowPlaintextPii?: string;
375
+ /** Short form over `kms` / `blindIndexKey` / `allowPlaintextPii` that wires
376
+ * the subject-key KMS from the env; not combinable with them. Unset keeps
377
+ * today's behavior: no KMS unless `kms` is passed, and the PII boot gate
378
+ * aborts when PII fields exist without one. See `KmsMode`.
379
+ * @example
380
+ * kmsMode: "required" */
381
+ readonly kmsMode?: KmsMode;
382
+ /** App-wide mail transport: mounts `channel-email` and wires the `auth.mail`
383
+ * flows without an `SMTP_HOST` probe. See `MailBootOption`.
384
+ * @example
385
+ * mail: { transport: "smtp-env", from: "Acme <noreply@acme.example>" } */
386
+ readonly mail?: MailBootOption;
387
+ /** File provider for `file-foundation` plus the `FILE_STORAGE_PROVIDER`
388
+ * default. See `FileStorageBootOption`.
389
+ * @example
390
+ * fileStorage: { provider: "auto" } */
391
+ readonly fileStorage?: FileStorageBootOption;
392
+ /** Stripe billing: `subscription-stripe` plus the tier-syncing webhook
393
+ * route. Needs `billing-foundation` and `tier-engine`. See `BillingBootOption`.
394
+ * @example
395
+ * billing: { stripe: { priceToTier: { [env.STRIPE_PRICE_PRO]: "pro" } }, defaultTier: "free" } */
396
+ readonly billing?: BillingBootOption;
397
+ /** Live-demo mode: read-only gate in the fetch chain plus the anonymous
398
+ * `GET /demo-mode` route. See `DemoModeOption`.
399
+ * @example
400
+ * demoMode: { readOnly: env.DEMO_READ_ONLY === "true", notice: { text: { en: "Read-only demo" } } } */
401
+ readonly demoMode?: DemoModeOption;
402
+ /** SIGTERM/SIGINT handling of `listen()`: drains via `stop()` (default
403
+ * timeout 25 s), exits 0, or 1 when the drain fails or times out. `false`
404
+ * leaves the signals to the app.
405
+ * @example
406
+ * signals: { drainTimeoutMs: 10_000 } */
407
+ readonly signals?: SignalsOption;
357
408
  /** Deploy topology. Default `true` (single container): this process runs
358
409
  * HTTP, both job lanes (api + worker) and the event dispatcher (MSP apply)
359
410
  * inline via `createAllInOneEntrypoint`. Worker-lane crons (e.g. the data
@@ -423,23 +474,22 @@ export type RunProdAppOptions = {
423
474
  * the fetch-handler directly (Bun.serve isn't available under vitest +
424
475
  * node), then call handle.listen() manually if needed. */
425
476
  readonly autoListen?: boolean;
426
- /** Feature-toggle resolver — durchgereicht an createApiEntrypoint's
427
- * dispatcherOptions. Sprint-8 Tier-Composition: per-Tenant unterschied-
428
- * liche features aktiv via globalFeatureToggleRuntime. Pattern:
429
- * createLateBoundHolder + post-boot runtime.initialize in einem
430
- * seed-fn (db ist erst nach migrations + features ready). */
477
+ /** Per-tenant feature resolver, passed to the dispatcher.
478
+ * @deprecated Removed in the next release. `createTierEngineFeature({ tierMap })`
479
+ * (or `composeSaasStack`) builds the resolver at boot, and prod-only features
480
+ * come from the `mail`, `fileStorage` and `billing` options. */
431
481
  readonly effectiveFeatures?: EffectiveFeaturesResolver;
432
482
  /** Composed Zod-schema for env-validation (from `composeEnvSchema({
433
- * features, extend })` in @cosmicdrift/kumiko-framework/env). When set:
483
+ * core, features, extend })` in @cosmicdrift/kumiko-framework/env):
434
484
  * - `process.env` is parsed against it BEFORE any boot work; missing
435
485
  * or invalid vars throw a `KumikoBootError` listing ALL problems
436
486
  * at once (not first-fail).
437
487
  * - `KUMIKO_DRY_RUN_ENV=human|json|pulumi|k8s` introspects the schema
438
488
  * and prints the env-var inventory, then exits without booting.
439
- *
440
- * 9.1 is additive: features that still read `process.env` directly
441
- * keep working. Migration to the schema is Sprint-9.2-9.5. */
442
- readonly envSchema?: ComposedEnvSchema;
489
+ * - `KUMIKO_DRY_RUN_ENV=boot` prints a per-variable table. With required
490
+ * values missing it exits 0 without validating the feature wiring; an
491
+ * invalid value exits 1; a complete env runs the boot validators. */
492
+ readonly envSchema: ComposedEnvSchema;
443
493
  /** Prefix for `pulumi config set <prefix><CamelCase(VAR)>` in dry-run
444
494
  * output and boot-error suggestions. Without this, suggestions use
445
495
  * bare `camelCase(VAR)` and ops has to guess the app prefix. */
@@ -466,16 +516,18 @@ export type RunProdAppOptions = {
466
516
  * PrometheusMeter-basierten `observability`-Provider antwortet `/metrics`
467
517
  * mit 503 + Misconfiguration-Hinweis statt echter Metriken. */
468
518
  readonly metrics?: import("@cosmicdrift/kumiko-framework/api").ServerOptions["metrics"];
469
- /** L1 (Login-Lockout) + L2 (globaler IP-Limit) Rate-Limit-Middleware —
470
- * 1:1 durchgereicht an `buildServer`s `ServerOptions.rateLimit`. Ohne
471
- * dieses Feld wird kein L1/L2 verdrahtet (L3 Handler-`rateLimit:`
472
- * funktioniert unabhängig davon bereits). */
473
- readonly rateLimit?: import("@cosmicdrift/kumiko-framework/api").ServerOptions["rateLimit"];
519
+ /** L1 (global per-IP limit) + L2 (auth endpoints) rate-limit middleware, passed
520
+ * to `buildServer`'s `ServerOptions.rateLimit`. Without a value L1 allows 600/60 s
521
+ * and L2 30/60 s; a partial value overrides only the fields it names, `false`
522
+ * (whole option or one layer) switches it off. Behind a reverse proxy the
523
+ * client-IP key needs `trustedProxyHops`, otherwise all clients share one bucket. */
524
+ readonly rateLimit?: RateLimitOption;
474
525
  /** Default security headers on every response (HSTS, X-Frame-Options,
475
- * X-Content-Type-Options, Referrer-Policy; CSP opt-in). Headers already
476
- * set by a response (e.g. hostDispatch's per-host CSP) are never
477
- * overridden. `false` disables the whole block; per-header overrides
478
- * via the object form — see SecurityHeadersOption. */
526
+ * X-Content-Type-Options, Referrer-Policy, Content-Security-Policy with
527
+ * the build's inline-script hashes). Headers already set by a response
528
+ * (e.g. hostDispatch's per-host CSP) are never overridden. `false`
529
+ * disables the whole block; per-header overrides via the object form —
530
+ * see SecurityHeadersOption. */
479
531
  readonly securityHeaders?: SecurityHeadersOption;
480
532
  /** Number of trusted reverse-proxy hops for client-IP resolution — 1:1
481
533
  * durchgereicht an `buildServer`s