@cosmicdrift/kumiko-server-runtime 0.329.0 → 0.330.2

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 (90) hide show
  1. package/README.md +15 -0
  2. package/dist/boot/apply-boot-seeds.d.ts +8 -0
  3. package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
  4. package/dist/boot/boot-crypto.d.ts +9 -0
  5. package/dist/boot/boot-crypto.js +39 -0
  6. package/dist/boot/job-run-logger.d.ts +19 -0
  7. package/dist/boot/job-run-logger.js +44 -0
  8. package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
  9. package/dist/boot/pii-event-backfill-on-boot.js +25 -0
  10. package/dist/build-prod-bundle.d.ts +103 -0
  11. package/dist/build-prod-bundle.js +751 -0
  12. package/dist/bun-serve-options.d.ts +32 -0
  13. package/dist/bun-serve-options.js +49 -0
  14. package/dist/compose-features.d.ts +31 -0
  15. package/dist/compose-features.js +124 -0
  16. package/dist/extra-routes-deps.d.ts +30 -0
  17. package/dist/extra-routes-deps.js +4 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +7 -0
  20. package/dist/pii-boot-gate.d.ts +13 -0
  21. package/dist/pii-boot-gate.js +46 -0
  22. package/dist/renderer-web-fonts.d.ts +9 -0
  23. package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
  24. package/dist/resolve-tailwind-cli.d.ts +14 -0
  25. package/dist/resolve-tailwind-cli.js +36 -0
  26. package/dist/run-prod-app-boot-context.d.ts +43 -0
  27. package/dist/run-prod-app-boot-context.js +198 -0
  28. package/dist/run-prod-app-static-files.d.ts +32 -0
  29. package/dist/run-prod-app-static-files.js +280 -0
  30. package/dist/run-prod-app.d.ts +471 -0
  31. package/dist/run-prod-app.js +741 -0
  32. package/dist/run-worker-app.d.ts +92 -0
  33. package/dist/run-worker-app.js +220 -0
  34. package/dist/security-headers.d.ts +20 -0
  35. package/dist/security-headers.js +49 -0
  36. package/dist/session-boot-gate.d.ts +11 -0
  37. package/dist/session-boot-gate.js +22 -0
  38. package/dist/session-wiring.d.ts +11 -0
  39. package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
  40. package/dist/try-hono-first.d.ts +24 -0
  41. package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
  42. package/package.json +102 -36
  43. package/src/changes.json +6 -0
  44. package/src/__tests__/boot-extra-context.test.ts +0 -163
  45. package/src/__tests__/boot-probe-fixture.ts +0 -64
  46. package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
  47. package/src/__tests__/build-prod-bundle.test.ts +0 -724
  48. package/src/__tests__/cache-headers.test.ts +0 -83
  49. package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
  50. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -609
  51. package/src/__tests__/compose-features.test.ts +0 -206
  52. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  53. package/src/__tests__/pii-boot-gate.test.ts +0 -112
  54. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  55. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  56. package/src/__tests__/renderer-web-theme-plugin.integration.test.ts +0 -77
  57. package/src/__tests__/require-env.test.ts +0 -34
  58. package/src/__tests__/resolve-auth-mail.test.ts +0 -144
  59. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  60. package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
  61. package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
  62. package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
  63. package/src/__tests__/run-prod-app-spec.test.ts +0 -167
  64. package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
  65. package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
  66. package/src/__tests__/run-worker-app-boot.test.ts +0 -183
  67. package/src/__tests__/run-worker-app.integration.test.ts +0 -218
  68. package/src/__tests__/security-headers.test.ts +0 -88
  69. package/src/__tests__/session-boot-gate.test.ts +0 -70
  70. package/src/__tests__/session-wiring.test.ts +0 -16
  71. package/src/__tests__/try-hono-first.test.ts +0 -108
  72. package/src/__tests__/websocket-route.integration.test.ts +0 -695
  73. package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
  74. package/src/boot/__tests__/job-run-logger.test.ts +0 -26
  75. package/src/boot/boot-crypto.ts +0 -67
  76. package/src/boot/job-run-logger.ts +0 -63
  77. package/src/boot/pii-event-backfill-on-boot.ts +0 -43
  78. package/src/build-prod-bundle.ts +0 -996
  79. package/src/bun-serve-options.ts +0 -110
  80. package/src/compose-features.ts +0 -197
  81. package/src/extra-routes-deps.ts +0 -38
  82. package/src/index.ts +0 -27
  83. package/src/pii-boot-gate.ts +0 -80
  84. package/src/resolve-tailwind-cli.ts +0 -45
  85. package/src/run-prod-app-boot-context.ts +0 -319
  86. package/src/run-prod-app-static-files.ts +0 -359
  87. package/src/run-prod-app.ts +0 -1420
  88. package/src/run-worker-app.ts +0 -366
  89. package/src/security-headers.ts +0 -67
  90. package/src/session-boot-gate.ts +0 -34
