@cosmicdrift/kumiko-dev-server 0.374.0 → 0.378.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 (34) hide show
  1. package/dist/compose-stacks.d.ts +6 -0
  2. package/dist/compose-stacks.js +13 -6
  3. package/dist/create-kumiko-server.d.ts +4 -2
  4. package/dist/create-kumiko-server.js +23 -25
  5. package/dist/managed-files/app-jobs.d.ts +4 -0
  6. package/dist/managed-files/app-jobs.js +68 -0
  7. package/dist/managed-files/check.d.ts +2 -0
  8. package/dist/managed-files/check.js +4 -2
  9. package/dist/managed-files/index.d.ts +2 -4
  10. package/dist/managed-files/index.js +2 -4
  11. package/dist/managed-files/registry.d.ts +7 -1
  12. package/dist/managed-files/registry.js +37 -5
  13. package/dist/managed-files/sync.js +10 -1
  14. package/dist/run-dev-app.d.ts +26 -10
  15. package/dist/run-dev-app.js +108 -37
  16. package/dist/scaffold-app-feature.js +1 -1
  17. package/dist/scaffold-app.d.ts +3 -0
  18. package/dist/scaffold-app.js +6 -7
  19. package/dist/scaffold-demo-tasks.js +1 -1
  20. package/dist/scaffold-deploy.d.ts +6 -0
  21. package/dist/scaffold-deploy.js +16 -1
  22. package/dist/testing.d.ts +17 -0
  23. package/dist/testing.js +46 -0
  24. package/package.json +19 -10
  25. package/src/changes.json +24 -0
  26. package/templates/managed/ci-public.yml.template +134 -0
  27. package/templates/managed/ci.yml.template +9 -6
  28. package/templates/managed/husky-pre-push.template +26 -12
  29. package/templates/managed/kumiko-schema.ts.template +16 -1
  30. package/templates/managed/release.yml.template +17 -1
  31. package/dist/managed-files/port-registry.d.ts +0 -17
  32. package/dist/managed-files/port-registry.js +0 -31
  33. package/dist/managed-files/static-app-config.d.ts +0 -16
  34. package/dist/managed-files/static-app-config.js +0 -99
