@cosmicdrift/kumiko-framework 0.370.0 → 0.371.1

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.
@@ -184,7 +184,7 @@ describe("chat-channels recipe", () => {
184
184
  });
185
185
  expect(rows.map((r) => r["recipientAddress"])).toEqual(["ops"]);
186
186
  expect(JSON.stringify(rows)).not.toContain("XXXX");
187
- }, 20000);
187
+ });
188
188
 
189
189
  test("announce rejects a caller without the Admin role", async () => {
190
190
  const attemptsBefore = await selectMany(db(), deliveryAttemptsTable, {
@@ -135,7 +135,10 @@ describe("KUMIKO_DRY_RUN_ENV=boot", () => {
135
135
 
136
136
  test("exits 1 on an invalid value and never prints it", async () => {
137
137
  const endpoint = "http://s3.leak-check.example.com";
138
- const { stdout, stderr, exitCode } = await runBootDryRun({ S3_ENDPOINT: endpoint });
138
+ const { stdout, stderr, exitCode } = await runBootDryRun({
139
+ NODE_ENV: "production",
140
+ S3_ENDPOINT: endpoint,
141
+ });
139
142
 
140
143
  expect(exitCode).toBe(1);
141
144
  expect(stdout).toMatch(/S3_ENDPOINT\s+file-provider-s3-env\s+optional\s+invalid/);
@@ -206,11 +209,18 @@ describe("env composition", () => {
206
209
  expect(report).not.toContain("S3_BUCKET");
207
210
  });
208
211
 
209
- test("rejects a remote http S3 endpoint", () => {
212
+ test("rejects a remote http S3 endpoint in production", () => {
210
213
  const composed = composeAppEnv();
211
- expect(() =>
212
- parseEnv(composed.schema, { ...minimalValidValues, S3_ENDPOINT: "http://s3.example.com" }),
213
- ).toThrow(KumikoBootError);
214
+ const originalNodeEnv = process.env["NODE_ENV"];
215
+ process.env["NODE_ENV"] = "production";
216
+ try {
217
+ expect(() =>
218
+ parseEnv(composed.schema, { ...minimalValidValues, S3_ENDPOINT: "http://s3.example.com" }),
219
+ ).toThrow(KumikoBootError);
220
+ } finally {
221
+ if (originalNodeEnv === undefined) delete process.env["NODE_ENV"];
222
+ else process.env["NODE_ENV"] = originalNodeEnv;
223
+ }
214
224
  });
215
225
  });
216
226
  ```
@@ -237,7 +237,7 @@ describe("inbound-mail triage sample", () => {
237
237
  } finally {
238
238
  await entry.stop();
239
239
  }
240
- }, 30_000);
240
+ });
241
241
  });
242
242
  ```
243
243
 
package/dist/env/index.js CHANGED
@@ -76,6 +76,18 @@ export function classifyField(field) {
76
76
  for (let i = 0; i < 8; i++) {
77
77
  if (current instanceof z.ZodDefault)
78
78
  return "withDefault";
79
+ // `.required()` on an object wraps optional fields in nonoptional(); the
80
+ // wrapper makes the field required, whatever the optional beneath it says.
81
+ if (current instanceof z.ZodNonOptional) {
82
+ const beneath = zodDef(current)?.innerType;
83
+ if (beneath instanceof z.ZodOptional) {
84
+ const unwrapped = zodDef(beneath)?.innerType;
85
+ if (unwrapped === undefined)
86
+ return "required";
87
+ current = unwrapped;
88
+ continue;
89
+ }
90
+ }
79
91
  if (current instanceof z.ZodOptional || current instanceof z.ZodNullable) {
80
92
  return "optional";
81
93
  }
@@ -324,7 +336,10 @@ export function parseEnv(schema, env, options = {}) {
324
336
  const isMissing = issue.code === "invalid_type" && !(name in cleaned);
325
337
  const kind = isMissing ? "missing" : "invalid";
326
338
  const desc = field ? getFieldDescription(field) : undefined;
327
- const message = desc ? `${issue.message} — ${desc}` : issue.message;
339
+ const issueText = issue.code === "invalid_type" && issue.expected === "nonoptional"
340
+ ? "Invalid input: required"
341
+ : issue.message;
342
+ const message = desc ? `${issueText} — ${desc}` : issueText;
328
343
  const suggestion = field ? buildPulumiSuggestion(name, field, options.pulumiPrefix) : undefined;
329
344
  const source = options.sources?.[name];
330
345
  return {
@@ -20,12 +20,21 @@ import { join, resolve as resolvePath } from "node:path";
20
20
  import { configureBlindIndexKey, configurePiiSubjectKms, resolveKmsWiringAsync, } from "./crypto/index.js";
21
21
  import { assertUniqueTableNames, assertValidMigrationName, baselineMigrations, createDbConnection, diffReplayAgainstSnapshot, fetchAppliedMigrations, findCommentedDropTables, generateMigration, isRetiredFrameworkTable, loadMigrationsFromDir, loadSnapshotJson, readMigrationSqlTexts, rebuildTablesFromDiff, replayMigrationsDir, runMigrationsFromDir, tableExists, writeRebuildMarker, writeSnapshotJson, } from "./db/index.js";
22
22
  import { validateBoot } from "./engine/boot-validator.js";
23
+ import { FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, } from "./engine/extension-names.js";
23
24
  import { createRegistry } from "./engine/registry.js";
24
25
  import { createEventsTable } from "./event-store/index.js";
25
26
  import { queueRebuildsFromMarkers, reportPendingRebuildRun, runPendingRebuilds, } from "./migrations/index.js";
26
27
  import { createEventConsumerStateTable, createProjectionStateTable } from "./pipeline/index.js";
27
28
  import { ensureTemporalPolyfill } from "./time/index.js";
28
29
  const SNAPSHOT_FILENAME = ".snapshot.json";
30
+ // `validate` only sees the composed FEATURES. The file provider normally comes from
31
+ // runProdApp's `fileStorage` option, which defaults FILE_STORAGE_PROVIDER at boot, so
32
+ // an unset variable here is no misconfiguration. The deploy env is checked by the real boot.
33
+ function withFileStoragePresence(env) {
34
+ return env[FILE_STORAGE_PROVIDER_ENV] === undefined
35
+ ? { ...env, [FILE_STORAGE_PROVIDER_ENV]: FILE_STORAGE_PROVIDER_BOOT_SENTINEL }
36
+ : { ...env };
37
+ }
29
38
  // Single load-path for kumiko/schema.ts (512/3) — `generate` and `validate`
30
39
  // both need ENTITY_METAS (and validate additionally wants FEATURES); this is
31
40
  // the one place that enforces "ENTITY_METAS must be an array" so the two
@@ -247,7 +256,9 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
247
256
  // 2. Boot validity — needs the composed feature set.
248
257
  if (Array.isArray(mod.FEATURES)) {
249
258
  try {
250
- validateBoot(mod.FEATURES);
259
+ validateBoot(mod.FEATURES, {
260
+ env: withFileStoragePresence(process.env),
261
+ });
251
262
  out.log(" ✓ boot: feature configuration is valid");
252
263
  }
253
264
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.370.0",
3
+ "version": "0.371.1",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -199,8 +199,8 @@
199
199
  "./package.json": "./package.json"
200
200
  },
201
201
  "dependencies": {
202
- "@cosmicdrift/kumiko-http": "0.370.0",
203
- "@cosmicdrift/kumiko-types": "0.370.0",
202
+ "@cosmicdrift/kumiko-http": "0.371.1",
203
+ "@cosmicdrift/kumiko-types": "0.371.1",
204
204
  "bullmq": "^5.76.7",
205
205
  "bun-types": "^1.3.13",
206
206
  "hono": "^4.13.1",
@@ -216,8 +216,8 @@
216
216
  "zod": "^4.4.3"
217
217
  },
218
218
  "devDependencies": {
219
- "@cosmicdrift/kumiko-dispatcher-live": "0.370.0",
220
- "@cosmicdrift/kumiko-testing": "0.370.0",
219
+ "@cosmicdrift/kumiko-dispatcher-live": "0.371.1",
220
+ "@cosmicdrift/kumiko-testing": "0.371.1",
221
221
  "bun-types": "^1.3.13",
222
222
  "pino-pretty": "^13.1.3"
223
223
  },
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.371.0",
4
+ "type": "improvement",
5
+ "title": "Dry-run env table and boot check respect required overrides and the fileStorage default",
6
+ "detail": "The env table of KUMIKO_DRY_RUN_ENV=boot shows a variable as required when the app schema overlays required() on an optional field, and the error says required. The schema-cli boot check applies runProdApp's fileStorage default instead of demanding FILE_STORAGE_PROVIDER."
7
+ },
2
8
  {
3
9
  "version": "0.370.0",
4
10
  "type": "improvement",