@cosmicdrift/kumiko-server-runtime 0.388.0 → 0.390.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 (55) hide show
  1. package/dist/boot/apply-boot-seeds.js +11 -8
  2. package/dist/boot/job-run-logger.js +1 -1
  3. package/dist/boot/shared-boot-steps.d.ts +8 -5
  4. package/dist/boot/shared-boot-steps.js +26 -16
  5. package/dist/boot/stream-tenant-backfill-on-boot.d.ts +27 -0
  6. package/dist/boot/stream-tenant-backfill-on-boot.js +161 -0
  7. package/dist/boot/wait-for-database.d.ts +2 -0
  8. package/dist/boot/wait-for-database.js +4 -3
  9. package/dist/boot/warn-row-level-security-off.d.ts +3 -0
  10. package/dist/boot/warn-row-level-security-off.js +6 -0
  11. package/dist/boot/worker-boot-core.d.ts +2 -1
  12. package/dist/boot/worker-boot-core.js +28 -5
  13. package/dist/boot/worker-metrics-server.js +2 -0
  14. package/dist/boot-options/demo-mode.d.ts +3 -3
  15. package/dist/boot-options/demo-mode.js +4 -1
  16. package/dist/boot-options/resolve-boot-options.d.ts +1 -0
  17. package/dist/boot-options/resolve-boot-options.js +2 -2
  18. package/dist/boot-options/seed-shortcuts.js +3 -1
  19. package/dist/boot-options/stack-boot-requirements.d.ts +1 -1
  20. package/dist/boot-options/stack-boot-requirements.js +1 -13
  21. package/dist/build-prod-bundle.js +13 -13
  22. package/dist/bundled-lists-table.d.ts +2 -0
  23. package/dist/bundled-lists-table.js +56 -0
  24. package/dist/compose-auth-options.d.ts +16 -0
  25. package/dist/compose-auth-options.js +49 -0
  26. package/dist/compose-features.d.ts +5 -17
  27. package/dist/compose-features.js +13 -53
  28. package/dist/extra-routes-deps.d.ts +5 -1
  29. package/dist/extra-routes-deps.js +1 -0
  30. package/dist/files-for-tenant.d.ts +3 -0
  31. package/dist/files-for-tenant.js +10 -0
  32. package/dist/renderer-web-fonts.js +3 -9
  33. package/dist/resolve-installed-package.d.ts +10 -0
  34. package/dist/resolve-installed-package.js +32 -0
  35. package/dist/resolve-tailwind-cli.d.ts +3 -8
  36. package/dist/resolve-tailwind-cli.js +8 -25
  37. package/dist/run-prod-app-boot-context.d.ts +2 -0
  38. package/dist/run-prod-app-boot-context.js +11 -9
  39. package/dist/run-prod-app-config.d.ts +2 -0
  40. package/dist/run-prod-app-config.js +3 -1
  41. package/dist/run-prod-app-connections.d.ts +2 -1
  42. package/dist/run-prod-app-connections.js +11 -7
  43. package/dist/run-prod-app-entrypoint.d.ts +1 -1
  44. package/dist/run-prod-app-entrypoint.js +6 -2
  45. package/dist/run-prod-app-handle.js +4 -1
  46. package/dist/run-prod-app-request-context.js +9 -2
  47. package/dist/run-prod-app-seeds.js +39 -24
  48. package/dist/run-prod-app-static-files.js +9 -1
  49. package/dist/run-prod-app.d.ts +19 -4
  50. package/dist/run-prod-app.js +7 -3
  51. package/dist/run-worker-app.d.ts +22 -4
  52. package/dist/run-worker-app.js +22 -4
  53. package/dist/seed-dispatcher.js +3 -1
  54. package/package.json +19 -8
  55. package/src/changes.json +92 -0
@@ -29,18 +29,6 @@ const OPTION_CHECKS = {
29
29
  isSatisfied: (options) => options.featureToggles === true,
30
30
  hint: "set featureToggles: true",
31
31
  },
