@cosmicdrift/kumiko-server-runtime 0.386.0 → 0.388.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -57
  2. package/dist/boot/boot-crypto.js +4 -5
  3. package/dist/boot/shared-boot-steps.d.ts +43 -0
  4. package/dist/boot/shared-boot-steps.js +154 -0
  5. package/dist/boot/wait-for-database.d.ts +3 -0
  6. package/dist/boot/wait-for-database.js +3 -3
  7. package/dist/boot/worker-boot-core.js +20 -40
  8. package/dist/boot-options/billing-option.d.ts +18 -9
  9. package/dist/boot-options/billing-option.js +44 -18
  10. package/dist/boot-options/file-storage-option.js +6 -6
  11. package/dist/boot-options/option-env-schema.js +11 -2
  12. package/dist/boot-options/resolve-boot-options.js +9 -3
  13. package/dist/boot-options/stack-boot-requirements.js +1 -1
  14. package/dist/build-prod-bundle.d.ts +16 -15
  15. package/dist/build-prod-bundle.js +106 -108
  16. package/dist/csp-hashes.js +1 -1
  17. package/dist/extra-routes-deps.d.ts +1 -1
  18. package/dist/fatal-error-handlers.js +7 -0
  19. package/dist/index.js +4 -4
  20. package/dist/pii-boot-gate.js +1 -1
  21. package/dist/require-env.d.ts +2 -0
  22. package/dist/require-env.js +21 -0
  23. package/dist/resolve-tailwind-cli.js +5 -7
  24. package/dist/run-bootstrap.d.ts +1 -1
  25. package/dist/run-bootstrap.js +1 -1
  26. package/dist/run-prod-app-auth.d.ts +17 -0
  27. package/dist/run-prod-app-auth.js +83 -0
  28. package/dist/run-prod-app-boot-context.d.ts +2 -2
  29. package/dist/run-prod-app-boot-context.js +28 -25
  30. package/dist/run-prod-app-config.d.ts +36 -0
  31. package/dist/run-prod-app-config.js +169 -0
  32. package/dist/run-prod-app-connections.d.ts +16 -0
  33. package/dist/run-prod-app-connections.js +55 -0
  34. package/dist/run-prod-app-entrypoint.d.ts +14 -0
  35. package/dist/run-prod-app-entrypoint.js +147 -0
  36. package/dist/run-prod-app-handle.d.ts +20 -0
  37. package/dist/run-prod-app-handle.js +104 -0
  38. package/dist/run-prod-app-request-context.d.ts +15 -0
  39. package/dist/run-prod-app-request-context.js +103 -0
  40. package/dist/run-prod-app-seeds.d.ts +13 -0
  41. package/dist/run-prod-app-seeds.js +86 -0
  42. package/dist/run-prod-app-static-files.js +43 -50
  43. package/dist/run-prod-app.d.ts +93 -103
  44. package/dist/run-prod-app.js +75 -772
  45. package/dist/run-worker-app.d.ts +1 -1
  46. package/dist/run-worker-app.js +5 -5
  47. package/dist/session-boot-gate.d.ts +1 -1
  48. package/dist/session-boot-gate.js +1 -1
  49. package/dist/session-wiring.d.ts +1 -1
  50. package/dist/session-wiring.js +1 -1
  51. package/dist/try-hono-first.js +1 -1
  52. package/package.json +16 -8
  53. package/src/changes.json +25 -0
@@ -31,10 +31,17 @@ function pickShape(shape, keys) {
31
31
  }
32
32
  return picked;
33
33
  }
