@cosmicdrift/kumiko-server-runtime 0.386.0 → 0.387.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.
@@ -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";
@@ -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
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- // Public API für den Kumiko-Production-Server-Boot. Symmetrisch zu
2
- // runDevApp (kumiko-dev-server), aber ohne Dev-/Scaffold-/Codegen-
3
- // Tooling (ts-morph) als Dependency — Prod-Apps ziehen so kein
4
- // Dev-Tooling mehr in ihre node_modules.
1
+ // Public API for Kumiko's production-server boot. Symmetric with
2
+ // runDevApp (kumiko-dev-server), but without Dev/Scaffold/Codegen
3
+ // tooling (ts-morph) as a dependency, so production apps do not pull
4
+ // development tooling into their node_modules.
5
5
  export { resolveBillingOption } from "./boot-options/billing-option.js";
6
6
  export { buildDemoModeRoute, withDemoReadOnlyFetch, } from "./boot-options/demo-mode.js";
7
7
  export { resolveFeatureTogglesOption, } from "./boot-options/feature-toggles-option.js";
@@ -13,7 +13,7 @@ export function assertPiiBootInvariants(features, opts) {
13
13
  const piiEntities = features.flatMap((feature) => Object.entries(feature.entities ?? {})
14
14
  .filter(([, entity]) => collectPiiSubjectFields(entity).length > 0)
15
15
  .map(([name]) => name));
16
- // fw#2776: a declared event stance is worth as much as an entity annotation —
16
+ // A declared event stance is worth as much as an entity annotation —
17
17
  // encryptEventPayloadPii no-ops without a KMS, so a catalogued event writes
18
18
  // plaintext PII into kumiko_events with nothing to crypto-shred later.
19
19
  const piiEvents = features.flatMap((feature) => Object.values(feature.events ?? {})
@@ -1,11 +1,9 @@
1
- // Resolved den lokal installierten @tailwindcss/cli-Bin auf seinen
2
- // absoluten Dateipfad. Wir vermeiden `bunx`, weil das auch bei lokal
3
- // installiertem Package noch das Registry-Manifest fragt und ohne Netz
4
- // mit `FailedToOpenSocket` stirbt.
1
+ // Resolves the locally installed @tailwindcss/cli binary to its absolute path.
2
+ // We avoid `bunx` because it still requests the registry manifest for a locally
3
+ // installed package and fails with `FailedToOpenSocket` when offline.
5
4
  //
6
- // Ausgelagert aus create-kumiko-server.ts, damit unter vitest/Node
7
- // testbar (Bun-Branch + Resolve-Fail-Branch) ohne den Server zu
8
- // booten.
5
+ // Kept separate from create-kumiko-server.ts so the Bun and resolution-failure
6
+ // branches can be tested under Vitest/Node without booting the server.
9
7
  import { dirname, resolve } from "node:path";
10
8
  export function resolveTailwindCli(deps) {
11
9
  if (deps.bun === undefined)
@@ -39,8 +39,8 @@ type BootExtraContextOptions = {
39
39
  readonly appOverrides?: ConfigOverridesInput;
40
40
  };
41
41
  export declare function buildBootExtraContext(opts: BootExtraContextOptions): Record<string, unknown>;
42
- /** Die Auth-Felder die resolveAuthMail liest/normalisiert — beide
43
- * App-Auth-Typen (prod + dev) erfüllen das strukturell. */
42
+ /** The auth fields read and normalized by resolveAuthMail; both
43
+ * app auth types (prod + dev) satisfy this structurally. */
44
44
  type AuthMailNormalizable = {
45
45
  readonly mail?: AuthMailOptions;
46
46
  readonly passwordReset?: PasswordResetSetup;
@@ -17,14 +17,14 @@ import { resolveBootCrypto } from "./boot/boot-crypto.js";
17
17
  // Boot-time context helpers for runProdApp: ctx-extra-context wiring
18
18
  // (templateResolver/delivery/secrets/config-resolver), auth-mail convenience
19
19
  // normalization, and prod session-auth wiring. Split out of run-prod-app.ts
20
- // (#1005, Welle 2) — mechanical relocation, these are self-contained pure
20
+ // — mechanical relocation, these are self-contained pure
21
21
  // functions, no closure over runProdApp's local boot state.
22
22
  // Shared with runDevApp (mergeConfigResolverDefault) for dev/prod parity.
23
23
  export function addConfigAccessorFactory(resolved, registry) {
24
24
  if (!resolved.configResolver)
25
25
  return resolved;
26
26
  // Both keys, always overriding an app-supplied value: the factory must derive from the
27
- // effective resolver (fw#3313); the underscore alias stays until the next major.
27
+ // effective resolver; the underscore alias stays until the next major.
28
28
  const configAccessorFactory = createConfigAccessorFactory(registry, resolved.configResolver);
29
29
  return {
30
30
  ...resolved,
@@ -32,20 +32,19 @@ export function addConfigAccessorFactory(resolved, registry) {
32
32
  _configAccessorFactory: configAccessorFactory,
33
33
  };
34
34
  }
35
- // Framework-Default-Provider für den AppContext — gleicher Mechanismus wie
36
- // der tenantTierResolver-Autowire (findTierResolverUsage): deklarierter
37
- // Bedarf (Feature gemountet) → Default aus db/env, App überschreibt nur die
38
- // Ausnahme. templateResolver ist unbedingt (createTemplateResolverApi wirft nie, baut
39
- // nur einen db-gebundenen Accessor). secrets wird nur auto-verdrahtet wenn
40
- // das secrets-Feature gemountet ist UND ein KEK tatsächlich verfügbar ist
41
- // (masterKey-Override ODER env-KEK present) — sonst skip, damit der eager
42
- // KEK-Detection + Provider/Cipher-Aufbau leben in boot/boot-crypto.ts
43
- // (envHasMasterKek, resolveBootCrypto) — gemeinsam mit runDevApp.
44
- // Prod-Misconfig (secrets gemountet, kein KEK) fängt schon secretsEnvSchema
45
- // beim Boot; fehlt der env-Schema-Pfad, wirft requireSecretsContext beim
46
- // ersten ctx.secrets-Zugriff mit Wiring-Hinweis. configResolver nur im
47
- // Auth-Mode. Exportiert + pure für Unit-Tests; der merge mit App-Werten
48
- // passiert beim Caller (App gewinnt).
35
+ // Framework-default providers for AppContext, using the same mechanism as
36
+ // tenantTierResolver auto-wiring (findTierResolverUsage): a mounted feature declares
37
+ // the need, the default comes from db/env, and the app can override the exception.
38
+ // templateResolver is always wired (createTemplateResolverApi never throws; it only
39
+ // creates a db-bound accessor). secrets is auto-wired only when the secrets feature is
40
+ // mounted AND a KEK is available (masterKey override OR env KEK present); otherwise it
41
+ // is skipped so eager KEK detection and provider/cipher construction remain in
42
+ // boot/boot-crypto.ts (envHasMasterKek, resolveBootCrypto), shared with runDevApp.
43
+ // A production misconfiguration (secrets mounted without a KEK) is caught by
44
+ // secretsEnvSchema during boot; without the env-schema path, requireSecretsContext
45
+ // throws with a wiring hint on the first ctx.secrets access. configResolver is only
46
+ // wired in auth mode. Exported and pure for unit tests; the caller merges it with
47
+ // app values (the app wins).
49
48
  // Prod/dev parity for ctx.notify: without this `_notifyFactory` is only wired
50
49
  // in tests (createDeliveryTestContext), so ctx.notify is undefined at runtime
51
50
  // and every notification silently skips. sseBroker optional (email/push don't
@@ -219,17 +218,17 @@ export function resolveAuthMail(auth, hmacSecret, envSource, mailWiring = {}) {
219
218
  const withResolvedSecrets = backfillTokenSecrets(auth, tokenSecret);
220
219
  if (!auth.mail)
221
220
  return withResolvedSecrets;
222
- // SMTP-presence gate: ohne SMTP_HOST-env wird KEIN Flow verdrahtet (Routes
223
- // blieben sonst 500). Der eigentliche Mail-Versand läuft über delivery
224
- // (channel-email), nicht über diesen Transport — er ist nur der Detektor
225
- // "ist Mail konfiguriert?".
221
+ // SMTP-presence gate: without SMTP_HOST in the environment, no flow is wired
222
+ // (the routes would otherwise return 500). Mail delivery itself runs through
223
+ // delivery (channel-email), not this transport; it only detects whether mail is
224
+ // configured.
226
225
  // An app transport from runProdApp's `mail` option replaces the probe.
227
226
  if (mailWiring.mailConfigured !== true &&
228
227
  !createSmtpTransportFromEnv(envSource, { fallbackFrom: auth.mail.from ?? "noreply@localhost" })) {
229
228
  return withResolvedSecrets;
230
229
  }
231
230
  const paths = makeAuthPaths(auth.mail.paths);
232
- // appName/locale fließen in alle vier Flow-Options (alle mailen via delivery).
231
+ // appName/locale flow into all four flow options because they all send through delivery.
233
232
  const mailPresentation = {
234
233
  ...(auth.mail.appName !== undefined && { appName: auth.mail.appName }),
235
234
  ...(auth.mail.locale !== undefined && { locale: auth.mail.locale }),
@@ -266,7 +265,7 @@ export function resolveAuthMail(auth, hmacSecret, envSource, mailWiring = {}) {
266
265
  };
267
266
  }
268
267
  export async function buildProdSessionAuth(db, registry, sessionsFeature, mfaFeature, patFeature) {
269
- // Resolve the sessions feature sessionStore provider (#1372).
268
+ // Resolve the sessions feature sessionStore provider.
270
269
  const store = await resolveSessionStore({ db, registry });
271
270
  if (sessionsFeature) {
272
271
  bindAutoRevokeFromFeature(sessionsFeature)?.(store.massRevoker);