@cosmicdrift/kumiko-dev-server 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.
@@ -59,7 +59,8 @@ const hasBun = typeof Bun !== "undefined";
59
59
  // reference the package at runtime (no native binding, plain HTTP client,
60
60
  // but a real runtime import). A production crash in money-horse,
61
61
  // "Cannot find package 'meilisearch'", occurred because it used to be in
62
- // BUILD_ONLY_EXTERNALS instead.
62
+ // BUILD_ONLY_EXTERNALS instead. Its version comes from the framework's
63
+ // peerDependencies range.
63
64
  const RUNTIME_EXTERNALS = [
64
65
  "@node-rs/argon2",
65
66
  "sharp",
@@ -296,7 +297,8 @@ async function resolveRuntimeDepsVersions(cwd, repoRoot, packages) {
296
297
  continue;
297
298
  const raw = await readFile(path, "utf-8");
298
299
  const parsed = parseJsonOrThrow(raw, path);
299
- Object.assign(allDeps, parsed.dependencies ?? {}, parsed.optionalDependencies ?? {});
300
+ // Peers first: a real dependency of the same source overrides a peer range.
301
+ Object.assign(allDeps, parsed.peerDependencies ?? {}, parsed.dependencies ?? {}, parsed.optionalDependencies ?? {});
300
302
  }
301
303
  // App package.json fills gaps only — pins extraRuntimeExternals without
302
304
  // overriding framework-native pins the app may also list.
@@ -2,7 +2,7 @@ import { resolveAnonymousAccessFromRegistry } from "@cosmicdrift/kumiko-bundled-
2
2
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
3
3
  import { createMetricsForFactory } from "@cosmicdrift/kumiko-framework/observability";
4
4
  import { startDevJobRunners } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
5
- import { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-server-runtime/extra-routes-deps";
5
+ import { makeDispatchSystemQuery, makeDispatchSystemWrite, makeFilesForTenant, } from "@cosmicdrift/kumiko-server-runtime/extra-routes-deps";
6
6
  import { pushEntityProjectionTables, setupTestStack, } from "@cosmicdrift/kumiko-testing/stack";
7
7
  import { logInfo } from "./dev-server-runtime.js";
8
8
  import { isPersistentDevDb } from "./is-persistent-dev-db.js";
@@ -39,6 +39,7 @@ function buildDevStackOptions(options, dbMode) {
39
39
  ...(options.extraRoutes !== undefined && { extraRoutes: options.extraRoutes }),
40
40
  ...(options.trustedProxyHops !== undefined && { trustedProxyHops: options.trustedProxyHops }),
41
41
  ...(options.errorDocs && { errorDocs: options.errorDocs }),
42
+ ...(options.rowLevelSecurity !== undefined && { rowLevelSecurity: options.rowLevelSecurity }),
42
43
  // jobs.consumerLane unset = enqueuer-only; startDevJobRunners is the sole
43
44
  // consumer/cron-scheduler per lane, so runOnBoot/cron jobs don't
44
45
  // double-fire.
@@ -48,7 +49,7 @@ function buildDevStackOptions(options, dbMode) {
48
49
  export async function bootDevStack(options, dbMode) {
49
50
  logInfo(`[kumiko-server] booting Kumiko stack${dbMode.persistentDb ? ` — persistent DB "${dbMode.devDbName}"` : " — ephemeral test DB"}…`);
50
51
  const stack = await setupTestStack(buildDevStackOptions(options, dbMode));
51
- await createEventsTable(stack.db);
52
+ await createEventsTable(stack.unscopedDb);
52
53
  await pushEntityProjectionTables(stack, stack.registry);
53
54
  // `extraRoutes` is already mounted by setupTestStack's buildServer();
54
55
  // `wire` is for co-running components that need the system-write
@@ -60,6 +61,7 @@ export async function bootDevStack(options, dbMode) {
60
61
  // so application code sees the same interface in development and production.
61
62
  redis: stack.redis.redis,
62
63
  registry: stack.registry,
64
+ filesForTenant: makeFilesForTenant(stack.context),
63
65
  metricsFor: createMetricsForFactory(stack.observability.meter),
64
66
  dispatchSystemWrite: makeDispatchSystemWrite(stack.dispatcher),
65
67
  dispatchSystemQuery: makeDispatchSystemQuery(stack.dispatcher),
@@ -85,13 +87,13 @@ export async function startDevBackgroundRunners(stack, jobQueueNamePrefix) {
85
87
  registry: stack.registry,
86
88
  db: stack.db,
87
89
  // The stack's own AppContext, not a `{ db, registry }` literal: these
88
- // runners consume the lanes, and a job reaching for ctx.files /
90
+ // runners consume the lanes, and a job reaching for ctx.services.files /
89
91
  // ctx.notify / ctx.config would otherwise die here while the very same
90
92
  // handler works on the request path.
91
93
  context: stack.context,
92
94
  redisUrl,
93
95
  // Same dispatcher the request path writes through — without this,
94
- // ctx.write/ctx.writeAs/ctx.queryAs inside a dev-run job throw on their
96
+ // ctx.dispatch.write/ctx.dispatch.writeAs/ctx.dispatch.queryAs inside a dev-run job throw on their
95
97
  // first call.
96
98
  dispatcher: stack.dispatcher,
97
99
  // Must match setupTestStack's jobs.queueNamePrefix so enqueuer and
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { existsSync, mkdtempSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
+ import { resolveInstalledSync } from "@cosmicdrift/kumiko-server-runtime/resolve-installed-package";
6
7
  import { canResolveTailwindStylesheet, resolveTailwindCli, } from "@cosmicdrift/kumiko-server-runtime/resolve-tailwind-cli";
7
8
  import { hasBun, logError, logInfo } from "./dev-server-runtime.js";
8
9
  // Duplicated in packages/testing/src/e2e/constants.ts: the Playwright config
@@ -44,18 +45,15 @@ export function resolveStylesheet(options) {
44
45
  // silently rather than reporting an error.
45
46
  return undefined;
46
47
  }
47
- try {
48
- const resolved = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", process.cwd());
49
- if (!canResolveTailwindStylesheet(resolved, { bun: Bun, cwd: process.cwd() })) {
50
- return undefined;
51
- }
52
- return resolved;
53
- }
54
- catch (err) {
48
+ const resolved = resolveInstalledSync("@cosmicdrift/kumiko-renderer/web/styles.css", process.cwd());
49
+ if (resolved === undefined) {
55
50
  logError("[kumiko-server] couldn't auto-resolve @cosmicdrift/kumiko-renderer/web/styles.css — " +
56
- "pass `stylesheet: <path>` or `stylesheet: false` explicitly.", err);
51
+ "pass `stylesheet: <path>` or `stylesheet: false` explicitly.");
57
52
  return undefined;
58
53
  }
54
+ return canResolveTailwindStylesheet(resolved, { bun: Bun, cwd: process.cwd() })
55
+ ? resolved
56
+ : undefined;
59
57
  }
60
58
  // The E2E template can only reach an app's own server entry through its env,
61
59
  // so the env has to count whenever the option is left undefined.
@@ -1,5 +1,6 @@
1
1
  /// <reference types="bun-types" preserve="true" />
2
2
  import type { AuthRoutesConfig, ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
3
+ import { type RowLevelSecurityMode } from "@cosmicdrift/kumiko-framework/db";
3
4
  import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
4
5
  import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
5
6
  import type { PageHeadResolver, PageHeadSystemQuery } from "@cosmicdrift/kumiko-renderer/headless/apex";
@@ -64,6 +65,8 @@ export type DevHostDispatch = (req: Request, deps: {
64
65
  readonly cspScriptHashes: readonly string[];
65
66
  }) => DevHostDispatchResult | Promise<DevHostDispatchResult>;
66
67
  export type CreateKumikoServerOptions = {
68
+ /** Row level security of the dev stack, as in prod. Default "on", so tenant-isolation bugs show up in development. */
69
+ readonly rowLevelSecurity?: RowLevelSecurityMode;
67
70
  /** Features whose entities, handlers, and screens get wired into the
68
71
  * dev stack. Pass every feature the app is supposed to run. */
69
72
  readonly features: readonly FeatureDefinition[];
@@ -141,7 +144,7 @@ export type CreateKumikoServerOptions = {
141
144
  readonly anonymousAccess?: TestStackOptions["anonymousAccess"];
142
145
  /** File-storage provider forwarded to `setupTestStack`. When set, the
143
146
  * stack mounts the upload routes (POST/GET/DELETE /api/files), exposes
144
- * `ctx.files`, and creates the `file_refs` table. Example:
147
+ * `ctx.services.files`, and creates the `file_refs` table. Example:
145
148
  * `{ storageProvider: createInMemoryFileProvider() }`. */
146
149
  readonly files?: TestStackOptions["files"];
147
150
  /** Feature-toggle resolver forwarded to `setupTestStack`. When set, the
@@ -14,6 +14,7 @@
14
14
  //
15
15
  // The companion prod entry will land at `@cosmicdrift/kumiko-framework/server`
16
16
  // with a different options shape (clientDist, auth config, db url).
17
+ import { assertNoActiveRowLevelSecurityScope, } from "@cosmicdrift/kumiko-framework/db";
17
18
  import { buildAppSchema } from "@cosmicdrift/kumiko-framework/engine";
18
19
  import { buildBunServeOptions, resolveDerivedMaxRequestBodySize, } from "@cosmicdrift/kumiko-server-runtime/bun-serve-options";
19
20
  import { stripNoRouteMatchHeader } from "@cosmicdrift/kumiko-server-runtime/try-hono-first";
@@ -33,6 +34,7 @@ import { hasBun, logInfo } from "./dev-server-runtime.js";
33
34
  function startDevHttpServer(port, handleFetch, stack) {
34
35
  if (!hasBun)
35
36
  return undefined;
37
+ assertNoActiveRowLevelSecurityScope("dev server");
36
38
  return Bun.serve(buildBunServeOptions(port, handleFetch, resolveDerivedMaxRequestBodySize(stack.registry), {
37
39
  upgradeFetch: async (req, env) => stripNoRouteMatchHeader(await stack.app.fetch(req, env)),
38
40
  }));
@@ -1,4 +1,4 @@
1
- import { type FeaturePattern, type ParseError } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { type FeaturePattern, type ParseError } from "@cosmicdrift/kumiko-feature-ast";
2
2
  export type AuthoringStyle = "canonical" | "legacy";
3
3
  export type CorpusWarning = {
4
4
  /** Repo-relative path to the file that triggered the warning. */
@@ -31,7 +31,7 @@
31
31
  // builder is permissive, and the Designer filters at read-time.
32
32
  import { existsSync, readdirSync, readFileSync } from "node:fs";
33
33
  import { dirname, join, relative, resolve } from "node:path";
34
- import { PATTERN_LIBRARY, parseFeatureFile, } from "@cosmicdrift/kumiko-framework/engine";
34
+ import { PATTERN_LIBRARY, parseFeatureFile, } from "@cosmicdrift/kumiko-feature-ast";
35
35
  import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
36
36
  // =============================================================================
37
37
  // Builder
@@ -13,6 +13,7 @@ function isDevServerManifest(value) {
13
13
  }
14
14
  // All @cosmicdrift packages release in lockstep (changesets fixed-group), so
15
15
  // the dev-server's own version is the framework version.
16
+ // Exact (no caret): `kumiko check managed` rejects ranges on @cosmicdrift pins.
16
17
  export function resolveFrameworkVersion(startDir = import.meta.dir) {
17
18
  let dir = startDir;
18
19
  while (true) {
@@ -21,7 +22,7 @@ export function resolveFrameworkVersion(startDir = import.meta.dir) {
21
22
  // A corrupt or foreign package.json on the way up must not abort the search.
22
23
  const manifest = parseJsonSafe(readFileSync(candidate, "utf-8"), undefined);
23
24
  if (isDevServerManifest(manifest))
24
- return `^${manifest.version}`;
25
+ return manifest.version;
25
26
  }
26
27
  const parent = dirname(dir);
27
28
  if (parent === dir)
@@ -92,6 +92,7 @@ function renderReleaseWorkflow(facts) {
92
92
  infraRef: infraRef ?? DEFAULT_INFRA_REF,
93
93
  }, { privateDeploy: visibility === "private", publicRollout: visibility === "public" });
94
94
  }
95
+ const COMPOSE_ONLY_DEPLOY_FILE = "migrate-step.sh";
95
96
  export function renderManagedTemplates(facts) {
96
97
  const deploy = renderDeployFiles({
97
98
  appName: facts.appName,
@@ -99,7 +100,10 @@ export function renderManagedTemplates(facts) {
99
100
  ensuredRegistryConfigFiles: [BUNFIG_FILES.unit],
100
101
  });
101
102
  return [
102
- ...deploy.files.map((file) => marked(`deploy/${file.output}`, file.content, "#", file.output.endsWith(".sh"))),
103
+ ...deploy.files
104
+ // Managed apps migrate through the migrate-db initContainer of the infra _app-deploy.yml; migrate-step.sh is compose-only.
105
+ .filter((file) => file.output !== COMPOSE_ONLY_DEPLOY_FILE)
106
+ .map((file) => marked(`deploy/${file.output}`, file.content, "#", file.output.endsWith(".sh"))),
103
107
  marked(".husky/pre-push", readTemplate("husky-pre-push.template"), "#", true),
104
108
  marked(".github/workflows/cache-cleanup.yml", readTemplate("cache-cleanup.yml.template"), "#"),
105
109
  marked(".github/workflows/release.yml", renderReleaseWorkflow(facts), "#"),
@@ -148,6 +148,7 @@ export async function runDevAfterSetup(stack, ctx) {
148
148
  admin: effectiveAuth?.admin,
149
149
  shortcutSeeds: wiring.shortcutSeeds,
150
150
  cipher: bootCrypto.configCipher,
151
+ processName: "runDevApp",
151
152
  });
152
153
  for (const seed of config.stackSeeds) {
153
154
  await seed(stack);
@@ -86,7 +86,7 @@ export async function resolveDevBootConfig(options, bootFailure) {
86
86
  warn: (message) => createFallbackLogger("runDevApp").warn(message),
87
87
  });
88
88
  const extraRoutes = collectDevExtraRoutes(options, unsubscribeRoutes, wiring);
89
- const features = composeAppFeatures(options.features, wiring.features, effectiveAuth);
89
+ const features = composeAppFeatures(options.features, wiring.features, effectiveAuth, options.bundledLists);
90
90
  return { stackSeeds, wiring, envSource, effectiveAuth, unsubscribeLinks, extraRoutes, features };
91
91
  }
92
92
  // An explicitly wired file provider (options.files) satisfies the
@@ -3,6 +3,7 @@ function pickDevClientOptions(options) {
3
3
  return {
4
4
  ...(options.clientEntry !== undefined && { clientEntry: options.clientEntry }),
5
5
  ...(options.errorDocs && { errorDocs: options.errorDocs }),
6
+ ...(options.rowLevelSecurity !== undefined && { rowLevelSecurity: options.rowLevelSecurity }),
6
7
  ...(options.clientEntries !== undefined && { clientEntries: options.clientEntries }),
7
8
  ...(options.hostDispatch !== undefined && { hostDispatch: options.hostDispatch }),
8
9
  ...(options.resolvePageHead !== undefined && { resolvePageHead: options.resolvePageHead }),
@@ -1,7 +1,8 @@
1
1
  import type { SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
2
2
  import type { PostAuthLandingResolver } from "@cosmicdrift/kumiko-framework/api";
3
3
  import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
4
- import { type FeatureDefinition, type Registry, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { RowLevelSecurityMode } from "@cosmicdrift/kumiko-framework/db";
5
+ import { type BundledListsOptions, type FeatureDefinition, type Registry, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
5
6
  import type { ComposedEnvSchema } from "@cosmicdrift/kumiko-framework/env";
6
7
  import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
7
8
  import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
@@ -11,7 +12,7 @@ import type { TestStack } from "@cosmicdrift/kumiko-testing/stack";
11
12
  import { type CreateKumikoServerOptions, type KumikoServerHandle } from "./create-kumiko-server.ts";
12
13
  export type { AccountLockoutSetup, AccountUnlockSetup, AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
13
14
  import type { AccountLockoutSetup, AccountUnlockSetup, AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
14
- import { type UnsubscribeOption } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
15
+ import { type ConfigOverridesInput, type UnsubscribeOption } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
15
16
  export type RunDevAppAuthOptions = {
16
17
  /** Admin user to seed at boot. Idempotent — re-runs in persistent-DB
17
18
  * mode reuse the existing user. */
@@ -78,6 +79,8 @@ export type RunDevAppOptions = {
78
79
  readonly features: readonly FeatureDefinition[];
79
80
  /** Opt-in boot-validator warnings — see ValidateBootOptions. */
80
81
  readonly validateBootOptions?: ValidateBootOptions;
82
+ /** One table block for every list of the bundled features, see ComposeFeaturesOptions.bundledLists. */
83
+ readonly bundledLists?: BundledListsOptions;
81
84
  /** Browser entry module, bundled to /client.js. Mutually exclusive with
82
85
  * `clientEntries`. */
83
86
  readonly clientEntry?: string;
@@ -124,7 +127,9 @@ export type RunDevAppOptions = {
124
127
  * no locale signal. Merged into AppContext before extraContext (app wins). */
125
128
  readonly defaultLocale?: string;
126
129
  readonly extraContext?: CreateKumikoServerOptions["extraContext"];
127
- /** MasterKeyProvider for the automatically wired `ctx.secrets`. Defaults to
130
+ /** App-level config values, same semantics and merge as RunProdAppOptions["appOverrides"]. */
131
+ readonly appOverrides?: ConfigOverridesInput;
132
+ /** MasterKeyProvider for the automatically wired `ctx.services.secrets`. Defaults to
128
133
  * `createEnvMasterKeyProvider`; override it for KMS backends. Only relevant
129
134
  * when the secrets feature is mounted. Symmetric with runProdApp. */
130
135
  readonly masterKey?: MasterKeyProvider;
@@ -149,7 +154,7 @@ export type RunDevAppOptions = {
149
154
  * role in `access.roles`. */
150
155
  readonly anonymousAccess?: CreateKumikoServerOptions["anonymousAccess"];
151
156
  /** File-Storage-Provider — aktiviert die Upload-Routes (/api/files) +
152
- * `ctx.files`. Demos: `{ storageProvider: createInMemoryFileProvider() }`. */
157
+ * `ctx.services.files`. Demos: `{ storageProvider: createInMemoryFileProvider() }`. */
153
158
  readonly files?: CreateKumikoServerOptions["files"];
154
159
  /** App-defined HTTP routes, such as /feed.xml and /sitemap.xml. They are
155
160
  * attached to the Hono app before the static-asset path. Symmetric with the
@@ -198,6 +203,8 @@ export type RunDevAppOptions = {
198
203
  readonly trustedProxyHops?: number;
199
204
  /** Links the app's own error reasons to its own docs (`docsUrl` in error responses); framework reasons keep the framework docs link. */
200
205
  readonly errorDocs?: ErrorDocsConfig;
206
+ /** Row level security of the dev stack, as in prod. Default "on", so tenant-isolation bugs show up in development. */
207
+ readonly rowLevelSecurity?: RowLevelSecurityMode;
201
208
  };
202
209
  export declare function runDevApp(options: RunDevAppOptions): Promise<KumikoServerHandle>;
203
- export declare function mergeConfigResolverDefault(ctx: CreateKumikoServerOptions["extraContext"], registry: Registry, envSource: Record<string, string | undefined>, cipher?: EnvelopeCipher): CreateKumikoServerOptions["extraContext"];
210
+ export declare function mergeConfigResolverDefault(ctx: CreateKumikoServerOptions["extraContext"], registry: Registry, envSource: Record<string, string | undefined>, cipher?: EnvelopeCipher, appOverrides?: ConfigOverridesInput): CreateKumikoServerOptions["extraContext"];
@@ -11,7 +11,7 @@
11
11
  // For maximum control, such as custom auth wiring, a different membership
12
12
  // query, or a custom LoginRateLimiter, use `createKumikoServer` directly from
13
13
  // @cosmicdrift/kumiko-dev-server.
14
- import { buildEnvConfigOverrides, createConfigResolver, } from "@cosmicdrift/kumiko-bundled-features/config";
14
+ import { createConfigResolver } from "@cosmicdrift/kumiko-bundled-features/config";
15
15
  import { parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, } from "@cosmicdrift/kumiko-framework/api";
16
16
  import { findTierResolverUsage, } from "@cosmicdrift/kumiko-framework/engine";
17
17
  import { createKumikoServer, } from "./create-kumiko-server.js";
@@ -19,7 +19,7 @@ import { createDevLateBindings, createLateBoundEffectiveFeatures, runDevAfterSet
19
19
  import { configureDevBootCrypto, resolveDevAuthRegistry, resolveDevBootConfig, validateDevBoot, } from "./run-dev-app-config.js";
20
20
  import { buildDevServerOptions } from "./run-dev-app-server-options.js";
21
21
  import { renderWelcomeBanner } from "./welcome-banner.js";
22
- import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
22
+ import { addConfigAccessorFactory, buildBootExtraContext, buildConfigOverrides, loggerFromExtraContext, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
23
23
  export async function runDevApp(options) {
24
24
  const bootFailure = { release: undefined };
25
25
  try {
@@ -105,7 +105,7 @@ function renderDevWelcomeBanner(options, config, handle) {
105
105
  function buildDevExtraContext(options, config, registry, bootCrypto) {
106
106
  const { features, envSource, unsubscribeLinks } = config;
107
107
  const cfgExtra = registry
108
- ? mergeConfigResolverDefault(options.extraContext, registry, envSource, bootCrypto.configCipher)
108
+ ? mergeConfigResolverDefault(options.extraContext, registry, envSource, bootCrypto.configCipher, options.appOverrides)
109
109
  : options.extraContext;
110
110
  return (deps) => {
111
111
  const base = typeof cfgExtra === "function" ? cfgExtra(deps) : (cfgExtra ?? {});
@@ -133,10 +133,10 @@ function buildDevExtraContext(options, config, registry, bootCrypto) {
133
133
  // pins that the dev configResolver-default carries the ENV→app-override bridge
134
134
  // and that a caller-supplied configResolver still overrides it. Not re-exported
135
135
  // from the package barrel (index.ts lists only runDevApp).
136
- export function mergeConfigResolverDefault(ctx, registry, envSource, cipher) {
136
+ export function mergeConfigResolverDefault(ctx, registry, envSource, cipher, appOverrides) {
137
137
  const defaults = {
138
138
  configResolver: createConfigResolver({
139
- appOverrides: buildEnvConfigOverrides(registry, envSource),
139
+ appOverrides: buildConfigOverrides(registry, envSource, appOverrides),
140
140
  ...(cipher && { cipher }),
141
141
  }),
142
142
  };
@@ -27,7 +27,8 @@ export type ScaffoldAppOptions = {
27
27
  * (the CLI's ctx.cwd) MUST pass it so the scaffold lands where the
28
28
  * command's output claims it does. */
29
29
  readonly cwd?: string;
30
- /** npm-version-pin for @cosmicdrift/* deps. Default "*" for latest. */
30
+ /** Exact version pin for @cosmicdrift/* deps. Default: the running framework version
31
+ * (resolveFrameworkVersion); scaffoldApp throws when that cannot be resolved. */
31
32
  readonly frameworkVersion?: string;
32
33
  /** Bundled-features to mount in run-config.ts. Default: secrets + auth-foundation + sessions
33
34
  * (the historical foundation). create-kumiko-app passes the picker output
@@ -21,7 +21,10 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
21
21
  import { dirname, join, resolve } from "node:path";
22
22
  import { APP_CONFIG_FILE } from "@cosmicdrift/kumiko-framework/config";
23
23
  import { collectTableMetas, generateMigration, writeSnapshotJson, } from "@cosmicdrift/kumiko-framework/db";
24
+ import { DEFAULT_ROW_LEVEL_SECURITY_MODE } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
25
+ import { escapeHtml } from "@cosmicdrift/kumiko-renderer/headless";
24
26
  import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
27
+ import { resolveFrameworkVersion } from "./framework-version.js";
25
28
  import { isKebabSegment } from "./kebab.js";
26
29
  import { CURRENT_INFRA_REF, PREPARE_SCRIPT, resolveManagedAppFacts, syncManagedFiles, } from "./managed-files/index.js";
27
30
  import { renderAgentsMd, renderClaudeMd, renderMountedFeatureList } from "./scaffold-agent-docs.js";
@@ -45,7 +48,10 @@ export async function scaffoldApp(options) {
45
48
  if (existsSync(destination)) {
46
49
  throw new Error(`scaffoldApp: ${destination} already exists — refusing to overwrite`);
47
50
  }
48
- const version = options.frameworkVersion ?? "*";
51
+ const version = options.frameworkVersion ?? resolveFrameworkVersion();
52
+ if (version === undefined) {
53
+ throw new Error("scaffoldApp: cannot resolve the framework version; pass frameworkVersion explicitly");
54
+ }
49
55
  const testSetup = options.testSetup?.({ appName: options.name, frameworkVersion: version });
50
56
  mkdirSync(join(destination, "bin"), { recursive: true });
51
57
  mkdirSync(join(destination, "src"), { recursive: true });
@@ -84,6 +90,9 @@ export async function scaffoldApp(options) {
84
90
  files.push("src/client.tsx");
85
91
  write(join(destination, "src", "styles.css"), renderStylesCss());
86
92
  files.push("src/styles.css");
93
+ mkdirSync(join(destination, "public"), { recursive: true });
94
+ write(join(destination, "public", "index.html"), renderIndexHtml(options.name));
95
+ files.push("public/index.html");
87
96
  write(join(destination, ".env.example"), renderEnvExample(options.name));
88
97
  files.push(".env.example");
89
98
  write(join(destination, "docker-compose.yml"), renderDockerCompose());
@@ -161,7 +170,7 @@ function renderPackageJson(name, version, testSetup, infraRef) {
161
170
  "@tailwindcss/cli": "^4.3.0",
162
171
  "@types/react": "^19.2.0",
163
172
  "@types/react-dom": "^19.2.0",
164
- "bun-types": "^1.4.0",
173
+ "bun-types": "^1.4.4",
165
174
  tailwindcss: "^4.3.0",
166
175
  typescript: "^6.0.3",
167
176
  ...testSetup?.devDependencies,
@@ -388,6 +397,7 @@ const kmsWiring =
388
397
  await runProdApp({
389
398
  features: APP_FEATURES,
390
399
  ...(appConfig.validateBootOptions && { validateBootOptions: appConfig.validateBootOptions }),
400
+ ...(appConfig.rowLevelSecurity && { rowLevelSecurity: appConfig.rowLevelSecurity }),
391
401
  envSchema,
392
402
  staticDir: "./dist",
393
403
  ...kmsWiring,
@@ -404,12 +414,9 @@ function renderServer(appName) {
404
414
  { specifier: "../kumiko.config", names: [], defaultName: "appConfig" },
405
415
  { specifier: "../src/seed", names: ["seedDemoTasks"] },
406
416
  ]);
407
- // Dev-only emailVerified skips the login verify gate for the seeded admin;
408
- // prod's bin/main.ts leaves it unset because the verify flow must run there.
409
417
  const authBlock = renderAdminAuthBlock(appName, [
410
418
  'password: "changeme",',
411
419
  'displayName: "Admin",',
412
- "emailVerified: true,",
413
420
  ]);
414
421
  return `// Dev bootstrap. \`bun --watch bin/server.ts\` (see package.json scripts.dev)
415
422
  // starts a full-featured dev server with auto-reload on code changes.
@@ -552,6 +559,8 @@ services:
552
559
  # Pinned to the project's own compose-file tag (663/1) — Alpine variant
553
560
  # (~90MB vs ~400MB) and a reproducible patch version, bump on PG18 minors.
554
561
  image: postgres:18.3-alpine
562
+ # Bun.SQL dials its whole pool eagerly; the default of 100 is too tight for app + tests.
563
+ command: postgres -c max_connections=300
555
564
  environment:
556
565
  POSTGRES_USER: postgres
557
566
  POSTGRES_PASSWORD: postgres
@@ -660,6 +669,23 @@ Files whose first line reads \`kumiko-managed\` (deploy/, .github/workflows/,
660
669
  For full docs see https://docs.kumiko.rocks.
661
670
  `;
662
671
  }
672
+ // No inline script: CSP is script-src 'self'.
673
+ function renderIndexHtml(appName) {
674
+ return `<!doctype html>
675
+ <html lang="en">
676
+ <head>
677
+ <meta charset="utf-8" />
678
+ <meta name="viewport" content="width=device-width,initial-scale=1,interactive-widget=resizes-content" />
679
+ <title>${escapeHtml(appName)}</title>
680
+ <link rel="stylesheet" href="/styles.css" />
681
+ </head>
682
+ <body>
683
+ <div id="root"></div>
684
+ <script type="module" src="/client.js"></script>
685
+ </body>
686
+ </html>
687
+ `;
688
+ }
663
689
  function renderStylesCss() {
664
690
  return [
665
691
  '@import "@cosmicdrift/kumiko-renderer/web/styles.css";',
@@ -688,21 +714,22 @@ function renderBinKumiko() {
688
714
  "// consumer status|restart <name>` to recover a dead event consumer without",
689
715
  "// ad-hoc SQL. kumiko-build bundles this file to dist-server/kumiko.js.",
690
716
  "",
691
- 'import { allAppFeatures } from "@cosmicdrift/kumiko-framework/config";',
692
717
  'import { runConsumerCli } from "@cosmicdrift/kumiko-framework/consumer-cli";',
693
718
  'import { kmsSlotsOf } from "@cosmicdrift/kumiko-framework/env";',
694
719
  'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
695
- 'import appConfig from "../kumiko.config";',
720
+ 'import { FEATURES } from "../kumiko/schema";',
696
721
  'import { envSchema } from "./env-schema";',
697
722
  "",
698
723
  "const [, , cmd, ...rest] = Bun.argv;",
699
724
  "const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };",
700
725
  "",
701
726
  'if (cmd === "schema") {',
702
- " const features = allAppFeatures(appConfig);",
703
727
  " const kmsSlots = kmsSlotsOf(envSchema.schema);",
704
728
  " process.exit(",
705
- " await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features, kmsSlots }),",
729
+ " await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, {",
730
+ " features: FEATURES,",
731
+ " kmsSlots,",
732
+ " }),",
706
733
  " );",
707
734
  "}",
708
735
  "",
@@ -747,6 +774,7 @@ async function writeInitMigration(destination, features) {
747
774
  const metas = collectTableMetas(composed);
748
775
  const result = generateMigration({
749
776
  metas,
777
+ rowLevelSecurity: DEFAULT_ROW_LEVEL_SECURITY_MODE,
750
778
  prevSnapshot: null,
751
779
  name: "init",
752
780
  sequenceNumber: 1,
@@ -12,7 +12,7 @@
12
12
  // scaffolding).
13
13
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
14
14
  import { join, resolve } from "node:path";
15
- import { renderFeatureFile, } from "@cosmicdrift/kumiko-framework/engine";
15
+ import { renderFeatureFile, } from "@cosmicdrift/kumiko-feature-ast";
16
16
  /**
17
17
  * Generate a starter feature workspace at `destination`. Throws when
18
18
  * the destination already exists — refuses to overwrite. The caller is
@@ -1,3 +1,4 @@
1
+ import { type RowLevelSecurityMode } from "@cosmicdrift/kumiko-framework/db";
1
2
  import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { type ComposeFeaturesOptions } from "@cosmicdrift/kumiko-server-runtime/compose-features";
3
4
  export type SchemaApplyOptions = ComposeFeaturesOptions & {
@@ -6,6 +7,8 @@ export type SchemaApplyOptions = ComposeFeaturesOptions & {
6
7
  readonly features: readonly FeatureDefinition[];
7
8
  /** Default INIT_CWD ?? process.cwd(); migrations live under <appCwd>/kumiko/migrations. */
8
9
  readonly appCwd?: string;
10
+ /** Pass appConfig.rowLevelSecurity: on a role the policies bind, migrations and rebuilds only see rows through an "on" connection. Default "on". */
11
+ readonly rowLevelSecurity?: RowLevelSecurityMode;
9
12
  };
10
13
  export declare function runSchemaApply(opts: SchemaApplyOptions): Promise<number>;
11
14
  export declare function runStandaloneSchemaCli(opts: SchemaApplyOptions): Promise<never>;
@@ -7,9 +7,10 @@
7
7
  // kumiko-build discovers the app bin via findRepoRoot() and bundles it.
8
8
  import { existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
- import { createDbConnection, runMigrationsFromDir } from "@cosmicdrift/kumiko-framework/db";
10
+ import { createDbConnection, runMigrationsFromDir, } from "@cosmicdrift/kumiko-framework/db";
11
11
  import { createRegistry } from "@cosmicdrift/kumiko-framework/engine";
12
12
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
13
+ import { DEFAULT_ROW_LEVEL_SECURITY_MODE } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
13
14
  import { queueRebuildsFromMarkers, reportPendingRebuildRun, runPendingRebuilds, } from "@cosmicdrift/kumiko-framework/migrations";
14
15
  import { createEventConsumerStateTable, createProjectionStateTable, } from "@cosmicdrift/kumiko-framework/pipeline";
15
16
  import { composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
@@ -25,7 +26,10 @@ export async function runSchemaApply(opts) {
25
26
  console.error(`\n ${migrationsDir} is missing — kumiko/migrations/ must be part of the image.\n`);
26
27
  return 1;
27
28
  }
28
- const { db, close } = createDbConnection({ url: dbUrl });
29
+ const { db, close } = createDbConnection({
30
+ url: dbUrl,
31
+ rowLevelSecurity: opts.rowLevelSecurity ?? DEFAULT_ROW_LEVEL_SECURITY_MODE,
32
+ });
29
33
  try {
30
34
  // Create infrastructure tables (event store + pipeline state) first; all are
31
35
  // tableExists-gated and idempotent: on an existing DB this is a no-op, while
@@ -5,6 +5,7 @@ export type TestStackPreset = "config" | "template-resolver";
5
5
  export type SetupTestStackFromFeaturesOptions = Omit<TestStackOptions, "features"> & {
6
6
  readonly includeBundled?: boolean;
7
7
  readonly authOptions?: ComposeFeaturesOptions["authOptions"];
8
+ readonly bundledLists?: ComposeFeaturesOptions["bundledLists"];
8
9
  readonly presets?: readonly TestStackPreset[];
9
10
  };
10
11
  export declare function mergeExtraContext(base: TestStackOptions["extraContext"], presets: readonly TestStackPreset[]): TestStackOptions["extraContext"];
@@ -36,8 +36,12 @@ export function mergeExtraContext(base, presets) {
36
36
  };
37
37
  }
38
38
  export async function setupTestStackFromFeatures(appFeatures, options = {}) {
39
- const { includeBundled = false, authOptions, presets = [], ...stackOptions } = options;
40
- const features = composeFeatures(appFeatures, { includeBundled, authOptions });
39
+ const { includeBundled = false, authOptions, bundledLists, presets = [], ...stackOptions } = options;
40
+ const features = composeFeatures(appFeatures, {
41
+ includeBundled,
42
+ authOptions,
43
+ ...(bundledLists && { bundledLists }),
44
+ });
41
45
  const extraContext = mergeExtraContext(stackOptions.extraContext, presets);
42
46
  // Same run-logger wiring as prod, so jobs that declare tenantVisibleFailure
43
47
  // write their failure rows in tests too.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.388.0",
3
+ "version": "0.390.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -77,18 +77,24 @@
77
77
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
78
78
  },
79
79
  "dependencies": {
80
- "@cosmicdrift/kumiko-bundled-features": "0.388.0",
81
- "@cosmicdrift/kumiko-framework": "0.388.0",
82
- "@cosmicdrift/kumiko-renderer": "0.388.0",
83
- "@cosmicdrift/kumiko-renderer-mail-html": "0.388.0",
84
- "@cosmicdrift/kumiko-guards": "0.388.0",
85
- "@cosmicdrift/kumiko-server-runtime": "0.388.0",
86
- "@cosmicdrift/kumiko-testing": "0.388.0",
87
- "@cosmicdrift/kumiko-types": "0.388.0",
88
- "bun-types": "^1.3.13",
80
+ "@cosmicdrift/kumiko-bundled-features": "0.390.0",
81
+ "@cosmicdrift/kumiko-feature-ast": "0.390.0",
82
+ "@cosmicdrift/kumiko-framework": "0.390.0",
83
+ "@cosmicdrift/kumiko-renderer": "0.390.0",
84
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.390.0",
85
+ "@cosmicdrift/kumiko-guards": "0.390.0",
86
+ "@cosmicdrift/kumiko-server-runtime": "0.390.0",
87
+ "@cosmicdrift/kumiko-testing": "0.390.0",
88
+ "@cosmicdrift/kumiko-types": "0.390.0",
89
89
  "ts-morph": "^28.0.0",
90
90
  "zod": "^4.4.3"
91
91
  },
92
+ "peerDependencies": {
93
+ "bun-types": "^1.4.4"
94
+ },
95
+ "devDependencies": {
96
+ "bun-types": "^1.4.4"
97
+ },
92
98
  "publishConfig": {
93
99
  "registry": "https://registry.npmjs.org",
94
100
  "access": "public",
package/src/changes.json CHANGED
@@ -1,4 +1,61 @@
1
1
  [
2
+ {
3
+ "version": "0.390.0",
4
+ "type": "fix",
5
+ "title": "The managed Dockerfile names the real k3s migration step",
6
+ "detail": "The comment above the migrations COPY in the managed Dockerfile pointed at a release.yml InitContainer that does not exist. It now names the migrate-db initContainer of the infra _app-deploy.yml, and migrate-step.sh for compose hosts. Only the comment changes; the next kumiko-upgrade shows it as a managed-file diff."
7
+ },
8
+ {
9
+ "version": "0.390.0",
10
+ "type": "fix",
11
+ "title": "Scaffolded bin/kumiko.ts passes FEATURES from kumiko/schema",
12
+ "detail": "The scaffolded standalone CLI handed allAppFeatures(appConfig) to runSchemaCli, a raw list without the bundled foundation. It now imports FEATURES from kumiko/schema, which the Schema CLI Features guard requires. Existing apps are not touched."
13
+ },
14
+ {
15
+ "version": "0.390.0",
16
+ "type": "breaking",
17
+ "title": "The dev server runs with row level security on",
18
+ "detail": "runDevApp and createKumikoServer default to rowLevelSecurity \"on\" and run the dev stack as kumiko_app. On boot against a persistent dev database, the stack creates the role and transfers ownership of the database and of the public schema's tables, views and sequences to kumiko_app. The database user in DATABASE_URL needs the right to create roles, which the superuser of a local Postgres has. Code that reads tenant tables through a raw connection without a scope sees no rows in development; the boot does not detect this. runSchemaApply takes rowLevelSecurity, and the managed kumiko/schema.ts exports ROW_LEVEL_SECURITY from appConfig; the managed file updates on the next sync.",
19
+ "migration": "Pass rowLevelSecurity: \"off\" to runDevApp while fixing code paths without tenant context; the dev database keeps kumiko_app as owner. A server entry written by an older scaffold passes appConfig.rowLevelSecurity to runProdApp by hand once the app turns row level security on. Manual migration, there is no codemod."
20
+ },
21
+ {
22
+ "version": "0.390.0",
23
+ "type": "improvement",
24
+ "title": "Scaffolded dev admin leaves emailVerified to the boot seed",
25
+ "detail": "The boot seed marks the operator-created admin and the seed shortcut accounts as email-verified, so the scaffolded `bin/server.ts` no longer sets `emailVerified: true` on its own. Self-registered users still go through the verify flow.",
26
+ "migration": "Optional. Existing apps can drop `emailVerified: true` from `auth.admin` in `bin/server.ts` and `bin/main.ts`; an explicit value still wins over the default."
27
+ },
28
+ {
29
+ "version": "0.389.0",
30
+ "type": "improvement",
31
+ "title": "Scaffolded apps pin Bun 1.4.4",
32
+ "detail": "The deploy Dockerfile template, the managed public CI workflow and the scaffolded bun-types range move from Bun 1.4.0 to 1.4.4, the minimum the Bun.SQL driver needs. The managed CI workflow updates on the next sync; the deploy Dockerfile is only written at scaffold time.",
33
+ "migration": "Raise BUN_VERSION in an existing deploy Dockerfile to 1.4.4 and bun-types to ^1.4.4 by hand."
34
+ },
35
+ {
36
+ "version": "0.389.0",
37
+ "type": "improvement",
38
+ "title": "Scaffolded docker-compose raises Postgres max_connections to 300",
39
+ "detail": "Bun.SQL dials its whole pool eagerly, so the default limit of 100 is too tight for an app plus its tests. Existing apps can add command postgres -c max_connections=300 to the postgres service."
40
+ },
41
+ {
42
+ "version": "0.389.0",
43
+ "type": "fix",
44
+ "title": "Fresh scaffolds build with bun run build",
45
+ "detail": "create-kumiko-app now writes public/index.html with the /styles.css and /client.js placeholders. Without it kumiko-build stopped with a missing-template error, so the scaffolded Dockerfile could not build either."
46
+ },
47
+ {
48
+ "version": "0.389.0",
49
+ "type": "fix",
50
+ "title": "Scaffolded apps get exact framework pins",
51
+ "detail": "resolveFrameworkVersion no longer prefixes ^ and scaffoldApp no longer falls back to \"*\", so a new app is green in kumiko check managed. Without a resolvable version and without an explicit frameworkVersion, scaffoldApp throws."
52
+ },
53
+ {
54
+ "version": "0.389.0",
55
+ "type": "improvement",
56
+ "title": "migrate-step.sh leaves the managed file set",
57
+ "detail": "Managed apps run migrations as the k3s InitContainer from release.yml, so deploy/migrate-step.sh is neither scaffolded nor synced for them. Delete it if present. kumiko-init-deploy keeps generating it for compose self-hosting."
58
+ },
2
59
  {
3
60
  "version": "0.388.0",
4
61
  "type": "improvement",
@@ -11,7 +11,7 @@
11
11
  #
12
12
  # Build context: repository root (the full source tree is only used in the build stage).
13
13
 
14
- ARG BUN_VERSION=1.4.0
14
+ ARG BUN_VERSION=1.4.4
15
15
  {{#hasPrivateGhPackages}}
16
16
  # Registry auth for @cosmicdriftgamestudio: CI passes the token as
17
17
  # NPM_AUTH_TOKEN via --build-arg. Multi-stage ARG scoping needs the
@@ -69,7 +69,8 @@ RUN bun install --production --frozen-lockfile
69
69
  # Client build (static assets) — served by runProdApp.staticDir.
70
70
  COPY --from=build --chown=app:app /app/dist ./dist
71
71
 
72
- # Checked-in SQL migrations for the pre-deploy `kumiko schema apply` step.
72
+ # Checked-in SQL migrations for the pre-deploy `kumiko schema apply` step
73
+ # (migrate-db initContainer of the infra _app-deploy.yml on k3s, or migrate-step.sh on compose hosts).
73
74
  # The shipped `kumiko-schema` bin reads `${INIT_CWD}/kumiko/migrations/*.sql`
74
75
  # (see ENV INIT_CWD=/app below). kumiko/schema.ts is NOT needed at runtime
75
76
  # (only generate reads it; apply is checksum-tracked SQL only).
@@ -31,7 +31,7 @@ jobs:
31
31
  fetch-depth: 0
32
32
  - uses: oven-sh/setup-bun@v2
33
33
  with:
34
- bun-version: "1.4.0"
34
+ bun-version: "1.4.4"
35
35
  - run: bun install --frozen-lockfile
36
36
  - run: bun run typecheck
37
37
  - run: bun run lint
@@ -81,7 +81,7 @@ jobs:
81
81
  - uses: actions/checkout@v7
82
82
  - uses: oven-sh/setup-bun@v2
83
83
  with:
84
- bun-version: "1.4.0"
84
+ bun-version: "1.4.4"
85
85
  - run: bun install --frozen-lockfile
86
86
  - name: Create test database
87
87
  env:
@@ -22,9 +22,13 @@ const HAS_AUTH =
22
22
 
23
23
  export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...CONFIGURED_FEATURES], {
24
24
  includeBundled: HAS_AUTH,
25
+ ...(appConfig.bundledLists && { bundledLists: appConfig.bundledLists }),
25
26
  });
26
27
 
27
28
  // `kumiko check` validates the boot with the options kumiko.config.ts declares.
28
29
  export const VALIDATE_BOOT_OPTIONS = appConfig.validateBootOptions;
29
30
 
31
+ // Decides whether `generate` emits row-level-security policies and `apply` runs as a policy-enforced connection.
32
+ export const ROW_LEVEL_SECURITY = appConfig.rowLevelSecurity;
33
+
30
34
  export const ENTITY_METAS: readonly EntityTableMeta[] = collectTableMetas(FEATURES);