34
+ function sysadminTenantNeedsEnvId(seeds) {
35
+ const { sysadminFromEnv } = seeds;
36
+ return (typeof sysadminFromEnv === "object" &&
37
+ sysadminFromEnv.tenant !== undefined &&
38
+ sysadminFromEnv.tenant.id === undefined);
39
+ }
34
40
  function seedEnvBlock(seeds) {
35
41
  const keys = [];
36
42
  const required = [];
37
- if (seeds.demoTenant && seeds.demoTenant.id === undefined) {
43
+ const demoTenantNeedsEnvId = seeds.demoTenant !== undefined && seeds.demoTenant.id === undefined;
44
+ if (demoTenantNeedsEnvId || sysadminTenantNeedsEnvId(seeds)) {
38
45
  keys.push("DEMO_TENANT_ID");
39
46
  required.push("DEMO_TENANT_ID");
40
47
  }
@@ -61,7 +68,9 @@ function fileStorageEnvBlocks(fileStorage) {
61
68
  blocks.push({
62
69
  source: "runProdApp.fileStorage",
63
70
  shape: s3EnvSchema.shape,
64
- required: fileStorage.provider === "s3-env" ? S3_REQUIRED_KEYS : [],
71
+ required: fileStorage.provider === "s3-env" || fileStorage.providers?.includes("s3-env")
72
+ ? S3_REQUIRED_KEYS
73
+ : [],
65
74
  });
66
75
  }
67
76
  for (const provider of fileStorage?.providers ?? []) {
@@ -1,6 +1,7 @@
1
1
  import { BILLING_FOUNDATION_FEATURE } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
2
2
  import { DELIVERY_FEATURE } from "@cosmicdrift/kumiko-bundled-features/delivery";
3
3
  import { FEATURE_TOGGLES_FEATURE } from "@cosmicdrift/kumiko-bundled-features/feature-toggles";
4
+ import { SUBSCRIPTION_DODO_FEATURE } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
4
5
  import { SUBSCRIPTION_STRIPE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
5
6
  import { TIER_ENGINE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
6
7
  import { resolveBillingOption } from "./billing-option.js";
@@ -82,10 +83,15 @@ export async function resolveBootOptionWiring(options, envSource) {
82
83
  envDefaults = { ...envDefaults, ...fileStorage.envDefaults };
83
84
  }
84
85
  if (options.billing) {
85
- requireFeatures(mounted, "billing.stripe", [BILLING_FOUNDATION_FEATURE, TIER_ENGINE_FEATURE], "Mount createBillingFoundationFeature() and createTierEngineFeature(), or composeSaasStack from @cosmicdrift/kumiko-dev-server/compose-stacks.");
86
- rejectDoubleMount(mounted, "billing.stripe", SUBSCRIPTION_STRIPE_FEATURE);
86
+ requireFeatures(mounted, "billing", [BILLING_FOUNDATION_FEATURE, TIER_ENGINE_FEATURE], "Mount createBillingFoundationFeature() and createTierEngineFeature(), or composeSaasStack from @cosmicdrift/kumiko-dev-server/compose-stacks.");
87
+ if (options.billing.stripe) {
88
+ rejectDoubleMount(mounted, "billing.stripe", SUBSCRIPTION_STRIPE_FEATURE);
89
+ }
90
+ if (options.billing.dodo) {
91
+ rejectDoubleMount(mounted, "billing.dodo", SUBSCRIPTION_DODO_FEATURE);
92
+ }
87
93
  const billing = resolveBillingOption(options.billing, envSource);
88
- features.push(billing.feature);
94
+ features.push(...billing.features);
89
95
  extraRoutes.push(billing.route);
90
96
  }
91
97
  const featureToggles = wireFeatureToggles(options, mounted);
@@ -23,7 +23,7 @@ const OPTION_CHECKS = {
23
23
  },
24
24
  billing: {
25
25
  isSatisfied: (options, mounted) => options.billing !== undefined || mountsExtension(mounted, SUBSCRIPTION_PROVIDER_EXTENSION),
26
- hint: "set billing: { stripe: { priceToTier }, defaultTier }",
26
+ hint: "set billing: { stripe: { priceToTier }, defaultTier } (or dodo)",
27
27
  },
28
28
  featureToggles: {
29
29
  isSatisfied: (options) => options.featureToggles === true,
@@ -26,28 +26,29 @@ export type ClientEntriesConfig = {
26
26
  export type BuildManifest = Readonly<Record<string, string>>;
27
27
  export type BuildResult = {
28
28
  readonly outDir: string;
29
- /** Logical → hashed-URL mapping. Beispiel:
29
+ /** Logical → hashed-URL mapping. Example:
30
30
  * { "client.js": "/assets/client-a3f2.js",
31
31
  * "styles.css": "/assets/styles-9b4c.css" } */
32
32
  readonly manifest: BuildManifest;
33
- /** Build-Identität (Hash über die Asset-URLs) + lesbarer Zeitstempel.
34
- * undefined bei vanilla public-only-Builds (kein Bundle). */
33
+ /** Build identity (hash of the asset URLs) plus a human-readable timestamp.
34
+ * undefined for vanilla public-only builds (no bundle). */
35
35
  readonly buildInfo?: BuildInfo;
36
36
  };
37
37
  /** Baked into index.html (`<meta name="kumiko-build">`) AND written as
38
38
  * dist/build-info.json. The UpdateChecker polls build-info.json and compares
39
39
  * `id` with the loaded build, showing a reload banner on drift. */
40
40
  export type BuildInfo = {
41
- /** Hash über die sortierten (content-gehashten) Asset-URLs. Ändert sich
42
- * gdw. sich ein Asset ändert — selbsttragend, kein Env-Var/Dockerfile. */
41
+ /** Hash of the sorted, content-hashed asset URLs. Changes exactly when an
42
+ * asset changes — self-contained, with no environment variable or Dockerfile. */
43
43
  readonly id: string;
44
- /** ISO-Zeitstempel des Builds, lesbare Anzeige-Version (ersetzt den rohen sha). */
44
+ /** ISO timestamp of the build, used as the human-readable display version
45
+ * instead of the raw hash. */
45
46
  readonly builtAt: string;
46
47
  };
47
48
  export declare const DEFAULT_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1,interactive-widget=resizes-content\" />\n <title>Kumiko</title>\n </head>\n <body>\n <div id=\"root\"></div>\n </body>\n</html>\n";
48
49
  /** Folder name relative to dist/ for hashed assets (JS, CSS, file-loader
49
- * outputs). Exported damit runProdApp dieselbe Konvention für Cache-
50
- * Header nutzt — Drift verhindern. */
50
+ * outputs). Exported so runProdApp uses the same convention for cache headers
51
+ * and cannot drift. */
51
52
  export declare const ASSETS_DIR = "assets";
52
53
  export declare function buildProdBundle(options?: BuildProdBundleOptions): Promise<BuildResult>;
53
54
  export type ClientEntry = {
@@ -71,7 +72,7 @@ export type ClientEntry = {
71
72
  * `kumiko-build`/`kumiko build` via `@cosmicdrift/kumiko-dev-server/build`,
72
73
  * not just from unit tests.
73
74
  *
74
- * Precedence (fw#2320 — explicit declaration, no filename inference):
75
+ * Precedence (explicit declaration, no filename inference):
75
76
  * 1. `declared.clientEntry` and `declared.clientEntries` both set → throw.
76
77
  * 2. `declared.clientEntries` (non-empty) → one entry per declaration.
77
78
  * 3. `declared.clientEntry` → single entry, name "client".
@@ -87,9 +88,9 @@ export declare function resolveClientEntries(cwd: string, declared: ClientEntrie
87
88
  export declare function readClientEntriesConfig(cwd: string): ClientEntriesConfig;
88
89
  type ResolvedStylesheet = {
89
90
  readonly path: string;
90
- /** True nur wenn wir auf das gepackte renderer-web-styles.css zurückfallen
91
- * (App hat clientEntry + renderer-web-Dep, aber kein eigenes src/styles.css).
92
- * Steuert den Shell-Klassen-Sentinel im Build. */
91
+ /** True only when falling back to the packaged renderer-web-styles.css
92
+ * (the app has clientEntry and a renderer-web dependency, but no
93
+ * src/styles.css). Controls the shell-class sentinel check during the build. */
93
94
  readonly isRendererWebFallback: boolean;
94
95
  };
95
96
  export declare function assertRendererWebShellPresent(css: string, stylesheet: ResolvedStylesheet): void;
@@ -98,9 +99,9 @@ export declare function runTailwindOnce(entry: string, cwd: string): Promise<str
98
99
  export declare function isEntryOutputFor(outName: string, baseName: string): boolean;
99
100
  export declare function injectAssetTags(html: string, manifest: BuildManifest, entry: ClientEntry, buildInfo?: BuildInfo): string;
100
101
  export declare function computeBuildId(manifest: BuildManifest): string;
101
- /** Formatiert ein BuildResult als CLI-freundliche Mehrzeilen-Zusammenfassung
102
- * mit ANSI-Farben. Wird sowohl von `kumiko build` als auch von dem
103
- * hoisted `kumiko-build`-Bin verwendet, damit das Output konsistent ist. */
102
+ /** Formats a BuildResult as a CLI-friendly, multi-line summary with ANSI
103
+ * colors. Used by both `kumiko build` and the hoisted `kumiko-build` binary
104
+ * so their output remains consistent. */
104
105
  export declare function formatBuildResult(result: BuildResult, durationMs: number): string;
105
106
  /** Hashes every inline script in the HTML files of a finished build output, so the
106
107
  * CSP only ever allows code that shipped in the artefact. */
@@ -1,9 +1,8 @@
1
- // buildProdBundle — Production-Build für Kumiko-Apps. Ein generischer
2
- // Build-Step ohne App-spezifisches Wissen: Convention-Discovery liest
3
- // die App-Struktur, Bun.build + Tailwind + Public-Folder-Copy
4
- // produzieren ein deploybares dist/.
1
+ // buildProdBundle — Production build for Kumiko apps. A generic build step
2
+ // with no app-specific knowledge: convention discovery reads the app structure;
3
+ // Bun.build + Tailwind + public-folder copying produce a deployable dist/.
5
4
  //
6
- // Client entries (fw#2320 — explicit, no filename inference):
5
+ // Client entries (explicit, no filename inference):
7
6
  //
8
7
  // package.json "kumiko.clientEntries": [{ name, sourceFile, htmlPath? }]
9
8
  // → one Bun.build bundle per declared entry
@@ -14,23 +13,22 @@
14
13
  // Convention for everything else (all optional, missing → skipped):
15
14
  //
16
15
  // src/styles.css → Tailwind one-shot
17
- // (oder fallback auf @cosmicdrift/kumiko-renderer/web/styles.css
18
- // wenn nur clientEntry da ist und kein eigenes CSS)
19
- // public/ → rsync 1:1 (kein Hash — User-bewusste URLs)
20
- // public/index.html | index.html → Template, Placeholder-Tags ersetzt:
16
+ // (or fallback to @cosmicdrift/kumiko-renderer/web/styles.css
17
+ // when only clientEntry exists and no custom CSS)
18
+ // public/ → rsync 1:1 (no hash — URLs are user-facing)
19
+ // public/index.html | index.html → template with placeholder tags replaced:
21
20
  // <script type="module" src="/client.js"> → /assets/client-<hash>.js
22
21
  // <link href="/styles.css"> → /assets/styles-<hash>.css
23
- // (kein HTML, vanilla) → Default-HTML ohne Asset-Tags
22
+ // (no HTML, vanilla) → default HTML without asset tags
24
23
  //
25
- // Fehler-Modus: hat client.tsx oder Tailwind etwas produziert, aber das HTML
26
- // hat keinen passenden Placeholder, wirft der Build mit dem exakten Snippet
27
- // zum Reinkopieren. Keine silent-injection — das HTML soll lesen wie's
28
- // auch im Dev-Server liefert.
24
+ // Failure mode: if client.tsx or Tailwind produced output but the HTML has no
25
+ // matching placeholder, the build throws with the exact snippet to copy in.
26
+ // No silent injection — the HTML should behave as it does in the dev server.
29
27
  //
30
28
  // Output:
31
29
  //
32
30
  // dist/
33
- // index.html ← Tags mit gehashten URLs
31
+ // index.html ← tags with hashed URLs
34
32
  // assets/
35
33
  // client-<hash>.js ← entry
36
34
  // <chunk>-<hash>.js ← split chunks
@@ -41,12 +39,12 @@
41
39
  // csp-hashes.json ← sha256 of every inline <script> in the HTML above
42
40
  // <public/* 1:1> ← favicon.ico, robots.txt, og-image.png, …
43
41
  //
44
- // Cache-Header (von runProdApp gesetzt, nicht hier):
42
+ // Cache headers (set by runProdApp, not here):
45
43
  //
46
44
  // /assets/* → public, max-age=31536000, immutable
47
45
  // /index.html, /sw.js → no-cache, must-revalidate
48
46
  // /manifest.json → no-cache
49
- // alles andere (public/) → default (auto-cache)
47
+ // everything else (public/) → default (auto-cache)
50
48
  var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
51
49
  if (typeof path === "string" && /^\.\.?\//.test(path)) {
52
50
  return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
@@ -66,18 +64,18 @@ import { BUNDLED_ASSETS_DIST_DIR, readBundledAssetDeclarations, resolveBundledAs
66
64
  import { CSP_HASHES_FILE, inlineScriptHashes } from "./csp-hashes.js";
67
65
  import { RENDERER_WEB_FONT_FILE_PATTERN, RENDERER_WEB_FONTS_DIST_DIR, resolveRendererWebFontsDir, } from "./renderer-web-fonts.js";
68
66
  import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli.js";
69
- // Bun-Runtime-Check als module-level Konstante: alle Build-Schritte
70
- // (Tailwind via Bun.spawn, Client-Bundle via Bun.build, Stylesheet-
71
- // Resolution via Bun.resolveSync) sind Bun-only. Pro-Funktions-Inline-
72
- // Checks driften sonst — eine Konstante hier hält das konsistent.
67
+ // Bun runtime check as a module-level constant: every build step (Tailwind via
68
+ // Bun.spawn, client bundling via Bun.build, and stylesheet resolution via
69
+ // Bun.resolveSync) is Bun-only. Per-function inline checks could diverge; one
70
+ // constant keeps the checks consistent.
73
71
  const hasBun = typeof Bun !== "undefined";
74
- // Default-HTML wird nur genutzt wenn der App-Author KEIN index.html liefert.
75
- // Hat keine Asset-Placeholder, weil der Default-Pfad für vanilla apps
76
- // (nur public/) gedacht ist — wer JS/CSS will, schreibt ein eigenes
77
- // index.html mit den richtigen Placeholder-Tags.
78
- // interactive-widget=resizes-content (fw#1918): keeps `position: fixed`
79
- // bottom bars anchored above a mobile keyboard instead of behind it. iOS Safari
80
- // ignores the key; renderer-web's useKeyboardInset covers it via window.visualViewport.
72
+ // The default HTML is used only when the app author provides no index.html.
73
+ // It has no asset placeholders because this path is intended for vanilla apps
74
+ // (public/ only); apps that need JS/CSS must provide an index.html with the
75
+ // appropriate placeholder tags.
76
+ // interactive-widget=resizes-content: keeps `position: fixed` bottom bars
77
+ // anchored above a mobile keyboard instead of behind it. iOS Safari ignores
78
+ // the key; renderer-web's useKeyboardInset covers it via window.visualViewport.
81
79
  export const DEFAULT_HTML = `<!doctype html>
82
80
  <html lang="en">
83
81
  <head>
@@ -91,8 +89,8 @@ export const DEFAULT_HTML = `<!doctype html>
91
89
  </html>
92
90
  `;
93
91
  /** Folder name relative to dist/ for hashed assets (JS, CSS, file-loader
94
- * outputs). Exported damit runProdApp dieselbe Konvention für Cache-
95
- * Header nutzt — Drift verhindern. */
92
+ * outputs). Exported so runProdApp uses the same convention for cache headers
93
+ * and cannot drift. */
96
94
  export const ASSETS_DIR = "assets";
97
95
  const ASSET_LOADERS = {
98
96
  ".png": "file",
@@ -111,7 +109,7 @@ export async function buildProdBundle(options = {}) {
111
109
  const cwd = resolve(options.cwd ?? process.cwd());
112
110
  const outDir = resolve(cwd, options.outDir ?? "dist");
113
111
  const assetsDir = join(outDir, ASSETS_DIR);
114
- // 1. Discovery: was ist da?
112
+ // 1. Discovery
115
113
  const clientEntries = resolveClientEntries(cwd, options);
116
114
  const firstClientSource = clientEntries[0]?.sourceFile;
117
115
  const stylesheet = resolveStylesheetEntry(cwd, firstClientSource, options.stylesheet);
@@ -128,9 +126,9 @@ export async function buildProdBundle(options = {}) {
128
126
  await mkdir(outDir, { recursive: true });
129
127
  await mkdir(assetsDir, { recursive: true });
130
128
  const manifest = {};
131
- // 3. Tailwind one-shot (vor JS, weil JS' loader auf .css trifft falls
132
- // der client.tsx ein "import './foo.css'" macht — den Fall lassen
133
- // wir hier raus, Tailwind ist die einzige CSS-Quelle).
129
+ // 3. Tailwind one-shot (before JS, because the JS loader would encounter .css
130
+ // if client.tsx imports "./foo.css" — that case is not supported here;
131
+ // Tailwind is the only CSS source).
134
132
  if (stylesheet) {
135
133
  const css = await runTailwindOnce(stylesheet.path, cwd);
136
134
  assertRendererWebShellPresent(css, stylesheet);
@@ -140,19 +138,19 @@ export async function buildProdBundle(options = {}) {
140
138
  manifest["styles.css"] = `/${ASSETS_DIR}/${filename}`;
141
139
  await copyRendererWebFonts(cwd, outDir);
142
140
  }
143
- // 4. Bun.build pro Entry (multi-entry produces N bundles + shared chunks).
144
- // Ein einzelner Bun.build-Call mit allen entrypoints würde shared
145
- // chunks deduplizieren, hashes deterministisch halten — passt zu
146
- // dem split-tree-Pattern von publicstatus (admin + public teilen
147
- // sich den renderer-web-core).
141
+ // 4. Bun.build per entry (multi-entry produces N bundles + shared chunks).
142
+ // A single Bun.build call with all entrypoints deduplicates shared chunks
143
+ // and keeps hashes deterministic, matching publicstatus's split-tree
144
+ // pattern (admin + public share renderer-web-core).
148
145
  if (clientEntries.length > 0) {
149
146
  const built = await buildClientBundles(clientEntries, assetsDir);
150
147
  for (const [manifestKey, filename] of Object.entries(built)) {
151
148
  manifest[manifestKey] = `/${ASSETS_DIR}/${filename}`;
152
149
  }
153
150
  }
154
- // 5. Public-Folder rsync (ohne index.html / *.html-templates — werden
155
- // separat gerendert). Filter-list = template-basenames der entries.
151
+ // 5. Public-folder rsync (excluding index.html / *.html templates, which are
152
+ // rendered separately). The filter list contains the entries' template
153
+ // basenames.
156
154
  const templateBasenames = new Set(clientEntries.map((e) => basenameOf(e.htmlPath)));
157
155
  if (hasPublicDir) {
158
156
  await copyPublicFolder(publicDir, outDir, templateBasenames);
@@ -160,21 +158,21 @@ export async function buildProdBundle(options = {}) {
160
158
  // 5a. Declared read-only assets (package.json "kumiko.assets"), copied after
161
159
  // public/ so a same-named public file cannot shadow them.
162
160
  await copyBundledAssets(cwd, outDir, bundledAssets);
163
- // 5b. Build-Identität: Hash über die content-gehashten Asset-URLs +
164
- // lesbarer Zeitstempel. Wird in index.html gebacken und als
165
- // build-info.json geschrieben (Update-Awareness, default an).
166
- // ponytail: id hängt nur an Assets — ein reiner Server-Deploy ohne
167
- // Client-Bundle-Change behält die id → kein Banner (die UI ist dann
168
- // nicht stale). Bei Bedarf an Server-Versionierung koppeln.
161
+ // 5b. Build identity: hash of the content-hashed asset URLs plus a readable
162
+ // timestamp. Baked into index.html and written to build-info.json
163
+ // (update awareness, enabled by default).
164
+ // ponytail: id depends only on assets — a server-only deploy with no
165
+ // client-bundle change keeps the id, so no banner appears (the UI is not
166
+ // stale). Couple it to the server version when needed.
169
167
  const buildInfo = Object.keys(manifest).length > 0
170
168
  ? { id: computeBuildId(manifest), builtAt: Temporal.Now.instant().toString() }
171
169
  : undefined;
172
- // 6. HTML pro Entry rendern. Convention: ein HTML-File pro Client-Entry,
173
- // jede mit ihrem eigenen Script-Tag. Server (runProdApp.hostDispatch)
174
- // serviert je nach Host das passende File.
170
+ // 6. Render HTML for each entry. Convention: one HTML file per client entry,
171
+ // each with its own script tag. The server (runProdApp.hostDispatch)
172
+ // serves the matching file for each host.
175
173
  if (clientEntries.length === 0) {
176
- // Vanilla-public-only-app: keine HTML-Files zu rendern, public-Folder
177
- // wurde schon kopiert.
174
+ // Vanilla public-only app: no HTML files need rendering; the public folder
175
+ // was already copied.
178
176
  }
179
177
  else {
180
178
  for (const entry of clientEntries) {
@@ -204,7 +202,7 @@ const SOURCE_EXTENSION_PATTERN = /\.(?:tsx?|jsx?)$/;
204
202
  * `kumiko-build`/`kumiko build` via `@cosmicdrift/kumiko-dev-server/build`,
205
203
  * not just from unit tests.
206
204
  *
207
- * Precedence (fw#2320 — explicit declaration, no filename inference):
205
+ * Precedence (explicit declaration, no filename inference):
208
206
  * 1. `declared.clientEntry` and `declared.clientEntries` both set → throw.
209
207
  * 2. `declared.clientEntries` (non-empty) → one entry per declaration.
210
208
  * 3. `declared.clientEntry` → single entry, name "client".
@@ -320,7 +318,7 @@ function resolveConventionEntries(cwd) {
320
318
  }
321
319
  return [];
322
320
  }
323
- // Pre-fw#2320 multi-entry convention: any src/client-<suffix>.tsx(x) became
321
+ // Legacy multi-entry convention: any src/client-<suffix>.tsx(x) became
324
322
  // its own bundle. Detecting these without a declaration means an unmigrated
325
323
  // app would silently ship without their bundles — throw instead.
326
324
  function findLegacyClientFiles(cwd) {
@@ -436,9 +434,10 @@ function discoverHtmlTemplateFor(cwd, basename) {
436
434
  }
437
435
  return undefined;
438
436
  }
439
- // Äußerster Wrapper von DefaultAppShell/WorkspaceShell trägt diese Utility
440
- // (app-layout.tsx: "flex min-h-screen …"). Echtes Tailwind-Utility (landet
441
- // also im kompilierten Output) ohne CSS-Escaping → stabiler Substring-Sentinel.
437
+ // The outer wrapper of DefaultAppShell/WorkspaceShell carries this utility
438
+ // (app-layout.tsx: "flex min-h-screen …"). It is a real Tailwind utility in
439
+ // the compiled output, so it provides a stable substring sentinel without CSS
440
+ // escaping.
442
441
  const RENDERER_WEB_SHELL_SENTINEL = "min-h-screen";
443
442
  function resolveStylesheetEntry(cwd, clientEntry, override) {
444
443
  if (override === false)
@@ -446,13 +445,13 @@ function resolveStylesheetEntry(cwd, clientEntry, override) {
446
445
  if (typeof override === "string") {
447
446
  return { path: resolve(cwd, override), isRendererWebFallback: false };
448
447
  }
449
- // App-eigenes styles.css schlägt den Default.
448
+ // An app's own styles.css takes precedence over the default.
450
449
  const local = resolve(cwd, "src/styles.css");
451
450
  if (existsSync(local))
452
451
  return { path: local, isRendererWebFallback: false };
453
- // Sonst: nur wenn ein client da ist, fallback auf renderer-web/styles.css.
454
- // Sample-Apps und Showcases nutzen das alle — gleiche Logik wie der dev-
455
- // server, damit lokal/prod identisch bauen.
452
+ // Otherwise, fall back to renderer-web/styles.css only when a client exists.
453
+ // Sample apps and showcases all use this, matching the dev server so local
454
+ // and production builds behave identically.
456
455
  if (!clientEntry)
457
456
  return undefined;
458
457
  if (!hasBun)
@@ -468,12 +467,12 @@ function resolveStylesheetEntry(cwd, clientEntry, override) {
468
467
  return undefined;
469
468
  }
470
469
  }
471
- // Build-Zeit-Guard gegen unstyled prod (#359): fällt der Build auf renderer-
472
- // web/styles.css zurück und fehlt im kompilierten CSS die Shell-Sentinel-
473
- // Klasse, wurden die DefaultAppShell-Styles nicht gescannt (renderer-web
474
- // nicht dort installiert wo Tailwind scannt, oder ein @source-Regress).
475
- // Laut failen statt ein 15KB-Image zu liefern, das in prod nackt rendert.
476
- // @internal — exportiert nur für Unit-Tests.
470
+ // Build-time guard against unstyled production output: when the build falls
471
+ // back to renderer-web/styles.css but the compiled CSS lacks the shell sentinel,
472
+ // DefaultAppShell styles were not scanned (renderer-web is not installed where
473
+ // Tailwind scans, or an @source regression occurred).
474
+ // Fail rather than shipping a 15KB image that renders unstyled in production.
475
+ // @internal — exported only for unit tests.
477
476
  export function assertRendererWebShellPresent(css, stylesheet) {
478
477
  // skip: not the renderer-web fallback stylesheet, invariant doesn't apply
479
478
  if (!stylesheet.isRendererWebFallback)
@@ -517,7 +516,7 @@ async function copyBundledAssets(cwd, outDir, declarations) {
517
516
  await cp(source, join(target, declaration.name));
518
517
  }
519
518
  }
520
- // @internal — exported nur für Unit-Tests.
519
+ // @internal — exported only for unit tests.
521
520
  export function discoverHtmlTemplate(cwd) {
522
521
  for (const candidate of ["index.html", "public/index.html"]) {
523
522
  const path = resolve(cwd, candidate);
@@ -529,7 +528,7 @@ export function discoverHtmlTemplate(cwd) {
529
528
  // ---------------------------------------------------------------------------
530
529
  // Build steps
531
530
  // ---------------------------------------------------------------------------
532
- // @internal — exportiert nur für Unit-Tests (Relocation-Test des @source-Layouts).
531
+ // @internal — exported only for unit tests (relocation test for the @source layout).
533
532
  export async function runTailwindOnce(entry, cwd) {
534
533
  if (!hasBun) {
535
534
  throw new Error("[kumiko build] Tailwind one-shot requires Bun (Bun.spawn) — run via `bun run …` or `bun kumiko build`.");
@@ -602,9 +601,9 @@ export function isEntryOutputFor(outName, baseName) {
602
601
  return false;
603
602
  return /^[A-Za-z0-9]+$/.test(outName.slice(prefix.length, -".js".length));
604
603
  }
605
- /** Multi-entry-build: ein Bun.build-Call mit allen entrypoints — shared
606
- * chunks werden dedupliziert, hashes deterministisch. Returns map
607
- * manifestKey → hashed-filename (basename, ohne /assets/-Prefix). */
604
+ /** Multi-entry build: one Bun.build call with all entrypoints, so shared
605
+ * chunks are deduplicated and hashes remain deterministic. Returns a map of
606
+ * manifestKey → hashed filename (basename, without the /assets/ prefix). */
608
607
  async function buildClientBundles(entries, outDir) {
609
608
  if (!hasBun) {
610
609
  throw new Error("[kumiko build] requires Bun — run via `bun run …` or `bun kumiko build`.");
@@ -615,9 +614,9 @@ async function buildClientBundles(entries, outDir) {
615
614
  target: "browser",
616
615
  splitting: true,
617
616
  minify: true,
618
- // Keine Source-Maps in Prod: 1.6 MB+ Müll im Container, plus
619
- // exposed Source-Code reverse-engineerable. Dev hat seine eigenen
620
- // sourcemaps via create-kumiko-server.ts.
617
+ // No source maps in production: 1.6 MB+ of container waste, plus exposed
618
+ // reverse-engineerable source code. Development has its own sourcemaps via
619
+ // create-kumiko-server.ts.
621
620
  sourcemap: "none",
622
621
  naming: {
623
622
  entry: "[name]-[hash].[ext]",
@@ -637,9 +636,9 @@ async function buildClientBundles(entries, outDir) {
637
636
  if (entryOutputs.length !== entries.length) {
638
637
  throw new Error(`[kumiko build] expected ${entries.length} entry-point outputs, got ${entryOutputs.length}`);
639
638
  }
640
- // Bun.build benennt entry-files nach Source-Basename (ohne extension):
641
- // `src/client-admin.tsx` → `client-admin-<hash>.js`. Wir mappen jedes
642
- // entry-output zurück auf seinen ClientEntry via Basename-match.
639
+ // Bun.build names entry files after the source basename (without the extension):
640
+ // `src/client-admin.tsx` → `client-admin-<hash>.js`. Map each entry output
641
+ // back to its ClientEntry by matching the basename.
643
642
  const result = {};
644
643
  for (const entry of entries) {
645
644
  const baseName = (entry.sourceFile.split("/").pop() ?? "").replace(SOURCE_EXTENSION_PATTERN, "");
@@ -655,9 +654,9 @@ function basenameOf(p) {
655
654
  return p.split("/").pop() ?? p;
656
655
  }
657
656
  async function copyPublicFolder(src, dst, templateBasenames) {
658
- // HTML-templates werden separat gerendert — nicht blind kopieren, sonst
659
- // überschreibt das die injizierte Version. (z.B. index.html, admin.html
660
- // bei multi-entry).
657
+ // HTML templates are rendered separately rather than copied blindly, because
658
+ // copying them would overwrite the injected version (for example index.html
659
+ // or admin.html in a multi-entry build).
661
660
  await cp(src, dst, {
662
661
  recursive: true,
663
662
  filter: (source) => {
@@ -674,10 +673,10 @@ async function copyPublicFolder(src, dst, templateBasenames) {
674
673
  // HTML render
675
674
  // ---------------------------------------------------------------------------
676
675
  async function renderHtml(templatePath, manifest, entry, buildInfo) {
677
- // Edge-Case: kein eigenes HTML-Template + Bun.build oder Tailwind hat
678
- // Output produziert. DEFAULT_HTML hat keine Placeholder (vanilla
679
- // template), also würde injectAssetTags eh fehlschlagen. Klarer Fehler
680
- // mit Vorschlag-Snippet zum Reinkopieren.
676
+ // Edge case: no custom HTML template exists, but Bun.build or Tailwind produced
677
+ // output. DEFAULT_HTML has no placeholders (it is a vanilla template), so
678
+ // injectAssetTags would fail anyway. Report a clear error with a snippet to
679
+ // copy in.
681
680
  if (!templatePath && Object.keys(manifest).length > 0) {
682
681
  throw new Error(buildMissingTemplateError(manifest, entry));
683
682
  }
@@ -691,7 +690,7 @@ function buildMissingTemplateError(manifest, entry) {
691
690
  ? ` <link rel="stylesheet" href="/styles.css" />\n`
692
691
  : "";
693
692
  const jsLine = manifest[entry.manifestKey]
694
- ? // html-ok: Error-Hilfetext (Tag-Snippet als Text), wird nie gerendert.
693
+ ? // html-ok: Error-help text (tag snippet as text), never rendered.
695
694
  ` <script type="module" src="/${entry.manifestKey}"></script>\n`
696
695
  : "";
697
696
  const sourceBasename = basenameOf(entry.sourceFile);
@@ -723,18 +722,17 @@ function buildMissingTemplateError(manifest, entry) {
723
722
  `Der Build ersetzt /styles.css und /${entry.manifestKey} durch die gehashten URLs.`);
724
723
  }
725
724
  const BUILD_META_NAME = "kumiko-build";
726
- // @internal — exported nur für Unit-Tests.
725
+ // @internal — exported only for unit tests.
727
726
  //
728
- // Convention: das HTML-Template MUSS Placeholder-Tags für jedes Asset
729
- // dieses Entries enthalten:
730
- // - `<script type="module" src="/client.js">` für single-mode entry "client"
731
- // - `<script type="module" src="/client-<name>.js">` für multi-mode entry "<name>"
732
- // - `<link href="/styles.css">` für styles (gemeinsam über alle entries)
733
- // Der Build ersetzt sie durch die gehashten URLs.
727
+ // The HTML template MUST contain placeholder tags for every asset of this entry:
728
+ // - `<script type="module" src="/client.js">` for single-mode entry "client"
729
+ // - `<script type="module" src="/client-<name>.js">` for multi-mode entry "<name>"
730
+ // - `<link href="/styles.css">` for styles shared by all entries
731
+ // The build replaces them with hashed URLs.
734
732
  //
735
- // Fehlt ein erwarteter Tag, wirft der Build einen Fehler mit dem exakten
736
- // Snippet zum Reinkopieren — kein silent injection mehr, weil das den
737
- // Diff zwischen Dev- und Prod-HTML unsichtbar macht.
733
+ // If an expected tag is missing, the build throws an error with the exact
734
+ // snippet to copy in — no silent injection, which would hide differences
735
+ // between development and production HTML.
738
736
  export function injectAssetTags(html, manifest, entry, buildInfo) {
739
737
  let result = html;
740
738
  // Meta tag instead of an inline script: strict CSPs (script-src 'self') block inline scripts.
@@ -755,12 +753,12 @@ export function injectAssetTags(html, manifest, entry, buildInfo) {
755
753
  hashedAssetHint: "/assets/styles-<hash>.css",
756
754
  }));
757
755
  }
758
- // html-ok: cssUrl ist ein selbst-generierter Manifest-Asset-Pfad (Hash-Name).
756
+ // html-ok: cssUrl is a self-generated manifest asset path (hashed name).
759
757
  result = result.replace(placeholder[0], `<link rel="stylesheet" href="${cssUrl}" />`);
760
758
  }
761
759
  const jsUrl = manifest[entry.manifestKey];
762
760
  if (jsUrl && !result.includes(jsUrl)) {
763
- // Placeholder-pattern: src="/client.js" oder src="/client-<name>.js"
761
+ // Placeholder pattern: src="/client.js" or src="/client-<name>.js"
764
762
  const placeholderRx = new RegExp(`<script\\b[^>]*src="\\/${entry.manifestKey.replace(/\./g, "\\.")}"[^>]*><\\/script>`);
765
763
  const placeholder = placeholderRx.exec(result);
766
764
  if (!placeholder) {
@@ -773,13 +771,13 @@ export function injectAssetTags(html, manifest, entry, buildInfo) {
773
771
  hashedAssetHint: `/assets/${baseAssetName}-<hash>.js`,
774
772
  }));
775
773
  }
776
- // html-ok: jsUrl ist ein selbst-generierter Manifest-Asset-Pfad (Hash-Name).
774
+ // html-ok: jsUrl is a self-generated manifest asset path (hashed name).
777
775
  result = result.replace(placeholder[0], `<script type="module" src="${jsUrl}"></script>`);
778
776
  }
779
777
  return result;
780
778
  }
781
- /** Einheitliche Error-Form für fehlende Asset-Tags im HTML-Template
782
- * (script-tag fürs JS-Bundle ODER stylesheet-link für Tailwind). */
779
+ /** Consistent error format for missing asset tags in the HTML template
780
+ * (the script tag for the JS bundle or the stylesheet link for Tailwind). */
783
781
  function buildMissingTagError(args) {
784
782
  const tpl = basenameOf(args.htmlPath);
785
783
  return (`[kumiko build] ${tpl} hat keinen Entry-Tag für /${args.assetKey} — füg ${args.insertHint} ein:\n` +
@@ -794,10 +792,10 @@ function buildMissingTagError(args) {
794
792
  function shortHash(content) {
795
793
  return createHash("sha256").update(content).digest("hex").slice(0, 8);
796
794
  }
797
- // Build-Identität aus den content-gehashten Asset-URLs. Sortiert →
798
- // deterministisch (Manifest-Key-Reihenfolge egal). Identischer Source →
799
- // gleiche id, geändertes Asset → andere id.
800
- // @internal — exported nur für Unit-Tests.
795
+ // Build identity from the content-hashed asset URLs. Sorted for determinism
796
+ // (manifest-key order does not matter). Identical source produces the same id;
797
+ // a changed asset produces a different id.
798
+ // @internal — exported only for unit tests.
801
799
  export function computeBuildId(manifest) {
802
800
  return createHash("sha256")
803
801
  .update(Object.values(manifest).sort().join("\n"))
@@ -807,9 +805,9 @@ export function computeBuildId(manifest) {
807
805
  // ---------------------------------------------------------------------------
808
806
  // CLI output
809
807
  // ---------------------------------------------------------------------------
810
- /** Formatiert ein BuildResult als CLI-freundliche Mehrzeilen-Zusammenfassung
811
- * mit ANSI-Farben. Wird sowohl von `kumiko build` als auch von dem
812
- * hoisted `kumiko-build`-Bin verwendet, damit das Output konsistent ist. */
808
+ /** Formats a BuildResult as a CLI-friendly, multi-line summary with ANSI
809
+ * colors. Used by both `kumiko build` and the hoisted `kumiko-build` binary
810
+ * so their output remains consistent. */
813
811
  export function formatBuildResult(result, durationMs) {
814
812
  const dim = "\x1b[2m";
815
813
  const green = "\x1b[32m";
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { isPlainObject, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
3
3
  import { isScriptHash } from "./security-headers.js";
4
4
  export const CSP_HASHES_FILE = "csp-hashes.json";
5
- const INLINE_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
5
+ const INLINE_SCRIPT_PATTERN = /<script\b((?:"[^"]*"|'[^']*'|[^'">])*)>([\s\S]*?)<\/script\s*>/gi;
6
6
  const SRC_ATTRIBUTE_PATTERN = /\ssrc\s*=/i;
7
7
  const TYPE_ATTRIBUTE_PATTERN = /\stype\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
8
8
  // Data blocks (application/json, ld+json, templates) never execute, so
@@ -7,7 +7,7 @@ import type { Redis } from "ioredis";
7
7
  export type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
8
8
  export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
9
9
  /** Deps for the `wire` hook (runProdApp/createKumikoServer, after
10
- * buildServer has mounted `extraRoutes` — see kumiko-framework#3050) and
10
+ * buildServer has mounted `extraRoutes`) and
11
11
  * for `runWorkerApp.wireComponents`. Naming: `deps` not `ctx` because
12
12
  * `ctx` is the framework's HandlerContext (user/tenant/registry) — this
13
13
  * scope is deliberately smaller and has NO `app` (routes are declared via