@@ -0,0 +1,471 @@
1
+ /// <reference types="bun-types" preserve="true" />
2
+ import { type AccountLockoutOptions, type AccountUnlockOptions, type AuthMailLocale, type AuthPaths, type EmailVerificationOptions, type InviteOptions, type PasswordResetOptions, type SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
3
+ import { type SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
4
+ import { type ExtraRouteDefinition, type KumikoServeEnv, type PostAuthLandingResolver, type SseBroker } from "@cosmicdrift/kumiko-framework/api";
5
+ import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
6
+ import { type EffectiveFeaturesResolver, type FeatureDefinition, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { type AllInOneEntrypoint, type ApiEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
8
+ import { type ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
9
+ import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
10
+ import type { ObservabilityOptions, ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
11
+ import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
12
+ import type { PageHeadMeta, PageHeadResolver, PageHeadSystemQuery } from "@cosmicdrift/kumiko-headless/apex";
13
+ import { Redis } from "ioredis";
14
+ import { type SystemWireDeps } from "./extra-routes-deps.js";
15
+ import { type SecurityHeadersOption } from "./security-headers.js";
16
+ export { buildBunServeOptions } from "./bun-serve-options.js";
17
+ export { addConfigAccessorFactory, buildBootExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
18
+ export { staticCachePolicy } from "./run-prod-app-static-files.js";
19
+ export declare function requireEnv(name: string, src?: Record<string, string | undefined>, context?: string): string;
20
+ /** Wrapper API for the password-reset flow.
21
+ *
22
+ * Since the delivery migration PasswordResetOptions carries `appUrl`
23
+ * (+ appName/locale) itself and the handler mails via ctx.notify — no
24
+ * sendResetEmail callback anymore. Apps pass `auth.mail` (convenience,
25
+ * resolveAuthMail builds the appUrl) OR an explicit block.
26
+ *
27
+ * hmacSecret is optional here (unlike PasswordResetOptions): resolveAuthMail
28
+ * backfills it, so overriding just appUrl needs no secret of its own. */
29
+ export type PasswordResetSetup = Omit<PasswordResetOptions, "hmacSecret"> & {
30
+ readonly hmacSecret?: string;
31
+ };
32
+ /** Wrapper API for the email-verification flow, symmetric to
33
+ * PasswordResetSetup (appUrl via delivery, optional hmacSecret). */
34
+ export type EmailVerificationSetup = Omit<EmailVerificationOptions, "hmacSecret"> & {
35
+ readonly hmacSecret?: string;
36
+ };
37
+ /** Wrapper-API für Magic-Link Self-Signup. = SignupOptions (appUrl, die
38
+ * Mail geht via delivery/ctx.notify wie reset/verify). Anders als reset/
39
+ * verify gibt's KEIN hmacSecret — Signup-Tokens sind opaque random in
40
+ * Redis, nicht HMAC-signed. */
41
+ export type SignupSetup = SignupOptions;
42
+ /** Wrapper-API für Tenant-Invite Magic-Link. = InviteOptions (appUrl, die
43
+ * Invite-Mail geht via delivery wie reset/verify/signup). Drei accept-
44
+ * Branches im framework; handler-names sind hardcoded in run-prod-app aus
45
+ * AuthHandlers (analog signup). */
46
+ export type InviteSetup = InviteOptions;
47
+ /** Wrapper API for the account-unlock flow (#1266). = AccountUnlockOptions
48
+ * (appUrl via delivery, symmetric to PasswordResetSetup). Self-service
49
+ * escape hatch for accountLockout's monotonic failure counter — only
50
+ * meaningful when `auth.accountLockout` is also set, but wired
51
+ * independently like the other flows. */
52
+ export type AccountUnlockSetup = AccountUnlockOptions;
53
+ /** Wrapper API for the account-lockout brute-force guard on the login
54
+ * handler. = AccountLockoutOptions (maxFailedAttempts, lockoutDurationMinutes
55
+ * — no mail/appUrl fields, it doesn't mount any routes). Symmetric to
56
+ * RunDevAppAuthOptions.accountLockout (kumiko-framework#1627) — before this,
57
+ * `accountUnlock` existed on both wrappers with no way to ever set the
58
+ * lockout it's meant to escape. */
59
+ export type AccountLockoutSetup = AccountLockoutOptions;
60
+ /** Auth-Mail-Convenience-Optionen — shared zwischen runProdApp + runDevApp.
61
+ * Verdrahtet alle 4 Mail-Flows aus einem env-SMTP-Transport + Standard-
62
+ * Templates (siehe `auth.mail` + resolveAuthMail). */
63
+ export type AuthMailOptions = {
64
+ /** App-Basis-URL inkl. Schema (z.B. "https://app.example.com"). */
65
+ readonly baseUrl: string;
66
+ /** App-Name für Mail-Subject + Body. Default "Account". */
67
+ readonly appName?: string;
68
+ /** Locale für die Mail-Templates. Default "de". */
69
+ readonly locale?: AuthMailLocale;
70
+ /** "strict" blockt Login bis emailVerified; "off" mountet ohne Gate. */
71
+ readonly emailVerificationMode?: "strict" | "off";
72
+ /** Fallback-From-Adresse wenn `SMTP_FROM`-env fehlt (env gewinnt). */
73
+ readonly from?: string;
74
+ /** Einzelne Auth-Pfade überschreiben (Default DEFAULT_AUTH_PATHS). */
75
+ readonly paths?: Partial<AuthPaths>;
76
+ /** Reset/verify token secret, decoupled from JWT_SECRET rotation.
77
+ * Falls back to resolveAuthMail's call-site secret when unset. */
78
+ readonly hmacSecret?: string;
79
+ };
80
+ export type RunProdAppAuthOptions = {
81
+ /** Initial admin user. Seeded once (idempotent — re-boots check first
82
+ * whether the email is already in the users table). */
83
+ readonly admin: SeedAdminOptions;
84
+ /** Optional override of the login error → HTTP status map. */
85
+ readonly loginErrorStatusMap?: Readonly<Record<string, number>>;
86
+ /** Auth-Mail-Convenience: verdrahtet alle 4 Mail-Flows (passwordReset,
87
+ * emailVerification, signup, invite) aus `auth.mail.baseUrl` + Standard-
88
+ * Pfaden. Alle vier mailen via delivery (ctx.notify) — ersetzt das per-App
89
+ * hand-gerollte `send*Email`-Callback-Wiring.
90
+ *
91
+ * Null-Transport-Guard: ohne `SMTP_HOST`-env wird KEIN Mail-Flow
92
+ * verdrahtet (Routes blieben sonst 500). Eine App die einen einzelnen
93
+ * Flow custom braucht, setzt `passwordReset`/`signup`/… explizit —
94
+ * der explizite Block gewinnt über den mail-Default.
95
+ *
96
+ * Bewusste Entscheidung: `mail` mountet ALLE 4 Flows inkl. signup +
97
+ * invite (Self-Registration on). Wer nur reset+verify will, lässt `mail`
98
+ * weg und setzt die gewünschten Flows explizit — kein impliziter
99
+ * Self-Signup. */
100
+ readonly mail?: AuthMailOptions;
101
+ /** Password-reset flow. When set, /api/auth/request-password-reset +
102
+ * /api/auth/reset-password are mounted as public routes UND der
103
+ * request/confirm-Handler im auth-email-password-Feature wird
104
+ * registriert (sonst dispatchen die Routes ins Leere → 500).
105
+ * Überschreibt den `mail`-Default für genau diesen Flow. */
106
+ readonly passwordReset?: PasswordResetSetup;
107
+ /** Email-verification flow. Symmetric to passwordReset. */
108
+ readonly emailVerification?: EmailVerificationSetup;
109
+ /** Self-Signup flow (Magic-Link). When set, /api/auth/signup-request +
110
+ * /api/auth/signup-confirm are mounted; signup-confirm mintet JWT +
111
+ * Cookies wie ein erfolgreicher login (Auto-Login direkt nach
112
+ * Activation). */
113
+ readonly signup?: SignupSetup;
114
+ /** Tenant-Invite flow (Magic-Link). When set, /api/auth/invite-accept,
115
+ * /api/auth/invite-accept-with-login, /api/auth/invite-signup-complete
116
+ * are mounted. */
117
+ readonly invite?: InviteSetup;
118
+ /** Account-unlock flow (#1266). When set, /api/auth/request-account-unlock
119
+ * + /api/auth/confirm-account-unlock are mounted. Self-service escape
120
+ * hatch for accountLockout's monotonic failure-counter — confirming
121
+ * clears the Redis lockout state, no entity write. */
122
+ readonly accountUnlock?: AccountUnlockSetup;
123
+ /** Brute-force guard on the login handler (maxFailedAttempts,
124
+ * lockoutDurationMinutes). Mounts no routes — unlike the other flows,
125
+ * it's a login-handler config knob, not a magic-link flow. Pair with
126
+ * `accountUnlock` for a self-service escape hatch. */
127
+ readonly accountLockout?: AccountLockoutSetup;
128
+ /** Domain attribute for both auth cookies (see
129
+ * AuthRoutesConfig.cookieDomain). Set to the registrable parent
130
+ * domain when login and app live on different subdomains. */
131
+ readonly cookieDomain?: string;
132
+ /** Server-side Origin allowlist for the CSRF guard (see
133
+ * AuthRoutesConfig.allowedOrigins). REQUIRED once `cookieDomain` is set —
134
+ * buildServer fails closed otherwise. Apex + admin host, never tenant
135
+ * subdomains. */
136
+ readonly allowedOrigins?: readonly string[];
137
+ /** Opt out of the Origin guard (see AuthRoutesConfig.unsafeSkipOriginCheck)
138
+ * — accept the wide-cookie CSRF risk explicitly instead of setting
139
+ * `allowedOrigins`. */
140
+ readonly unsafeSkipOriginCheck?: boolean;
141
+ /** Number of trusted reverse-proxy hops between the client and this
142
+ * process for client-IP derivation (see AuthRoutesConfig.trustedProxyHops,
143
+ * kumiko-framework#1539) — closes the X-Forwarded-For spoofing hole on
144
+ * the auth rate-limiters. Falls back to the `KUMIKO_TRUSTED_PROXY_HOPS`
145
+ * env var when unset; both unset means 0 hops, the safe socket-only
146
+ * default (only the socket address counts, XFF ignored). Deprecated in
147
+ * favor of the top-level `trustedProxyHops` option, which wins over
148
+ * this and the env var. */
149
+ readonly trustedProxyHops?: number;
150
+ /** Server-computed post-auth redirect (see AuthRoutesConfig.postAuthLanding).
151
+ * Result lands in the login/signup/invite response as `landingPath`; an
152
+ * invalid path or a throwing resolver just omits the field. */
153
+ readonly postAuthLanding?: PostAuthLandingResolver;
154
+ };
155
+ /** Hook for app-specific seeding — runs after the admin (when auth is
156
+ * active). Each seed is responsible for its own idempotence (seeds are
157
+ * expected to check "is my row already there?" before inserting). */
158
+ export type ProdSeedFn = (deps: {
159
+ db: import("@cosmicdrift/kumiko-framework/db").DbConnection;
160
+ }) => Promise<void>;
161
+ /** Boot-Time-Deps die `extraContext` + `anonymousAccess` Factories als
162
+ * Argument bekommen. Closure dann in der returned Config (z.B. ein
163
+ * TenantResolver der gegen `db` queriet, oder ein extraContext-Provider
164
+ * der direkt SSE-Events publishen will). Single-source: identisch zu
165
+ * setupTestStack's extraContext-Factory-Shape damit Test/Prod gleich
166
+ * aussehen. */
167
+ export type RunProdAppDeps = {
168
+ readonly db: import("@cosmicdrift/kumiko-framework/db").DbConnection;
169
+ readonly redis: Redis;
170
+ readonly registry: import("@cosmicdrift/kumiko-framework/engine").Registry;
171
+ readonly sseBroker: SseBroker;
172
+ };
173
+ export type AnonymousAccessOption = import("@cosmicdrift/kumiko-framework/api").AnonymousAccessConfig | ((deps: RunProdAppDeps) => import("@cosmicdrift/kumiko-framework/api").AnonymousAccessConfig);
174
+ export type ExtraContextOption = Record<string, unknown> | ((deps: RunProdAppDeps) => Record<string, unknown>);
175
+ /** Per-host routing decision for the staticDir fallback, returned from
176
+ * hostDispatch. Three modes:
177
+ * - "html": serve a specific HTML file (relative to staticDir), optionally
178
+ * with CSP. HTML never carries a schema — createKumikoApp fetches it
179
+ * itself from the authenticated GET /api/schema, after the
180
+ * clientFeature gates (auth) let it through.
181
+ * - "redirect": 301/302 to the given location.
182
+ * - "not-found": reject outright (e.g. unknown subdomain).
183
+ *
184
+ * Only consulted when the path would otherwise fall through to the HTML
185
+ * fallback — i.e. for "/", "/index.html", or SPA routes that neither Hono
186
+ * matches nor a concrete disk file. Asset paths (/assets/*) and API paths
187
+ * run independent of the host. */
188
+ export type HostDispatchResult = {
189
+ readonly kind: "html";
190
+ readonly file: string;
191
+ /** @deprecated Ignored — HTML never carries the schema; createKumikoApp
192
+ * fetches it from the authenticated GET /api/schema. */
193
+ readonly injectSchema?: boolean;
194
+ readonly csp?: string;
195
+ } | {
196
+ readonly kind: "redirect";
197
+ readonly to: string;
198
+ readonly status?: 301 | 302;
199
+ } | {
200
+ readonly kind: "not-found";
201
+ };
202
+ export type HostDispatchFn = (req: {
203
+ readonly host: string;
204
+ readonly path: string;
205
+ /** Query string incl. leading `?`, `""` if none. Redirects that
206
+ * route the path to a different host (e.g. auth routes with
207
+ * `?token=` from old mail links) MUST append it to `to`. */
208
+ readonly search: string;
209
+ },
210
+ /** `systemQuery` runs as the anonymous role (like PageHeadSystemQuery) —
211
+ * hostDispatch is reachable by any public visitor, same access gate as a
212
+ * real anonymous request. Lets a Multi-Tenant hostDispatch look up the
213
+ * tenant for a subdomain instead of needing its own db handle. */
214
+ deps: {
215
+ readonly systemQuery: PageHeadSystemQuery;
216
+ }) => HostDispatchResult | Promise<HostDispatchResult>;
217
+ export type { PageHeadMeta, PageHeadResolver, PageHeadSystemQuery };
218
+ export type RunProdAppOptions = {
219
+ /** App-specific features. config/user/tenant/auth-email-password are
220
+ * auto-mixed when `auth:` is set — don't add them yourself. */
221
+ readonly features: readonly FeatureDefinition[];
222
+ /** Opt-in boot-validator warnings — see ValidateBootOptions. */
223
+ readonly validateBootOptions?: ValidateBootOptions;
224
+ /** Listen-Port. Default 3000 (or $PORT). */
225
+ readonly port?: number;
226
+ /** Ceiling Bun.serve buffers a request body to before any app-level
227
+ * body-limit middleware runs. Default is derived from `createFilesFeature`'s
228
+ * configured `maxUploadSize`/field `maxSize` (see
229
+ * resolveDerivedMaxRequestBodySize in bun-serve-options.ts) — override
230
+ * only for a custom ceiling unrelated to that config. */
231
+ readonly maxRequestBodySize?: number;
232
+ /** Auth-mode: standard features + routes wired, admin seeded. */
233
+ readonly auth?: RunProdAppAuthOptions;
234
+ /** Custom seed functions, run after the admin seed (when auth-mode). */
235
+ readonly seeds?: readonly ProdSeedFn[];
236
+ /** Pfad zum seeds-Directory für ES-Operations / Seed-Migrations
237
+ * (file-basiert wie drizzle-migrate). Wenn gesetzt + KUMIKO_SKIP_ES_OPS
238
+ * != "1": runProdApp scannt das Verzeichnis nach `<id>.ts` Files,
239
+ * diff vs kumiko_es_operations-Table, läuft pending in Tx.
240
+ * Plan: kumiko-platform/docs/plans/features/es-ops.md */
241
+ readonly seedsDir?: string;
242
+ /** Anonymous-access for public endpoints (same shape as runDevApp).
243
+ * Akzeptiert entweder einen statischen Config-Object ODER eine
244
+ * Factory `({db, redis, registry}) => Config` — die Factory wird
245
+ * einmal zur Boot-Zeit aufgerufen, NACHDEM db/redis/registry konstruiert
246
+ * sind. Der Caller closure'd typischerweise db/redis/registry in den
247
+ * TenantResolver damit z.B. ein Subdomain → Tenant-Lookup gegen die
248
+ * DB möglich ist (siehe samples/showcases/publicstatus für das
249
+ * Multi-Tenant-Pattern). */
250
+ readonly anonymousAccess?: AnonymousAccessOption;
251
+ /** Static-file root for HTML / assets. Served on the catch-all route
252
+ * for any path that doesn't match an /api/ handler. Use this for the
253
+ * public status page HTML, embed widget JS, etc. */
254
+ readonly staticDir?: string;
255
+ /** Host-aware routing hook for multi-tenant + multi-app deployments
256
+ * (e.g. publicstatus's `<sub>.publicstatus.eu` (public page) +
257
+ * `admin.publicstatus.eu` (admin UI) + `publicstatus.eu` (apex/
258
+ * marketing) in the SAME container).
259
+ *
260
+ * Called when the staticDir fallback would generate an HTML response
261
+ * (root or SPA route). Default behavior without hostDispatch: index.html
262
+ * (single-app), no schema.
263
+ *
264
+ * Schema safety: HTML (per-host as well as single-app) never carries an
265
+ * AppSchema — createKumikoApp fetches it via GET /api/schema, which sits
266
+ * behind the auth middleware and only serves non-anonymous, signed-in
267
+ * users. Per-host CSP headers can additionally restrict asset paths. */
268
+ readonly hostDispatch?: HostDispatchFn;
269
+ /** Per-request head-metadata resolver (Open-Graph/title/description) for
270
+ * the static-fallback HTML shell — see `PageHeadResolver`. Consulted on
271
+ * the same two HTML-serving paths as `hostDispatch` (host-dispatched
272
+ * HTML + the default single-app index.html), right before the response
273
+ * is sent. On error, `null`, or a resolve time over 300ms, the shell
274
+ * ships unchanged with status 200 — this must never turn into a 500 or
275
+ * a blank page. `systemQuery` inside the resolver runs as the
276
+ * anonymous role (not system) — the resolver is reachable by any
277
+ * public visitor, same access gate as a real anonymous request. */
278
+ readonly resolvePageHead?: PageHeadResolver;
279
+ /** Pfad zu kumiko/migrations für den Boot-Gate. Default "./kumiko/
280
+ * migrations" relativ zum process-cwd (wo die App gestartet wird —
281
+ * bei Container-Deploys typischerweise der App-Workspace-Root, weil
282
+ * WORKDIR im Dockerfile dorthin zeigt). Boot wirft SchemaDriftError
283
+ * wenn Migrations pending sind oder erwartete Tabellen fehlen.
284
+ * Setze auf `false` um den Gate komplett zu deaktivieren — nur für
285
+ * Setups die ihren eigenen Schema-Check fahren (z.B. bring-your-own-
286
+ * ORM). Standard-Apps lassen das default. */
287
+ readonly migrations?: {
288
+ readonly dir: string;
289
+ } | false;
290
+ /** Extra AppContext keys. Framework-Defaults werden zur Boot-Zeit
291
+ * unter den Factory-Werten ergänzt, App-Werte gewinnen immer:
292
+ * - `templateResolver` (createTemplateResolverApi(db)) — immer
293
+ * - `secrets` (createSecretsContext) — nur wenn das `secrets`-Feature
294
+ * gemountet ist (sonst kein KEK-env-Zwang für Apps ohne secrets)
295
+ * - `configResolver` — im Auth-Mode (env-config-overrides)
296
+ * Akzeptiert einen statischen Object ODER eine Factory
297
+ * `({db, redis, registry}) => Record<string, unknown>` — gleiches
298
+ * Pattern wie `anonymousAccess`. Eine App die einen dieser Keys selbst
299
+ * setzt (z.B. eigener configResolver) überschreibt den Default. */
300
+ /** Boot default locale (BCP-47) for ctx.locale when the request carries
301
+ * no locale signal. Merged into AppContext before extraContext (app wins). */
302
+ readonly defaultLocale?: string;
303
+ readonly extraContext?: ExtraContextOption;
304
+ /** MasterKeyProvider für die auto-verdrahtete `ctx.secrets`. Default:
305
+ * `createEnvMasterKeyProvider` (KEK aus `KUMIKO_SECRETS_MASTER_KEY_V<n>`).
306
+ * Override für KMS-Backends (AWS/GCP/Azure) statt env-KEK. Nur relevant
307
+ * wenn das `secrets`-Feature gemountet ist. */
308
+ readonly masterKey?: MasterKeyProvider;
309
+ /** Subject-Key-Adapter für Crypto-Shredding (DSGVO Art. 17). Wenn gesetzt,
310
+ * steht er Feature-Code als `ctx.kms` zur Verfügung und der Boot prüft
311
+ * seine health() — die App startet nicht gegen ein unerreichbares KMS
312
+ * (Silent-Start würde jeden PII-Read mit 503 beantworten). Kein Default:
313
+ * ohne Adapter bleibt Crypto-Shredding aus. */
314
+ readonly kms?: KmsAdapter;
315
+ /** 32-Byte-Key (base64) für Blind-Index-HMACs auf `lookupable`-Feldern
316
+ * (env: KUMIKO_BLIND_INDEX_KEY, `openssl rand -base64 32`). Bewusst
317
+ * getrennt vom PLATFORM_KEK und nicht Teil des KmsAdapter-Contracts.
318
+ * Pflicht sobald `kms` gesetzt ist UND ein Feature lookupable-Felder
319
+ * deklariert — sonst bricht jeder Equality-Lookup auf Ciphertext
320
+ * (Login by email!) und der Boot failt hart. */
321
+ readonly blindIndexKey?: string;
322
+ /** Explizites Opt-out aus dem harten PII-Boot-Gate: Features deklarieren
323
+ * pii/userOwned/tenantOwned-Felder, aber es ist (noch) kein `kms`
324
+ * provisioniert — die Felder liegen dann als KLARTEXT in Rows und
325
+ * Events und DSGVO-Erasure via Crypto-Shredding ist unmöglich. Der Wert
326
+ * ist eine Begründung für den Audit-Trail (z.B. "kms rollout pending,
327
+ * infra#188"). Ohne Flag und ohne `kms` bricht der Boot ab. */
328
+ readonly allowPlaintextPii?: string;
329
+ /** Deploy-Topologie. Default `true` (Single-Container): dieser Prozess
330
+ * fährt HTTP + BEIDE Job-Lanes (api + worker) + den Event-Dispatcher
331
+ * (MSP-Anwendung) inline — via `createAllInOneEntrypoint`. Damit laufen
332
+ * worker-Lane-Crons (z.B. der Daten-Export `run-export-jobs`, default
333
+ * `runIn:"worker"`) und r.multiStreamProjection ohne separaten Worker.
334
+ *
335
+ * `false` NUR mit einem dezidierten Worker-Deployment setzen: dann fährt
336
+ * dieser Prozess API-only (`createApiEntrypoint`), und worker-Lane-Jobs
337
+ * + MSPs werden NICHT mehr lokal angewandt — der Worker muss sie
338
+ * übernehmen, sonst bleiben Export-Jobs pending und die Read-Side leer
339
+ * (2026-06-11-Incident-Klasse). */
340
+ readonly runSingleInstance?: boolean;
341
+ /** Job-Block. Wenn das Feature `r.job(...)` registriert, wird er
342
+ * automatisch verdrahtet (siehe runSingleInstance). */
343
+ readonly jobs?: {
344
+ /** BullMQ-Queue-Prefix (default "kumiko"). */
345
+ readonly queueNamePrefix?: string;
346
+ /** Source of active tenant ids for `perTenant: true` jobs. Omit when the
347
+ * tenant feature is mounted — the framework then resolves tenants on
348
+ * its own via that feature's active-tenant-ids query. Only needed when
349
+ * the app manages tenants outside that feature. */
350
+ readonly getActiveTenantIds?: JobRunnerOptions["getActiveTenantIds"];
351
+ };
352
+ /** Event-Dispatcher (MSP-Anwendung) im API-Process. Default AN —
353
+ * runProdApp ist das Single-Container-Deployment, es gibt keinen
354
+ * Worker-Process der multiStreamProjections anwenden könnte. Bis
355
+ * 2026-06-11 fehlte der Dispatcher hier komplett: jede MSP-basierte
356
+ * Read-Projektion (z.B. custom-fields jsonb) blieb in Prod leer,
357
+ * kumiko_event_consumers blieb ohne Rows. `disabled: true` nur für
358
+ * Setups mit dezidiertem Worker-Process. */
359
+ readonly eventDispatcher?: {
360
+ readonly disabled?: boolean;
361
+ /** Poll-Intervall des Dispatcher-Loops (default siehe
362
+ * createEventDispatcher). LISTEN/NOTIFY-Wiring kommt mit einem
363
+ * späteren pgClient-Pass-through. */
364
+ readonly pollIntervalMs?: number;
365
+ };
366
+ /** Declarative HTTP-routes outside the /api/write|query|batch pipeline —
367
+ * each entry declares its access tier (`entry`: "anonymous" | "user" |
368
+ * "signature"), buildServer wires the matching guard + deps. Mounted
369
+ * inside buildServer, right after the r.httpRoute loop, before seeds
370
+ * (kumiko-framework#3050). */
371
+ readonly extraRoutes?: readonly ExtraRouteDefinition[];
372
+ /** Hook for app-wired co-running components that need the system-write
373
+ * dispatcher (e.g. wiring a webhook plugin's system-scoped secrets, or
374
+ * starting a co-running supervisor) — runs after buildServer, before
375
+ * seeds, with NO `app` (routes are declared via `extraRoutes`, not
376
+ * wired here). */
377
+ readonly wire?: (deps: SystemWireDeps) => void | Promise<void>;
378
+ /** When true (default), Bun.serve is started before runProdApp resolves —
379
+ * the common case: `await runProdApp({...})` boots the server and the
380
+ * process stays up listening on PORT. Set to false in tests that drive
381
+ * the fetch-handler directly (Bun.serve isn't available under vitest +
382
+ * node), then call handle.listen() manually if needed. */
383
+ readonly autoListen?: boolean;
384
+ /** Feature-toggle resolver — durchgereicht an createApiEntrypoint's
385
+ * dispatcherOptions. Sprint-8 Tier-Composition: per-Tenant unterschied-
386
+ * liche features aktiv via globalFeatureToggleRuntime. Pattern:
387
+ * createLateBoundHolder + post-boot runtime.initialize in einem
388
+ * seed-fn (db ist erst nach migrations + features ready). */
389
+ readonly effectiveFeatures?: EffectiveFeaturesResolver;
390
+ /** Composed Zod-schema for env-validation (from `composeEnvSchema({
391
+ * features, extend })` in @cosmicdrift/kumiko-framework/env). When set:
392
+ * - `process.env` is parsed against it BEFORE any boot work; missing
393
+ * or invalid vars throw a `KumikoBootError` listing ALL problems
394
+ * at once (not first-fail).
395
+ * - `KUMIKO_DRY_RUN_ENV=human|json|pulumi|k8s` introspects the schema
396
+ * and prints the env-var inventory, then exits without booting.
397
+ *
398
+ * 9.1 is additive: features that still read `process.env` directly
399
+ * keep working. Migration to the schema is Sprint-9.2-9.5. */
400
+ readonly envSchema?: ComposedEnvSchema;
401
+ /** Prefix for `pulumi config set <prefix><CamelCase(VAR)>` in dry-run
402
+ * output and boot-error suggestions. Without this, suggestions use
403
+ * bare `camelCase(VAR)` and ops has to guess the app prefix. */
404
+ readonly pulumiPrefix?: string;
405
+ /** Handler for KumikoBootError. Default: print formatted error to
406
+ * stderr and `process.exit(1)` so the container restarts with a
407
+ * visible log line. Override in tests that drive runProdApp directly
408
+ * (avoid the exit). Return type is `void` rather than `never` to keep
409
+ * test-overrides honest — if a reporter returns, runProdApp falls
410
+ * through to a regular `throw err` as the safety net. */
411
+ readonly bootErrorReporter?: (err: KumikoBootError) => void;
412
+ /** Override `process.env` for env-validation. Default: `process.env`.
413
+ * Tests use this to feed crafted env-maps without polluting the
414
+ * global. */
415
+ readonly envSource?: Record<string, string | undefined>;
416
+ /** Observability-Provider (Tracer + Meter). Default: Noop (kein Overhead,
417
+ * kein `/metrics`-Endpoint). Setze `createPrometheusMeter()`-basierten
418
+ * Provider + `metrics` um `/metrics` real zu exposen (publicstatus#91). */
419
+ readonly observability?: ObservabilityProvider;
420
+ /** Konfiguriert die Auto-Instrumentation des resolvierten Observability-
421
+ * Providers (siehe `BaseEntrypointOptions.observabilityOptions`). */
422
+ readonly observabilityOptions?: ObservabilityOptions;
423
+ /** Aktiviert den `/metrics`-Endpoint (Open-Metrics-Format). Ohne einen
424
+ * PrometheusMeter-basierten `observability`-Provider antwortet `/metrics`
425
+ * mit 503 + Misconfiguration-Hinweis statt echter Metriken. */
426
+ readonly metrics?: import("@cosmicdrift/kumiko-framework/api").ServerOptions["metrics"];
427
+ /** L1 (Login-Lockout) + L2 (globaler IP-Limit) Rate-Limit-Middleware —
428
+ * 1:1 durchgereicht an `buildServer`s `ServerOptions.rateLimit`. Ohne
429
+ * dieses Feld wird kein L1/L2 verdrahtet (L3 Handler-`rateLimit:`
430
+ * funktioniert unabhängig davon bereits). */
431
+ readonly rateLimit?: import("@cosmicdrift/kumiko-framework/api").ServerOptions["rateLimit"];
432
+ /** Default security headers on every response (HSTS, X-Frame-Options,
433
+ * X-Content-Type-Options, Referrer-Policy; CSP opt-in). Headers already
434
+ * set by a response (e.g. hostDispatch's per-host CSP) are never
435
+ * overridden. `false` disables the whole block; per-header overrides
436
+ * via the object form — see SecurityHeadersOption. */
437
+ readonly securityHeaders?: SecurityHeadersOption;
438
+ /** Number of trusted reverse-proxy hops for client-IP resolution — 1:1
439
+ * durchgereicht an `buildServer`s
440
+ * top-level `ServerOptions.trustedProxyHops`. Wins over the deprecated
441
+ * `auth.trustedProxyHops` and `KUMIKO_TRUSTED_PROXY_HOPS`. Default 0 =
442
+ * trust no proxy header, only the socket address (or "unknown") counts. */
443
+ readonly trustedProxyHops?: number;
444
+ };
445
+ export type ProdAppHandle = {
446
+ /** The composed entrypoint — AllInOne (runSingleInstance, default) or
447
+ * Api-only (runSingleInstance:false). In KUMIKO_DRY_RUN_ENV mode WITH
448
+ * `envSource` injected (test path), no boot ran and this slot is an
449
+ * undefined-cast — do not access. Production dry-run hits
450
+ * `process.exit(0)` before returning a handle. */
451
+ readonly entrypoint: ApiEntrypoint | AllInOneEntrypoint;
452
+ /** The fetch-handler — wired into Bun.serve in production, called
453
+ * directly in tests. Composes Hono + static-fallback. Apps with
454
+ * `autoListen: false` that call `Bun.serve` themselves must pass the
455
+ * 2nd arg from `server.requestIP(req)?.address`, extracted in their
456
+ * outermost Bun fetch callback — `requestIP` only resolves for Bun's
457
+ * original Request instance, never a cloned/rebuilt one. */
458
+ readonly fetch: (req: Request, socketAddress?: string) => Promise<Response> | Response;
459
+ /** Serves WebSocket-upgrade requests (r.webSocketRoute) with Bun's original
460
+ * Request. Apps with `autoListen: false` that run their own `Bun.serve`
461
+ * must pass it as `{ upgradeFetch }` in the 4th argument of `buildBunServeOptions` to get
462
+ * WebSockets; `listen()` already wires it. */
463
+ readonly webSocketUpgradeFetch: (req: Request, env: KumikoServeEnv) => Promise<Response>;
464
+ /** Active Bun-server (only set when listen() was called — tests skip
465
+ * listen() because Bun.serve isn't available under vitest/node). */
466
+ server?: ReturnType<typeof Bun.serve>;
467
+ /** Bind to PORT and start serving. Production calls this; tests don't. */
468
+ readonly listen: (port?: number) => Promise<void>;
469
+ readonly stop: () => Promise<void>;
470
+ };
471
+ export declare function runProdApp(options: RunProdAppOptions): Promise<ProdAppHandle>;