32
- demoMode: {
33
- isSatisfied: (options) => options.demoMode !== undefined,
34
- hint: "set demoMode: { readOnly }",
35
- },
36
- seeds: {
37
- isSatisfied: (options) => options.seeds !== undefined,
38
- hint: "set seeds",
39
- },
40
- signals: {
41
- isSatisfied: (options) => options.signals !== false,
42
- hint: "drop signals: false",
43
- },
44
32
  rateLimit: {
45
33
  isSatisfied: (options) => options.rateLimit !== false,
46
34
  hint: "drop rateLimit: false",
@@ -65,7 +53,7 @@ function unsatisfiedRequirements(options, severity, scope) {
65
53
  }
66
54
  /** Reads `bootRequires` from the raw feature list: dedupe may later drop the
67
55
  * tagged copy of a feature two stacks share. `scope` limits the check to the
68
- * options the calling boot function can set at all (runDevApp has no `signals`). */
56
+ * options the calling boot function can set at all. */
69
57
  export function assertStackBootRequirements(options, scope) {
70
58
  const gaps = unsatisfiedRequirements(options, "error", scope);
71
59
  if (gaps.length > 0) {
@@ -63,6 +63,7 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
63
63
  import { BUNDLED_ASSETS_DIST_DIR, readBundledAssetDeclarations, resolveBundledAssetSource, } from "./bundled-assets.js";
64
64
  import { CSP_HASHES_FILE, inlineScriptHashes } from "./csp-hashes.js";
65
65
  import { RENDERER_WEB_FONT_FILE_PATTERN, RENDERER_WEB_FONTS_DIST_DIR, resolveRendererWebFontsDir, } from "./renderer-web-fonts.js";
66
+ import { resolveInstalledSync } from "./resolve-installed-package.js";
66
67
  import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli.js";
67
68
  // Bun runtime check as a module-level constant: every build step (Tailwind via
68
69
  // Bun.spawn, client bundling via Bun.build, and stylesheet resolution via
@@ -454,18 +455,11 @@ function resolveStylesheetEntry(cwd, clientEntry, override) {
454
455
  // and production builds behave identically.
455
456
  if (!clientEntry)
456
457
  return undefined;
457
- if (!hasBun)
458
- return undefined;
459
- try {
460
- const resolved = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", cwd);
461
- if (!canResolveTailwindStylesheet(resolved, { bun: Bun, cwd })) {
462
- return undefined;
463
- }
464
- return { path: resolved, isRendererWebFallback: true };
465
- }
466
- catch {
458
+ const resolved = resolveInstalledSync("@cosmicdrift/kumiko-renderer/web/styles.css", cwd);
459
+ if (resolved === undefined || !canResolveTailwindStylesheet(resolved, { bun: Bun, cwd })) {
467
460
  return undefined;
468
461
  }
462
+ return { path: resolved, isRendererWebFallback: true };
469
463
  }
470
464
  // Build-time guard against unstyled production output: when the build falls
471
465
  // back to renderer-web/styles.css but the compiled CSS lacks the shell sentinel,
@@ -579,9 +573,15 @@ function isTailwindEngine(candidate) {
579
573
  // Both packages are the CLI's own dependencies, so they resolve from the CLI package directory
580
574
  // (same version the CLI would run), not from the app root.
581
575
  async function loadTailwindEngine(cliPath, bun) {
582
- const cliDir = dirname(bun.resolveSync("@tailwindcss/cli/package.json", dirname(cliPath)));
583
- const node = await import(__rewriteRelativeImportExtension(bun.resolveSync("@tailwindcss/node", cliDir)));
584
- const oxide = await import(__rewriteRelativeImportExtension(bun.resolveSync("@tailwindcss/oxide", cliDir)));
576
+ const cliDir = resolve(cliPath, "..", "..");
577
+ const importFromCli = (packageName) => {
578
+ const entry = resolveInstalledSync(packageName, cliDir, bun);
579
+ if (entry !== undefined)
580
+ return import(__rewriteRelativeImportExtension(entry));
581
+ throw new Error(`[kumiko build] ${packageName} is not installed next to @tailwindcss/cli (${cliDir}).`);
582
+ };
583
+ const node = await importFromCli("@tailwindcss/node");
584
+ const oxide = await importFromCli("@tailwindcss/oxide");
585
585
  const engine = {
586
586
  compile: isRecord(node) ? node["compile"] : undefined,
587
587
  optimize: isRecord(node) ? node["optimize"] : undefined,
@@ -0,0 +1,2 @@
1
+ import { type BundledListsOptions, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function applyBundledListsTable(features: readonly FeatureDefinition[], options: BundledListsOptions | undefined): FeatureDefinition[];
@@ -0,0 +1,56 @@
1
+ import { normalizeListColumn, validateBundledListsOptions, } from "@cosmicdrift/kumiko-framework/engine";
2
+ export function applyBundledListsTable(features, options) {
3
+ if (options === undefined)
4
+ return [...features];
5
+ validateBundledListsOptions(options);
6
+ return features.map((feature) => feature.adoptsBundledLists === true ? withListTableDefaults(feature, options) : feature);
7
+ }
8
+ function withListTableDefaults(feature, options) {
9
+ return {
10
+ ...feature,
11
+ screens: Object.fromEntries(Object.entries(feature.screens).map(([id, screen]) => [
12
+ id,
13
+ withScreenTableDefaults(screen, options),
14
+ ])),
15
+ };
16
+ }
17
+ function withScreenTableDefaults(screen, options) {
18
+ switch (screen.type) {
19
+ case "entityList":
20
+ case "projectionList": {
21
+ const table = { ...options.table, ...screen.table };
22
+ return { ...screen, table, columns: pinFirstColumn(screen.columns, table, options) };
23
+ }
24
+ case "projectionDetail":
25
+ case "entityEdit":
26
+ return {
27
+ ...screen,
28
+ layout: {
29
+ ...screen.layout,
30
+ sections: screen.layout.sections.map((section) => withSectionTableDefaults(section, options)),
31
+ },
32
+ };
33
+ default:
34
+ return screen;
35
+ }
36
+ }
37
+ function withSectionTableDefaults(section, options) {
38
+ if (section.kind !== "relatedList")
39
+ return section;
40
+ // export is only valid on entityList and projectionList
41
+ const { export: _export, ...defaultsWithoutExport } = options.table;
42
+ const table = { ...defaultsWithoutExport, ...section.table };
43
+ return { ...section, table, columns: pinFirstColumn(section.columns, table, options) };
44
+ }
45
+ function pinFirstColumn(columns, table, options) {
46
+ if (options.pinFirstColumn !== true || table.pinning !== true)
47
+ return columns;
48
+ const normalized = columns.map(normalizeListColumn);
49
+ if (normalized.some((column) => column.pinned !== undefined))
50
+ return columns;
51
+ // A hidden column only appears via the column picker; pinning it would surprise the user.
52
+ const firstVisible = normalized.findIndex((column) => column.hidden !== true);
53
+ if (firstVisible === -1)
54
+ return columns;
55
+ return columns.map((column, index) => index === firstVisible ? { ...normalizeListColumn(column), pinned: "left" } : column);
56
+ }
@@ -0,0 +1,16 @@
1
+ import type { AccountLockoutOptions, AccountUnlockOptions, AuthEmailPasswordOptions, EmailVerificationOptions, InviteOptions, PasswordResetOptions, SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
+ /** Shape of any run{Prod,Dev}App auth block that can carry a
3
+ * passwordReset/emailVerification config, once resolveAuthMail has resolved
4
+ * the wrapper's optional hmacSecret into the feature API's required one —
5
+ * a structural-typed lookup on the auth-only subset is enough. Lets
6
+ * buildComposeAuthOptions be called with both RunProd- and
7
+ * RunDev-AuthOptions without building the helper twice. */
8
+ export type AuthOptionsCarrier = {
9
+ readonly passwordReset?: PasswordResetOptions;
10
+ readonly emailVerification?: EmailVerificationOptions;
11
+ readonly signup?: SignupOptions;
12
+ readonly invite?: InviteOptions;
13
+ readonly accountUnlock?: AccountUnlockOptions;
14
+ readonly accountLockout?: AccountLockoutOptions;
15
+ };
16
+ export declare function buildComposeAuthOptions(auth: AuthOptionsCarrier | undefined): AuthEmailPasswordOptions | undefined;
@@ -0,0 +1,49 @@
1
+ // The magic-link mail fields shared by all five flows. Conditional spreads omit
2
+ // undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
3
+ // trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
4
+ // reset/verify layer hmacSecret (+ mode) on top.
5
+ function pickMailFields(src) {
6
+ return {
7
+ appUrl: src.appUrl,
8
+ ...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
9
+ ...(src.appName !== undefined && { appName: src.appName }),
10
+ ...(src.locale !== undefined && { locale: src.locale }),
11
+ };
12
+ }
13
+ export function buildComposeAuthOptions(auth) {
14
+ if (!auth)
15
+ return undefined;
16
+ const opts = {};
17
+ if (auth.passwordReset) {
18
+ opts.passwordReset = {
19
+ hmacSecret: auth.passwordReset.hmacSecret,
20
+ ...pickMailFields(auth.passwordReset),
21
+ };
22
+ }
23
+ if (auth.emailVerification) {
24
+ opts.emailVerification = {
25
+ hmacSecret: auth.emailVerification.hmacSecret,
26
+ ...pickMailFields(auth.emailVerification),
27
+ ...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
28
+ };
29
+ }
30
+ if (auth.signup) {
31
+ opts.signup = pickMailFields(auth.signup);
32
+ }
33
+ if (auth.invite) {
34
+ opts.invite = pickMailFields(auth.invite);
35
+ }
36
+ if (auth.accountUnlock) {
37
+ opts.accountUnlock = {
38
+ hmacSecret: auth.accountUnlock.hmacSecret,
39
+ ...pickMailFields(auth.accountUnlock),
40
+ };
41
+ }
42
+ if (auth.accountLockout) {
43
+ opts.accountLockout = auth.accountLockout;
44
+ }
45
+ return hasAnyAuthFlow(opts) ? opts : undefined;
46
+ }
47
+ function hasAnyAuthFlow(opts) {
48
+ return Object.values(opts).some(Boolean);
49
+ }
@@ -1,5 +1,6 @@
1
- import { type AccountLockoutOptions, type AccountUnlockOptions, type AuthEmailPasswordOptions, type EmailVerificationOptions, type InviteOptions, type PasswordResetOptions, type SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
- import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { type AuthEmailPasswordOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
+ import { type BundledListsOptions, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
3
+ export { type AuthOptionsCarrier, buildComposeAuthOptions } from "./compose-auth-options.ts";
3
4
  export type ComposeFeaturesOptions = {
4
5
  /** When true, prepends config + user + tenant + auth-email-password
5
6
  * (+ auth-self-registration when authOptions.signup is set) before the
@@ -12,20 +13,7 @@ export type ComposeFeaturesOptions = {
12
13
  * auth-routes.ts but no handler dispatches). Goes hand-in-hand with the
13
14
  * passwordReset block in RunProdAppAuthOptions / RunDevAppAuthOptions. */
14
15
  readonly authOptions?: AuthEmailPasswordOptions;
16
+ /** One table block for every list screen and relatedList of the bundled features; screen-own keys win. */
17
+ readonly bundledLists?: BundledListsOptions;
15
18
  };
16
19
  export declare function composeFeatures(appFeatures: readonly FeatureDefinition[], options: ComposeFeaturesOptions): FeatureDefinition[];
17
- /** Shape of any run{Prod,Dev}App auth block that can carry a
18
- * passwordReset/emailVerification config, once resolveAuthMail has resolved
19
- * the wrapper's optional hmacSecret into the feature API's required one —
20
- * a structural-typed lookup on the auth-only subset is enough. Lets
21
- * buildComposeAuthOptions be called with both RunProd- and
22
- * RunDev-AuthOptions without building the helper twice. */
23
- export type AuthOptionsCarrier = {
24
- readonly passwordReset?: PasswordResetOptions;
25
- readonly emailVerification?: EmailVerificationOptions;
26
- readonly signup?: SignupOptions;
27
- readonly invite?: InviteOptions;
28
- readonly accountUnlock?: AccountUnlockOptions;
29
- readonly accountLockout?: AccountLockoutOptions;
30
- };
31
- export declare function buildComposeAuthOptions(auth: AuthOptionsCarrier | undefined): AuthEmailPasswordOptions | undefined;
@@ -21,9 +21,14 @@ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-featu
21
21
  import { collectAssignableAppRoles, createTenantFeature, } from "@cosmicdrift/kumiko-bundled-features/tenant";
22
22
  import { TENANT_LIFECYCLE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
23
23
  import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
24
- import { dedupeFeatures } from "@cosmicdrift/kumiko-framework/engine";
24
+ import { dedupeFeatures, } from "@cosmicdrift/kumiko-framework/engine";
25
25
  import { FILES_FEATURE } from "@cosmicdrift/kumiko-framework/files";
26
26
  import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
27
+ import { applyBundledListsTable } from "./bundled-lists-table.js";
28
+ export { buildComposeAuthOptions } from "./compose-auth-options.js";
29
+ // Output of a first composeFeatures pass comes back as app features when a caller composes twice (the app
30
+ // config already composed, boot composes again); those copies are dropped silently, only app-supplied ones warn.
31
+ const composedBundledInstances = new WeakSet();
27
32
  export function composeFeatures(appFeatures, options) {
28
33
  // ponytail: includeBundled:false skips the auth-mfa auto-wiring below —
29
34
  // an app composing its own foundation and mounting auth-mfa itself is
@@ -32,7 +37,7 @@ export function composeFeatures(appFeatures, options) {
32
37
  // bypasses MFA. Upgrade if this trips someone: warn here when appFeatures
33
38
  // contains AUTH_MFA_FEATURE but includeBundled is false.
34
39
  if (!options.includeBundled)
35
- return dedupeFeatures(appFeatures);
40
+ return applyBundledListsTable(dedupeFeatures(appFeatures), options.bundledLists);
36
41
  // Bundled foundation goes first so its instances carry the runDevApp /
37
42
  // runProdApp `authOptions` (passwordReset wiring etc.). App-features that
38
43
  // ALSO declare one of these names — e.g. the create-kumiko-app picker
@@ -72,6 +77,8 @@ export function composeFeatures(appFeatures, options) {
72
77
  // "on unless an operator flips it off at runtime" contract.
73
78
  ...(authOptions?.signup !== undefined ? [createAuthSelfRegistrationToggleFeature()] : []),
74
79
  ];
80
+ for (const f of bundled)
81
+ composedBundledInstances.add(f);
75
82
  const bundledNames = new Set(bundled.map((f) => f.name));
76
83
  const filteredApp = [];
77
84
  for (const f of dedupedAppFeatures) {
@@ -81,6 +88,8 @@ export function composeFeatures(appFeatures, options) {
81
88
  // authOptions.signup and only get it from there.
82
89
  if (f.name === AUTH_SELF_REGISTRATION_FEATURE)
83
90
  continue;
91
+ if (composedBundledInstances.has(f))
92
+ continue;
84
93
  // biome-ignore lint/suspicious/noConsole: boot-time UX warning
85
94
  console.warn(`[composeFeatures] "${f.name}" already auto-mounted via includeBundled — dropping the explicit copy from APP_FEATURES. Remove it from run-config.ts to silence this warning.`);
86
95
  continue;
@@ -92,12 +101,12 @@ export function composeFeatures(appFeatures, options) {
92
101
  const needsFilesTenantData = names.has(FILES_FEATURE) &&
93
102
  names.has(TENANT_LIFECYCLE_FEATURE) &&
94
103
  !names.has(FILES_TENANT_DATA_FEATURE);
95
- return [
104
+ return applyBundledListsTable([
96
105
  ...bundled,
97
106
  ...missingAuthMailStack(names),
98
107
  ...filteredApp,
99
108
  ...(needsFilesTenantData ? [createFilesTenantDataFeature()] : []),
100
- ];
109
+ ], options.bundledLists);
101
110
  }
102
111
  // auth-email-password declares its mails as stored templates, and the boot seed
103
112
  // fails without the template-resolver and a mail renderer. Mounting the missing
@@ -110,52 +119,3 @@ function missingAuthMailStack(mountedNames) {
110
119
  createRendererMailHtmlFeature(),
111
120
  ].filter((feature) => !mountedNames.has(feature.name));
112
121
  }
113
- // The magic-link mail fields shared by all five flows. Conditional spreads omit
114
- // undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
115
- // trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
116
- // reset/verify layer hmacSecret (+ mode) on top.
117
- function pickMailFields(src) {
118
- return {
119
- appUrl: src.appUrl,
120
- ...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
121
- ...(src.appName !== undefined && { appName: src.appName }),
122
- ...(src.locale !== undefined && { locale: src.locale }),
123
- };
124
- }
125
- export function buildComposeAuthOptions(auth) {
126
- if (!auth)
127
- return undefined;
128
- const opts = {};
129
- if (auth.passwordReset) {
130
- opts.passwordReset = {
131
- hmacSecret: auth.passwordReset.hmacSecret,
132
- ...pickMailFields(auth.passwordReset),
133
- };
134
- }
135
- if (auth.emailVerification) {
136
- opts.emailVerification = {
137
- hmacSecret: auth.emailVerification.hmacSecret,
138
- ...pickMailFields(auth.emailVerification),
139
- ...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
140
- };
141
- }
142
- if (auth.signup) {
143
- opts.signup = pickMailFields(auth.signup);
144
- }
145
- if (auth.invite) {
146
- opts.invite = pickMailFields(auth.invite);
147
- }
148
- if (auth.accountUnlock) {
149
- opts.accountUnlock = {
150
- hmacSecret: auth.accountUnlock.hmacSecret,
151
- ...pickMailFields(auth.accountUnlock),
152
- };
153
- }
154
- if (auth.accountLockout) {
155
- opts.accountLockout = auth.accountLockout;
156
- }
157
- return hasAnyAuthFlow(opts) ? opts : undefined;
158
- }
159
- function hasAnyAuthFlow(opts) {
160
- return Object.values(opts).some(Boolean);
161
- }
@@ -1,11 +1,13 @@
1
1
  import type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { Registry, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
4
5
  import type { MetricsHandle } from "@cosmicdrift/kumiko-framework/observability";
5
6
  import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
6
7
  import type { Redis } from "ioredis";
7
8
  export type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
8
9
  export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
10
+ export { makeFilesForTenant } from "./files-for-tenant.ts";
9
11
  /** Deps for the `wire` hook (runProdApp/createKumikoServer, after
10
12
  * buildServer has mounted `extraRoutes`) and
11
13
  * for `runWorkerApp.wireComponents`. Naming: `deps` not `ctx` because
@@ -18,11 +20,13 @@ export type SystemWireDeps = {
18
20
  /** Feature-registry — e.g. for plugin-lookups via
19
21
  * `registry.getExtensionUsages("subscriptionProvider")`. */
20
22
  readonly registry: Registry;
23
+ /** File context of a tenant's storage; undefined when no file storage is mounted. Required so a consumer cannot lose attachment reading by omission. */
24
+ readonly filesForTenant: (tenantId: string) => FileContext | undefined;
21
25
  /** The boot's master key keyring; absent when no `KUMIKO_SECRETS_MASTER_KEY_V<n>` is set. */
22
26
  readonly masterKeyProvider?: MasterKeyProvider;
23
27
  /** Cipher for `encrypted: true` config keys, same instance and DEK cache as the handlers use. */
24
28
  readonly configEncryption?: EnvelopeCipher;
25
- /** Same contract as `ctx.metricsFor`: unregistered metric names no-op. */
29
+ /** Same contract as `ctx.obs.metricsFor`: unregistered metric names no-op. */
26
30
  readonly metricsFor: (featureName: string) => MetricsHandle;
27
31
  /** Writes through the /api/*-command-dispatcher (same idempotency/job
28
32
  * hooks) as an auto-constructed SystemAdmin of the target tenant,
@@ -2,3 +2,4 @@
2
2
  // truth lives in framework/api next to buildServer's own `extraRoutes`
3
3
  // mount so the two never drift on how the SystemAdmin identity is built.
4
4
  export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
5
+ export { makeFilesForTenant } from "./files-for-tenant.js";
@@ -0,0 +1,3 @@
1
+ import { type AppContext } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
3
+ export declare function makeFilesForTenant(context: AppContext): (tenantId: string) => FileContext | undefined;
@@ -0,0 +1,10 @@
1
+ import { fileContextFromResolver } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
2
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ // Takes the AppContext buildServer returned: only that one carries `_fileProviderResolver`.
4
+ // Without a resolver, `context.files` is bound to the system tenant and reaches no other one.
5
+ export function makeFilesForTenant(context) {
6
+ const resolver = context._fileProviderResolver;
7
+ if (resolver)
8
+ return (tenantId) => fileContextFromResolver(resolver, tenantId);
9
+ return (tenantId) => (tenantId === SYSTEM_TENANT_ID ? context.files : undefined);
10
+ }
@@ -3,26 +3,20 @@
3
3
  // dir (dev) or hashed into dist/assets (prod), away from the source fonts.
4
4
  // data: URIs are out because apps run with `font-src 'self'`.
5
5
  import { dirname, join } from "node:path";
6
+ import { resolveInstalledSync } from "./resolve-installed-package.js";
6
7
  export const RENDERER_WEB_FONTS_URL_PREFIX = "/assets/kumiko/fonts/";
7
8
  /** dist-relative folder the prod build copies the fonts into. */
8
9
  export const RENDERER_WEB_FONTS_DIST_DIR = "assets/kumiko/fonts";
9
10
  /** Allowlist for servable font file names; also rules out path segments. */
10
11
  export const RENDERER_WEB_FONT_FILE_PATTERN = /^[a-z0-9-]+\.woff2$/;
11
- const hasBun = typeof Bun !== "undefined";
12
12
  /** Directory of the woff2 files next to renderer-web's styles.css, or
13
13
  * undefined when the package can't be resolved from any `searchFrom` dir
14
14
  * (or outside Bun, which does the package-exports resolution). */
15
15
  export function resolveRendererWebFontsDir(searchFrom) {
16
- if (!hasBun)
17
- return undefined;
18
16
  for (const from of searchFrom) {
19
- try {
20
- const stylesheet = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", from);
17
+ const stylesheet = resolveInstalledSync("@cosmicdrift/kumiko-renderer/web/styles.css", from);
18
+ if (stylesheet !== undefined)
21
19
  return join(dirname(stylesheet), "fonts");
22
- }
23
- catch {
24
- // try the next search root
25
- }
26
20
  }
27
21
  return undefined;
28
22
  }
@@ -0,0 +1,10 @@
1
+ export type BunResolver = {
2
+ readonly resolveSync: (id: string, from: string) => string;
3
+ };
4
+ export declare function packageNameOf(specifier: string): string;
5
+ /** Walks node_modules upwards like Node does; returns the real path so transitive deps
6
+ * of an isolated-linker install are found as siblings. */
7
+ export declare function findInstalledPackageDir(packageName: string, fromDir: string): string | undefined;
8
+ /** Bun.resolveSync auto-installs from the registry (since 1.4.4) when no node_modules tree
9
+ * exists, so only call it for packages that are already installed. */
10
+ export declare function resolveInstalledSync(specifier: string, fromDir: string, bun?: BunResolver | undefined): string | undefined;
@@ -0,0 +1,32 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ export function packageNameOf(specifier) {
4
+ const segments = specifier.split("/");
5
+ return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : (segments[0] ?? specifier);
6
+ }
7
+ /** Walks node_modules upwards like Node does; returns the real path so transitive deps
8
+ * of an isolated-linker install are found as siblings. */
9
+ export function findInstalledPackageDir(packageName, fromDir) {
10
+ for (let dir = resolve(fromDir);; dir = resolve(dir, "..")) {
11
+ const packageDir = join(dir, "node_modules", packageName);
12
+ if (existsSync(join(packageDir, "package.json")))
13
+ return realpathSync(packageDir);
14
+ if (resolve(dir, "..") === dir)
15
+ return undefined;
16
+ }
17
+ }
18
+ const globalBun = typeof Bun !== "undefined" ? Bun : undefined;
19
+ /** Bun.resolveSync auto-installs from the registry (since 1.4.4) when no node_modules tree
20
+ * exists, so only call it for packages that are already installed. */
21
+ export function resolveInstalledSync(specifier, fromDir, bun = globalBun) {
22
+ if (bun === undefined)
23
+ return undefined;
24
+ if (findInstalledPackageDir(packageNameOf(specifier), fromDir) === undefined)
25
+ return undefined;
26
+ try {
27
+ return bun.resolveSync(specifier, fromDir);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
@@ -1,14 +1,9 @@
1
- type BunResolver = {
2
- resolveSync: (id: string, from: string) => string;
3
- };
1
+ import { type BunResolver } from "./resolve-installed-package.ts";
4
2
  export type ResolveTailwindCliDeps = {
5
3
  readonly bun?: BunResolver;
6
4
  readonly cwd: string;
7
5
  };
8
6
  export declare function resolveTailwindCli(deps: ResolveTailwindCliDeps): string | undefined;
9
- /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
10
- * CSS file. Skip renderer-web fallback (or fail build) when peer deps
11
- * aren't resolvable from that directory — e.g. Bun cache copies of
12
- * `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
7
+ /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry CSS file, so
8
+ * tailwindcss must be installed above it (not just next to a Bun cache copy of renderer-web). */
13
9
  export declare function canResolveTailwindStylesheet(entryCss: string, deps: ResolveTailwindCliDeps): boolean;
14
- export {};
@@ -1,34 +1,17 @@
1
1
  // Resolves the locally installed @tailwindcss/cli binary to its absolute path.
2
2
  // We avoid `bunx` because it still requests the registry manifest for a locally
3
3
  // installed package and fails with `FailedToOpenSocket` when offline.
4
- //
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.
7
- import { dirname, resolve } from "node:path";
4
+ import { dirname, join } from "node:path";
5
+ import { findInstalledPackageDir } from "./resolve-installed-package.js";
8
6
  export function resolveTailwindCli(deps) {
9
7
  if (deps.bun === undefined)
10
8
  return undefined;
11
- try {
12
- const pkgJsonPath = deps.bun.resolveSync("@tailwindcss/cli/package.json", deps.cwd);
13
- // bin: { tailwindcss: "./dist/index.mjs" } → absoluter Pfad
14
- return resolve(pkgJsonPath, "..", "dist", "index.mjs");
15
- }
16
- catch {
17
- return undefined;
18
- }
9
+ const cliDir = findInstalledPackageDir("@tailwindcss/cli", deps.cwd);
10
+ return cliDir === undefined ? undefined : join(cliDir, "dist", "index.mjs");
19
11
  }
20
- /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
21
- * CSS file. Skip renderer-web fallback (or fail build) when peer deps
22
- * aren't resolvable from that directory — e.g. Bun cache copies of
23
- * `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
12
+ /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry CSS file, so
13
+ * tailwindcss must be installed above it (not just next to a Bun cache copy of renderer-web). */
24
14
  export function canResolveTailwindStylesheet(entryCss, deps) {
25
- if (deps.bun === undefined)
26
- return false;
27
- try {
28
- deps.bun.resolveSync("tailwindcss", dirname(entryCss));
29
- return true;
30
- }
31
- catch {
32
- return false;
33
- }
15
+ return (deps.bun !== undefined &&
16
+ findInstalledPackageDir("tailwindcss", dirname(entryCss)) !== undefined);
34
17
  }
@@ -16,6 +16,8 @@ export declare function addConfigAccessorFactory<T extends {
16
16
  export declare function loggerFromExtraContext(extraContext: Record<string, unknown>): Logger | undefined;
17
17
  /** Config key (`feature:config:name`) to value; wins over the key's env variable. */
18
18
  export type ConfigOverridesInput = Readonly<Record<string, string | number | boolean>>;
19
+ /** Env bridge first, validated app overrides on top: the one merge every boot function shares. */
20
+ export declare function buildConfigOverrides(registry: Registry, envSource: Record<string, string | undefined>, appOverrides: ConfigOverridesInput | undefined): import("@cosmicdrift/kumiko-bundled-features/config").AppConfigOverrides;
19
21
  type BootExtraContextOptions = {
20
22
  readonly db: DbConnection;
21
23
  readonly features: readonly FeatureDefinition[];
@@ -23,13 +23,11 @@ import { resolveBootCrypto } from "./boot/boot-crypto.js";
23
23
  export function addConfigAccessorFactory(resolved, registry) {
24
24
  if (!resolved.configResolver)
25
25
  return resolved;
26
- // Both keys, always overriding an app-supplied value: the factory must derive from the
27
- // effective resolver; the underscore alias stays until the next major.
26
+ // Always overrides an app-supplied value: the factory must derive from the effective resolver.
28
27
  const configAccessorFactory = createConfigAccessorFactory(registry, resolved.configResolver);
29
28
  return {
30
29
  ...resolved,
31
30
  configAccessorFactory,
32
- _configAccessorFactory: configAccessorFactory,
33
31
  };
34
32
  }
35
33
  // Framework-default providers for AppContext, using the same mechanism as
@@ -42,7 +40,7 @@ export function addConfigAccessorFactory(resolved, registry) {
42
40
  // boot/boot-crypto.ts (envHasMasterKek, resolveBootCrypto), shared with runDevApp.
43
41
  // A production misconfiguration (secrets mounted without a KEK) is caught by
44
42
  // secretsEnvSchema during boot; without the env-schema path, requireSecretsContext
45
- // throws with a wiring hint on the first ctx.secrets access. configResolver is only
43
+ // throws with a wiring hint on the first ctx.services.secrets access. configResolver is only
46
44
  // wired in auth mode. Exported and pure for unit tests; the caller merges it with
47
45
  // app values (the app wins).
48
46
  // Prod/dev parity for ctx.notify: without this `_notifyFactory` is only wired
@@ -116,16 +114,20 @@ function bootCryptoContext(crypto) {
116
114
  ...(crypto.masterKeyProvider && { masterKeyProvider: crypto.masterKeyProvider }),
117
115
  // Encrypt/decrypt partner for `encrypted: true` config keys. Wired
118
116
  // whenever a master key exists — NOT gated on the secrets feature,
119
- // config encryption must work without mounting ctx.secrets.
117
+ // config encryption must work without mounting ctx.services.secrets.
120
118
  ...(crypto.configCipher && { configEncryption: crypto.configCipher }),
121
119
  };
122
120
  }
123
- function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
121
+ /** Env bridge first, validated app overrides on top: the one merge every boot function shares. */
122
+ export function buildConfigOverrides(registry, envSource, appOverrides) {
124
123
  const envOverrides = buildEnvConfigOverrides(registry, envSource);
124
+ return appOverrides
125
+ ? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
126
+ : envOverrides;
127
+ }
128
+ function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
125
129
  return createConfigResolver({
126
- appOverrides: appOverrides
127
- ? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
128
- : envOverrides,
130
+ appOverrides: buildConfigOverrides(registry, envSource, appOverrides),
129
131
  ...(crypto.configCipher && { cipher: crypto.configCipher }),
130
132
  });
131
133
  }
@@ -1,4 +1,5 @@
1
1
  import { loadJwtSecretOrKeyring } from "@cosmicdrift/kumiko-framework/api";
2
+ import type { RowLevelSecurityMode } from "@cosmicdrift/kumiko-framework/db";
2
3
  import { type FeatureDefinition, type Registry } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
4
5
  import { resolveBootCrypto } from "./boot/boot-crypto.ts";
@@ -15,6 +16,7 @@ export type ProdBootConfig = {
15
16
  readonly envSource: Record<string, string | undefined>;
16
17
  readonly bootKms: BootOptionWiring["kms"];
17
18
  readonly databaseUrl: string;
19
+ readonly rowLevelSecurity: RowLevelSecurityMode;
18
20
  readonly redisUrl: string;
19
21
  readonly jwtSecret: string;
20
22
  readonly jwtSecretOrKeyring: ReturnType<typeof loadJwtSecretOrKeyring>;
@@ -2,6 +2,7 @@ import { EXT_SESSION_STORE } from "@cosmicdrift/kumiko-bundled-features/auth-fou
2
2
  import { SESSIONS_FEATURE } from "@cosmicdrift/kumiko-bundled-features/sessions";
3
3
  import { loadJwtSecretOrKeyring, TRUSTED_PROXY_HOPS_ENV } from "@cosmicdrift/kumiko-framework/api";
4
4
  import { createRegistry, } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { DEFAULT_ROW_LEVEL_SECURITY_MODE } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
5
6
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
6
7
  import { resolveBootCrypto } from "./boot/boot-crypto.js";
7
8
  import { composeAppFeatures, validateComposedFeatures, withEnvDefaults, } from "./boot/shared-boot-steps.js";
@@ -100,6 +101,7 @@ export async function resolveProdBootConfig(options, decryptedEnvSource, log, re
100
101
  envSource,
101
102
  bootKms,
102
103
  databaseUrl,
104
+ rowLevelSecurity: options.rowLevelSecurity ?? DEFAULT_ROW_LEVEL_SECURITY_MODE,
103
105
  redisUrl,
104
106
  jwtSecret,
105
107
  jwtSecretOrKeyring,
@@ -136,7 +138,7 @@ export function composeProdFeatures(options, config, log) {
136
138
  // auth-email-password via composeFeatures, using the same feature list as
137
139
  // runDevApp and the per-app schema generator. Build it before any connection
138
140
  // so boot mode can validate wiring without opening Postgres or Redis sockets.
139
- const features = composeAppFeatures(options.features, bootOptionWiring.features, effectiveAuth);
141
+ const features = composeAppFeatures(options.features, bootOptionWiring.features, effectiveAuth, options.bundledLists);
140
142
  const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
141
143
  validateComposedFeatures(features, {
142
144
  env: envSource,