@@ -23,15 +23,17 @@ import { createInMemoryLoginRateLimiter, parseTrustedProxyHopsEnv, TRUSTED_PROXY
23
23
  import { configureBlindIndexKey, configurePiiSubjectKms, } from "@cosmicdrift/kumiko-framework/crypto";
24
24
  import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
25
25
  import { collectWriteHandlerQns, createRegistry, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, findTierResolverUsage, requireTierResolverPlugin, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
26
+ import { parseEnv } from "@cosmicdrift/kumiko-framework/env";
26
27
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
27
28
  import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
29
+ import { assertStackBootRequirements, isSeedFnList, rejectRemovedEffectiveFeatures, resolveBootOptionWiring, withBootOptionEnvBlocks, } from "@cosmicdrift/kumiko-server-runtime";
28
30
  import { applyBootSeeds } from "@cosmicdrift/kumiko-server-runtime/boot/apply-boot-seeds";
29
31
  import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
30
32
  import { buildComposeAuthOptions, composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
31
33
  import { assertPiiBootInvariants } from "@cosmicdrift/kumiko-server-runtime/pii-boot-gate";
32
34
  import { assertSessionBootInvariants } from "@cosmicdrift/kumiko-server-runtime/session-boot-gate";
33
35
  import { createKumikoServer, logInfo, } from "./create-kumiko-server.js";
34
- import { captureEphemeralDevPiiKms, resolveDevPiiKms } from "./resolve-dev-pii-kms.js";
36
+ import { captureEphemeralDevPiiKms, resolveDevPiiKms, } from "./resolve-dev-pii-kms.js";
35
37
  import { renderWelcomeBanner } from "./welcome-banner.js";
36
38
  import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, requireEnv, resolveAuthMail, resolveUnsubscribeWiring, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
37
39
  export async function runDevApp(options) {
@@ -39,12 +41,52 @@ export async function runDevApp(options) {
39
41
  // source of truth — auch runProdApp und der per-app drizzle-Schema-
40
42
  // Generator nutzen denselben Helper, damit Migration und Runtime nie
41
43
  // auseinanderdriften können).
42
- const envSource = options.envSource ?? process.env;
44
+ const rawEnvSource = options.envSource ?? process.env;
45
+ const seedsConfig = options.seeds !== undefined && !isSeedFnList(options.seeds) ? options.seeds : undefined;
46
+ const stackSeeds = options.seeds === undefined
47
+ ? []
48
+ : isSeedFnList(options.seeds)
49
+ ? options.seeds
50
+ : (options.seeds.run ?? []);
51
+ if (options.envSchema) {
52
+ // Free dev seeds take the test stack; the env blocks only read the declarative part.
53
+ const { seeds: _devSeeds, kms: devKms, ...bootFields } = options;
54
+ const composed = withBootOptionEnvBlocks(options.envSchema, {
55
+ ...bootFields,
56
+ ...(devKms && { kms: devKms }),
57
+ ...(seedsConfig && { seeds: { ...seedsConfig, run: [] } }),
58
+ features: options.features,
59
+ });
60
+ parseEnv(composed.schema, rawEnvSource, { sources: composed.sources });
61
+ }
62
+ rejectRemovedEffectiveFeatures(options);
63
+ const wiring = await resolveBootOptionWiring({
64
+ features: options.features,
65
+ ...(options.mail && { mail: options.mail }),
66
+ ...(options.fileStorage && { fileStorage: options.fileStorage }),
67
+ ...(options.kmsMode !== undefined && { kmsMode: options.kmsMode }),
68
+ ...(options.billing && { billing: options.billing }),
69
+ ...(options.featureToggles && { featureToggles: true }),
70
+ ...(options.demoMode && { demoMode: options.demoMode }),
71
+ ...(seedsConfig && { seeds: { ...seedsConfig, run: [] } }),
72
+ ...(options.kms && { kms: options.kms }),
73
+ ...(options.blindIndexKey !== undefined && { blindIndexKey: options.blindIndexKey }),
74
+ ...(options.auth && { auth: options.auth }),
75
+ }, rawEnvSource);
76
+ const envSource = Object.keys(wiring.envDefaults).length > 0
77
+ ? { ...wiring.envDefaults, ...rawEnvSource }
78
+ : rawEnvSource;
79
+ assertStackBootRequirements({
80
+ features: options.features,
81
+ ...(options.featureToggles && { featureToggles: true }),
82
+ }, ["featureToggles"]);
43
83
  // auth.mail → normalisiert in die expliziten Flow-Felder (symmetrisch zu
44
84
  // runProdApp). hmacSecret = JWT_SECRET-env (fail-fast, symmetrisch zu
45
85
  // runProdApp). Ab hier IMMER effectiveAuth statt options.auth.
46
86
  const effectiveAuth = options.auth
47
- ? resolveAuthMail(options.auth, requireEnv("JWT_SECRET", envSource, "runDevApp"), envSource)
87
+ ? resolveAuthMail(options.auth, requireEnv("JWT_SECRET", envSource, "runDevApp"), envSource, {
88
+ mailConfigured: wiring.mailConfigured,
89
+ })
48
90
  : undefined;
49
91
  const composeAuthOptions = buildComposeAuthOptions(effectiveAuth);
50
92
  const { unsubscribeLinks, unsubscribeRoutes } = resolveUnsubscribeWiring({
@@ -54,10 +96,10 @@ export async function runDevApp(options) {
54
96
  envSource,
55
97
  warn: (message) => createFallbackLogger("runDevApp").warn(message),
56
98
  });
57
- const extraRoutes = unsubscribeRoutes.length === 0
99
+ const extraRoutes = unsubscribeRoutes.length === 0 && wiring.extraRoutes.length === 0
58
100
  ? options.extraRoutes
59
- : [...(options.extraRoutes ?? []), ...unsubscribeRoutes];
60
- const features = composeFeatures(options.features, {
101
+ : [...(options.extraRoutes ?? []), ...unsubscribeRoutes, ...wiring.extraRoutes];
102
+ const features = composeFeatures([...options.features, ...wiring.features], {
61
103
  includeBundled: !!effectiveAuth,
62
104
  ...(composeAuthOptions && { authOptions: composeAuthOptions }),
63
105
  });
@@ -65,13 +107,19 @@ export async function runDevApp(options) {
65
107
  // FILE_STORAGE_PROVIDER boot gate. Passed via the validateBoot env option
66
108
  // instead of mutating process.env, so nothing leaks into later runDevApp
67
109
  // calls in the same process.
68
- const baseBootEnv = options.validateBootOptions?.env ?? process.env;
110
+ const baseBootEnv = {
111
+ ...wiring.envDefaults,
112
+ ...(options.validateBootOptions?.env ?? process.env),
113
+ };
114
+ const hasBootEnvDefaults = Object.keys(wiring.envDefaults).length > 0;
69
115
  const bootOptions = options.files !== undefined && baseBootEnv[FILE_STORAGE_PROVIDER_ENV] === undefined
70
116
  ? {
71
117
  ...options.validateBootOptions,
72
118
  env: { ...baseBootEnv, [FILE_STORAGE_PROVIDER_ENV]: FILE_STORAGE_PROVIDER_BOOT_SENTINEL },
73
119
  }
74
- : options.validateBootOptions;
120
+ : hasBootEnvDefaults
121
+ ? { ...options.validateBootOptions, env: baseBootEnv }
122
+ : options.validateBootOptions;
75
123
  // Boot-Validation als allererstes — vor dem Server. Dieselbe
76
124
  // Fehlerklasse (unqualifizierte nav-/handler-QNs, screen-access etc.),
77
125
  // die früher nur runProdApp fing und sonst erst den Prod-Pod im
@@ -87,34 +135,33 @@ export async function runDevApp(options) {
87
135
  warnIfNonUtcServerTimeZone();
88
136
  validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
89
137
  // Sprint-8a Tier-Composition auto-wire: scan features for a
90
- // tenantTierResolver-extension. If found AND user didn't supply own
91
- // effectiveFeatures, we wire a late-bound wrapper here and fill it
92
- // in onAfterSetup (where stack.db + stack.registry are available).
138
+ // tenantTierResolver-extension and wire a late-bound wrapper here; it is
139
+ // filled in onAfterSetup (where stack.db + stack.registry are available).
140
+ // The global operator toggles layer over the same wrapper.
93
141
  // App-Author sees nothing — `createTierEngineFeature(opts)` mounts +
94
142
  // framework auto-wires.
95
- const tierResolverUsage = options.effectiveFeatures ? undefined : findTierResolverUsage(features);
143
+ const tierResolverUsage = findTierResolverUsage(features);
96
144
  const tierResolverHolder = {
97
145
  resolver: undefined,
98
146
  };
99
- const finalEffectiveFeatures = options.effectiveFeatures ??
100
- (tierResolverUsage
101
- ? Object.assign((tenantId) => {
102
- // Defensive: Server starts AFTER onAfterSetup completes, so the
103
- // resolver is filled before any request comes in. Throwing here
104
- // means a programming error (boot order) rather than silent
105
- // "all-features-on" misbehavior.
106
- if (!tierResolverHolder.resolver) {
107
- throw new Error("tier-resolver: extension found but resolver not yet built — boot order issue?");
108
- }
109
- return tierResolverHolder.resolver(tenantId);
110
- }, {
111
- // Late-bind: der echte trialGate hängt erst nach plugin.build() am
112
- // holder.resolver. Delegieren (nicht kopieren), weil dieses
113
- // Wrapper-Closure vor build() konstruiert wird. Kein Trial → false.
114
- trialGate: (tenantId, featureName) => tierResolverHolder.resolver?.trialGate?.(tenantId, featureName) ??
115
- Promise.resolve(false),
116
- })
117
- : undefined);
147
+ const finalEffectiveFeatures = tierResolverUsage || wiring.featureToggles
148
+ ? Object.assign((tenantId) => {
149
+ // Defensive: Server starts AFTER onAfterSetup completes, so the
150
+ // resolver is filled before any request comes in. Throwing here
151
+ // means a programming error (boot order) rather than silent
152
+ // "all-features-on" misbehavior.
153
+ if (!tierResolverHolder.resolver) {
154
+ throw new Error("tier-resolver: extension found but resolver not yet built — boot order issue?");
155
+ }
156
+ return tierResolverHolder.resolver(tenantId);
157
+ }, {
158
+ // Late-bind: der echte trialGate hängt erst nach plugin.build() am
159
+ // holder.resolver. Delegieren (nicht kopieren), weil dieses
160
+ // Wrapper-Closure vor build() konstruiert wird. Kein Trial → false.
161
+ trialGate: (tenantId, featureName) => tierResolverHolder.resolver?.trialGate?.(tenantId, featureName) ??
162
+ Promise.resolve(false),
163
+ })
164
+ : undefined;
118
165
  // configResolver-default fürs config-feature — im auth-mode immer
119
166
  // hinzufügen, im no-auth-mode dem Caller überlassen. Factory-form
120
167
  // wird gewrap't damit der spread auf das aufgerufene Result greift,
@@ -129,10 +176,21 @@ export async function runDevApp(options) {
129
176
  // Subject-KMS + Blind-Index (symmetrisch zu runProdApp). Dev warnt bei
130
177
  // Klartext-PII statt zu failen; kms+lookupable ohne blindIndexKey bricht
131
178
  // auch hier (Lookups wären in jedem Modus kaputt).
132
- const piiKms = resolveDevPiiKms(options);
179
+ const piiKms = options.kmsMode !== undefined
180
+ ? {
181
+ kms: wiring.kms.kms,
182
+ blindIndexKey: wiring.kms.blindIndexKey,
183
+ generatedBlindIndexKey: false,
184
+ mode: wiring.kms.kms ? "app-kms" : "plaintext",
185
+ }
186
+ : resolveDevPiiKms(options);
133
187
  configurePiiSubjectKms(piiKms.kms);
134
188
  configureBlindIndexKey(piiKms.blindIndexKey);
135
189
  const releaseEphemeralPiiKms = captureEphemeralDevPiiKms(piiKms);
190
+ const releasePiiKms = async () => {
191
+ releaseEphemeralPiiKms();
192
+ await wiring.kms.close();
193
+ };
136
194
  assertPiiBootInvariants(features, {
137
195
  kms: piiKms.kms,
138
196
  blindIndexKey: piiKms.blindIndexKey,
@@ -260,6 +318,7 @@ export async function runDevApp(options) {
260
318
  ...(extraRoutes !== undefined && { extraRoutes }),
261
319
  ...(options.wire !== undefined && { wire: options.wire }),
262
320
  ...(trustedProxyHops !== undefined && { trustedProxyHops }),
321
+ ...(options.demoMode && { wrapFetch: wiring.wrapFetch }),
263
322
  ...(finalEffectiveFeatures !== undefined && {
264
323
  effectiveFeatures: finalEffectiveFeatures,
265
324
  }),
@@ -342,6 +401,15 @@ export async function runDevApp(options) {
342
401
  cacheSync: stack.cacheSync,
343
402
  });
344
403
  }
404
+ if (wiring.featureToggles) {
405
+ const bound = await wiring.featureToggles.bind({
406
+ db: stack.db,
407
+ registry: stack.registry,
408
+ cacheSync: stack.cacheSync,
409
+ tierResolver: tierResolverHolder.resolver,
410
+ });
411
+ tierResolverHolder.resolver = bound.resolver;
412
+ }
345
413
  if (effectiveAuth && stack.registry.getExtensionUsages(EXT_SESSION_STORE).length > 0) {
346
414
  // Secure-by-default (#1372): resolve sessionStore provider; bind
347
415
  // password-change mass-revoke + MFA revokeAllOthers.
@@ -374,17 +442,20 @@ export async function runDevApp(options) {
374
442
  // Runs before user-supplied seed callbacks so those can read /
375
443
  // override the deploy-defaults. The helper indirection is what
376
444
  // config-seed-boot.integration.ts pins — keep it as a single call.
445
+ for (const seed of wiring.shortcutSeeds) {
446
+ await seed(stack.db);
447
+ }
377
448
  await applyBootSeeds({
378
449
  registry: stack.registry,
379
450
  db: stack.db,
380
451
  ...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
381
452
  });
382
- for (const seed of options.seeds ?? []) {
453
+ for (const seed of stackSeeds) {
383
454
  await seed(stack);
384
455
  }
385
456
  },
386
- }).catch((error) => {
387
- releaseEphemeralPiiKms();
457
+ }).catch(async (error) => {
458
+ await releasePiiKms().catch(() => { });
388
459
  throw error;
389
460
  });
390
461
  if (options.welcomeBanner) {
@@ -411,7 +482,7 @@ export async function runDevApp(options) {
411
482
  await handle.stop();
412
483
  }
413
484
  finally {
414
- releaseEphemeralPiiKms();
485
+ await releasePiiKms();
415
486
  }
416
487
  },
417
488
  };
@@ -427,7 +498,7 @@ export function mergeConfigResolverDefault(ctx, registry, envSource, cipher) {
427
498
  ...(cipher && { cipher }),
428
499
  }),
429
500
  };
430
- // ctx.config wird per-Request aus _configAccessorFactory geminted (siehe
501
+ // ctx.config wird per-Request aus configAccessorFactory geminted (siehe
431
502
  // addConfigAccessorFactory) — sonst bleibt ctx.config undefined und Handler
432
503
  // die es lesen (createFileProviderForTenant) werfen. Aus dem EFFEKTIVEN
433
504
  // Resolver (Caller-Override gewinnt) gebaut, symmetrisch zu runProdApp.
@@ -155,7 +155,7 @@ function renderWebIndex(name) {
155
155
  // createKumikoApp({ clientFeatures: [...] }) einhängen — das passiert
156
156
  // NICHT automatisch. Screens: eine Datei pro Screen in diesem Ordner.
157
157
 
158
- import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
158
+ import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer/web";
159
159
 
160
160
  export const ${camel}Client: ClientFeatureDefinition = {
161
161
  name: "${name}",
@@ -37,6 +37,9 @@ export type ScaffoldAppOptions = {
37
37
  * @cosmicdrift/kumiko-testing/scaffold. Without it the app gets the managed
38
38
  * bunfigs but no example tests and fewer standard scripts. */
39
39
  readonly testSetup?: ScaffoldTestSetupFactory;
40
+ /** Infra tag written to `package.json#kumiko.deploy.infraRef`. Unset: the managed
41
+ * workflows call infra at `main` and `kumiko check managed` warns. */
42
+ readonly infraRef?: string;
40
43
  };
41
44
  export type ScaffoldAppResult = {
42
45
  readonly destination: string;
@@ -51,7 +51,7 @@ export async function scaffoldApp(options) {
51
51
  mkdirSync(join(destination, "src"), { recursive: true });
52
52
  mkdirSync(join(destination, "kumiko"), { recursive: true });
53
53
  const files = [];
54
- write(join(destination, "package.json"), renderPackageJson(options.name, version, testSetup));
54
+ write(join(destination, "package.json"), renderPackageJson(options.name, version, testSetup, options.infraRef));
55
55
  files.push("package.json");
56
56
  write(join(destination, "kumiko.json"), renderKumikoManifest());
57
57
  files.push("kumiko.json");
@@ -137,18 +137,19 @@ function appScripts(testSetup) {
137
137
  prepare: PREPARE_SCRIPT,
138
138
  };
139
139
  }
140
- function renderPackageJson(name, version, testSetup) {
140
+ function renderPackageJson(name, version, testSetup, infraRef) {
141
141
  return `${JSON.stringify({
142
142
  name,
143
143
  version: "0.1.0",
144
144
  private: true,
145
145
  type: "module",
146
+ ...(infraRef === undefined ? {} : { kumiko: { deploy: { infraRef } } }),
146
147
  scripts: appScripts(testSetup),
147
148
  dependencies: {
148
149
  "@cosmicdrift/kumiko-bundled-features": version,
149
150
  "@cosmicdrift/kumiko-dev-server": version,
150
151
  "@cosmicdrift/kumiko-framework": version,
151
- "@cosmicdrift/kumiko-renderer-web": version,
152
+ "@cosmicdrift/kumiko-renderer": version,
152
153
  "@cosmicdrift/kumiko-server-runtime": version,
153
154
  react: "^19.2.6",
154
155
  "react-dom": "^19.2.6",
@@ -461,7 +462,7 @@ function renderClient(appName) {
461
462
  },
462
463
  { specifier: "./features/tasks/web", names: ["tasksClient"] },
463
464
  {
464
- specifier: "@cosmicdrift/kumiko-renderer-web",
465
+ specifier: "@cosmicdrift/kumiko-renderer/web",
465
466
  names: ["type AppSchema", "createKumikoApp", "DefaultAppShell"],
466
467
  },
467
468
  { specifier: "react", names: ["ReactNode"], typeOnly: true },
@@ -659,7 +660,7 @@ For full docs see https://docs.kumiko.rocks.
659
660
  }
660
661
  function renderStylesCss() {
661
662
  return [
662
- '@import "@cosmicdrift/kumiko-renderer-web/styles.css";',
663
+ '@import "@cosmicdrift/kumiko-renderer/web/styles.css";',
663
664
  "",
664
665
  '@source "./**/*.{ts,tsx}";',
665
666
  "",
@@ -693,7 +694,6 @@ function renderBinKumiko() {
693
694
  'import { envSchema } from "./env-schema";',
694
695
  "",
695
696
  "const [, , cmd, ...rest] = Bun.argv;",
696
- "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
697
697
  "const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };",
698
698
  "",
699
699
  'if (cmd === "schema") {',
@@ -708,7 +708,6 @@ function renderBinKumiko() {
708
708
  " process.exit(await runConsumerCli(rest, out));",
709
709
  "}",
710
710
  "",
711
- "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
712
711
  "console.error(",
713
712
  ` \`\\n Unknown: kumiko \${cmd ?? ""} — the standalone bundle only runs 'kumiko schema' and 'kumiko consumer'.\\n\`,`,
714
713
  ");",
@@ -195,7 +195,7 @@ export const tasksTranslations: TranslationsByLocale = {
195
195
  `;
196
196
  }
197
197
  export function renderDemoTasksWebIndex() {
198
- return `import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
198
+ return `import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer/web";
199
199
  import { tasksTranslations } from "../i18n";
200
200
 
201
201
  export const tasksClient: ClientFeatureDefinition = {
@@ -88,9 +88,15 @@ export declare function scaffoldDeploy(options: ScaffoldDeployOptions): Scaffold
88
88
  /** Read-only: compares the rendered deploy files against what's on disk
89
89
  * without writing anything. */
90
90
  export declare function checkDeployDrift(options: RenderDeployFilesOptions): CheckDeployDriftResult;
91
+ export declare const DEPLOY_VISIBILITIES: readonly ["private", "public"];
92
+ export type DeployVisibility = (typeof DEPLOY_VISIBILITIES)[number];
91
93
  export type KumikoReleaseTarget = {
92
94
  readonly namespace: string;
93
95
  readonly deployment: string;
96
+ readonly visibility: DeployVisibility;
97
+ /** Unset until the app pins an infra tag; templates render `main` then. */
98
+ readonly infraRef: string | undefined;
99
+ readonly docStatusGuard: boolean;
94
100
  };
95
101
  /** Release target from `package.json#kumiko.deploy`; throws on an invalid deploy config. */
96
102
  export declare function readReleaseTarget(sourceDir: string, appName: string): KumikoReleaseTarget;
@@ -147,6 +147,9 @@ const DB_IDENTIFIER_RE = /^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,62}$/;
147
147
  // Kubernetes object names (RFC 1123 label); they are interpolated into the
148
148
  // generated release workflow, so nothing outside this charset may pass.
149
149
  const K8S_NAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
150
+ // The ref lands in `uses: org/infra/...@<ref>` lines of generated workflows.
151
+ const INFRA_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$/;
152
+ export const DEPLOY_VISIBILITIES = ["private", "public"];
150
153
  const kumikoDeployConfigSchema = z
151
154
  .object({
152
155
  dbUser: z.string().regex(DB_IDENTIFIER_RE).optional(),
@@ -155,6 +158,12 @@ const kumikoDeployConfigSchema = z
155
158
  namespace: z.string().regex(K8S_NAME_RE).optional(),
156
159
  /** Name of the prod Deployment; default = appName. */
157
160
  deployment: z.string().regex(K8S_NAME_RE).optional(),
161
+ /** Tag or branch of cosmicdriftgamestudio/infra that ci.yml and release.yml pin their reusable workflows to. */
162
+ infraRef: z.string().regex(INFRA_REF_RE).optional(),
163
+ /** A public repo cannot call the private infra workflows; default "private". */
164
+ visibility: z.enum(DEPLOY_VISIBILITIES).optional(),
165
+ /** `false` turns the doc-status guard off in ci.yml; default on. */
166
+ docStatusGuard: z.boolean().optional(),
158
167
  // "directory" is the only behaviour left; the key stays valid so existing
159
168
  // package.json files keep scaffolding.
160
169
  stackNetwork: z
@@ -201,7 +210,13 @@ export function readReleaseTarget(sourceDir, appName) {
201
210
  throw new Error(`scaffoldDeploy: "${value}" cannot serve as the release ${field} (must match ${K8S_NAME_RE}); set package.json#kumiko.deploy.${field}`);
202
211
  }
203
212
  }
204
- return { namespace, deployment };
213
+ return {
214
+ namespace,
215
+ deployment,
216
+ visibility: config.visibility ?? "private",
217
+ infraRef: config.infraRef,
218
+ docStatusGuard: config.docStatusGuard ?? true,
219
+ };
205
220
  }
206
221
  function resolveDbIdentifier(configured, appName, field, role) {
207
222
  const effective = configured ?? appName;
@@ -0,0 +1,17 @@
1
+ import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
2
+ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { type InProcessSeededTenant, type PersistedTenant, type SeedTenantOptions, type SeedWriter } from "@cosmicdrift/kumiko-testing";
4
+ import { type E2eSeedRoutesOptions } from "@cosmicdrift/kumiko-testing/e2e/seed-route";
5
+ import { type TestStack } from "@cosmicdrift/kumiko-testing/stack";
6
+ import { type SetupTestStackFromFeaturesOptions } from "./setup-test-stack-from-features.ts";
7
+ export type SetupAppTestStackOptions = SetupTestStackFromFeaturesOptions & {
8
+ readonly registryTables?: boolean;
9
+ };
10
+ export declare function setupAppTestStack(features: readonly FeatureDefinition[], options?: SetupAppTestStackOptions): Promise<TestStack>;
11
+ export declare function seedTenant(stack: TestStack, options?: SeedTenantOptions): Promise<InProcessSeededTenant>;
12
+ export declare function persistTenantRows(write: SeedWriter, options?: {
13
+ readonly name?: string;
14
+ readonly users?: number;
15
+ readonly admin?: SeedTenantOptions["admin"];
16
+ }): Promise<PersistedTenant>;
17
+ export declare function createE2eSeedRoutes(options?: E2eSeedRoutesOptions): readonly ExtraRouteDefinition[];
@@ -0,0 +1,46 @@
1
+ // Ready-made test functions that kumiko-testing cannot hold itself: it has no bundled-features or
2
+ // dev-server dependency, so those pieces are injected here and apps import the bound result.
3
+ import { hashPassword } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
4
+ import { createMailTransportInMemoryFeature, getInbox, } from "@cosmicdrift/kumiko-bundled-features/mail-transport-inmemory";
5
+ import { TenantHandlers, TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
6
+ import { UserHandlers } from "@cosmicdrift/kumiko-bundled-features/user";
7
+ import { persistTenantRowsWith, seedTenantWith, } from "@cosmicdrift/kumiko-testing";
8
+ import { createE2eSeedRoutes as createE2eSeedRoutesWith, } from "@cosmicdrift/kumiko-testing/e2e/seed-route";
9
+ import { pushEntityProjectionTables } from "@cosmicdrift/kumiko-testing/stack";
10
+ import { setupTestStackFromFeatures, } from "./setup-test-stack-from-features.js";
11
+ export async function setupAppTestStack(features, options = {}) {
12
+ const { registryTables = true, includeBundled = true, ...stackOptions } = options;
13
+ const stack = await setupTestStackFromFeatures(features, {
14
+ ...stackOptions,
15
+ includeBundled,
16
+ entityTables: registryTables,
17
+ });
18
+ if (!registryTables)
19
+ return stack;
20
+ try {
21
+ await pushEntityProjectionTables(stack, stack.registry);
22
+ }
23
+ catch (error) {
24
+ await stack.cleanup();
25
+ throw error;
26
+ }
27
+ return stack;
28
+ }
29
+ const seedBundledApi = { hashPassword, UserHandlers, TenantHandlers };
30
+ const seedRouteBundledApi = {
31
+ ...seedBundledApi,
32
+ tenantMeQuery: TenantQueries.me,
33
+ mailInbox: {
34
+ featureName: createMailTransportInMemoryFeature().name,
35
+ read: getInbox,
36
+ },
37
+ };
38
+ export function seedTenant(stack, options = {}) {
39
+ return seedTenantWith(stack, options, seedBundledApi);
40
+ }
41
+ export function persistTenantRows(write, options = {}) {
42
+ return persistTenantRowsWith(write, options, seedBundledApi);
43
+ }
44
+ export function createE2eSeedRoutes(options = {}) {
45
+ return createE2eSeedRoutesWith(options, seedRouteBundledApi);
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.374.0",
3
+ "version": "0.378.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": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -19,7 +19,8 @@
19
19
  "bunOnlyExports": [
20
20
  ".",
21
21
  "./managed-files",
22
- "./setup-test-stack-from-features"
22
+ "./setup-test-stack-from-features",
23
+ "./testing"
23
24
  ]
24
25
  },
25
26
  "exports": {
@@ -47,6 +48,10 @@
47
48
  "types": "./dist/setup-test-stack-from-features.d.ts",
48
49
  "default": "./dist/setup-test-stack-from-features.js"
49
50
  },
51
+ "./testing": {
52
+ "types": "./dist/testing.d.ts",
53
+ "default": "./dist/testing.js"
54
+ },
50
55
  "./schema-apply": {
51
56
  "types": "./dist/schema-apply.d.ts",
52
57
  "default": "./dist/schema-apply.js"
@@ -72,14 +77,14 @@
72
77
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
73
78
  },
74
79
  "dependencies": {
75
- "@cosmicdrift/kumiko-bundled-features": "0.374.0",
76
- "@cosmicdrift/kumiko-framework": "0.374.0",
77
- "@cosmicdrift/kumiko-headless": "0.374.0",
78
- "@cosmicdrift/kumiko-renderer-mail-html": "0.374.0",
79
- "@cosmicdrift/kumiko-renderer-web": "0.374.0",
80
- "@cosmicdrift/kumiko-server-runtime": "0.374.0",
81
- "@cosmicdrift/kumiko-testing": "0.374.0",
82
- "@cosmicdrift/kumiko-types": "0.374.0",
80
+ "@cosmicdrift/kumiko-bundled-features": "0.378.0",
81
+ "@cosmicdrift/kumiko-framework": "0.378.0",
82
+ "@cosmicdrift/kumiko-renderer": "0.378.0",
83
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.378.0",
84
+ "@cosmicdrift/kumiko-guards": "0.378.0",
85
+ "@cosmicdrift/kumiko-server-runtime": "0.378.0",
86
+ "@cosmicdrift/kumiko-testing": "0.378.0",
87
+ "@cosmicdrift/kumiko-types": "0.378.0",
83
88
  "bun-types": "^1.3.13",
84
89
  "ts-morph": "^28.0.0",
85
90
  "zod": "^4.4.3"
@@ -112,6 +117,10 @@
112
117
  "types": "./dist/setup-test-stack-from-features.d.ts",
113
118
  "default": "./dist/setup-test-stack-from-features.js"
114
119
  },
120
+ "./testing": {
121
+ "types": "./dist/testing.d.ts",
122
+ "default": "./dist/testing.js"
123
+ },
115
124
  "./schema-apply": {
116
125
  "types": "./dist/schema-apply.d.ts",
117
126
  "default": "./dist/schema-apply.js"
package/src/changes.json CHANGED
@@ -1,4 +1,28 @@
1
1
  [
2
+ {
3
+ "version": "0.378.0",
4
+ "type": "improvement",
5
+ "title": "composeRendererStack forwards templateResolver options",
6
+ "detail": "RendererStackOptions.templateResolver is passed to createTemplateResolverFeature, so an app can set tenantEditable."
7
+ },
8
+ {
9
+ "version": "0.376.0",
10
+ "type": "improvement",
11
+ "title": "composeSaasStack with stripe warns instead of requiring billing",
12
+ "detail": "A stripe stack without the billing option logs a boot warning instead of aborting, and composeSaasStack takes featureToggles to require that option."
13
+ },
14
+ {
15
+ "version": "0.376.0",
16
+ "type": "improvement",
17
+ "title": "Managed workflows take infraRef, visibility and app-* jobs",
18
+ "detail": "ci.yml and release.yml render the infra reusables as @<infraRef> from package.json#kumiko.deploy.infraRef. Without it sync-managed renders @main and kumiko check managed warns; this warning becomes an error in a later release. kumiko.deploy.visibility \"public\" renders a self-contained ci.yml and a release.yml that only prints the gh workflow run hint for the public rollout. Jobs named app-* survive a sync and are not drift. kumiko.deploy.docStatusGuard false renders doc-status-guard false."
19
+ },
20
+ {
21
+ "version": "0.376.0",
22
+ "type": "improvement",
23
+ "title": "./testing subpath with the bound test stack and seed helpers",
24
+ "detail": "setupAppTestStack, seedTenant, persistTenantRows and createE2eSeedRoutes, wired to the bundled user, tenant and auth features."
25
+ },
2
26
  {
3
27
  "version": "0.373.0",
4
28
  "type": "improvement",