@cosmicdrift/kumiko-framework 0.336.0 → 0.336.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.
@@ -301,7 +301,7 @@ function clientFaultLoggingEnabled() {
301
301
  return !FAULT_LOG_SILENCED_LEVELS.has(process.env["LOG_LEVEL"] ?? "");
302
302
  }
303
303
  // A failing request must leave a trace even when it ends in 4xx — a paid
304
- // external call that 422s was invisible before (offlot#117). Status, error
304
+ // external call that 422s was invisible before. Status, error
305
305
  // code and duration only: message/details/stack can carry submitted values.
306
306
  function logClientWarn(message, requestId, type, fields) {
307
307
  if (!clientFaultLoggingEnabled()) {
@@ -45,9 +45,9 @@ function resolvePiiStance(eventType) {
45
45
  // (user/tenant/self — resolveEventSubject, fw#2801). No-op when the event
46
46
  // type is uncatalogued or no subject KMS is configured (plaintext rollout
47
47
  // mode — the hard boot gate governs whether that is acceptable). A
48
- // user-subject field with no resolvable owner (system cron runs,
49
- // recipient-less skip attempts) leaves the value plaintext: there is no
50
- // user key to shred. This is the ONLY live-write encrypt path — the backfill
48
+ // user-subject field with an empty owner follows its declared `whenAbsent`
49
+ // (tenant key or acknowledged plaintext) and aborts the append when none is
50
+ // declared. This is the ONLY live-write encrypt path — the backfill
51
51
  // catalog branch (`backfill-pii.ts`) resolves through the same
52
52
  // resolveEventSubject so a field never ends up encrypted under different
53
53
  // subjects depending on which path wrote it.
@@ -53,7 +53,8 @@ function validateEscapeHatchReason(feature, kind, handlerName, escapeHatch) {
53
53
  return;
54
54
  throw new Error(`[Feature ${feature.name}] ${kind} handler "${handlerName}" declares ` +
55
55
  `{ escapeHatch: { reason: "" } } — the reason must be a non-empty string ` +
56
- "explaining why this handler needs db.global() write access or a SYSTEM identity switch.");
56
+ "explaining why this handler needs db.global() write access, a SYSTEM identity switch " +
57
+ "or a cross-tenant ctx.queryProjection({ unsafeAllTenants: true }) read.");
57
58
  }
58
59
  function validatePersonalDataOnlyOnWrite(feature, kind, handlerName, access) {
59
60
  // skip: write handlers may declare personalData, or it wasn't declared here
@@ -432,7 +432,8 @@ function projectField(fieldDef) {
432
432
  out["multiple"] = def["multiple"];
433
433
  // MultiSelect: display picks checkboxes vs. combobox in the renderer,
434
434
  // columns/maxRows size the checkbox grid — without these the renderer
435
- // always falls back to the combobox (fw#2494).
435
+ // always falls back to the combobox (fw#2494). Select: display picks
436
+ // "radio" | "dropdown" the same way.
436
437
  if (typeof def["display"] === "string")
437
438
  out["display"] = def["display"];
438
439
  if (typeof def["columns"] === "number")
@@ -70,11 +70,9 @@ export function defineQueryHandler(def,
70
70
  return def;
71
71
  }
72
72
  // Runtime marker set only by definePagedQueryHandler. A plain
73
- // defineQueryHandler-built object never carries it. Kept as a type-level
74
- // signal (fw#2216) even though no validator gates on it — QueryHandlerDef
75
- // has no output schema, so a boot check can't distinguish "returns
76
- // PagedRows" from "doesn't" without running the handler; the actual
77
- // enforcement is a runtime shape guard in the renderer (kumiko-screen.tsx).
73
+ // defineQueryHandler-built object never carries it. query-output-columns.ts
74
+ // reads it to gate the paged envelope check on outputSchema; the renderer's
75
+ // runtime shape guard (kumiko-screen.tsx) remains as a second line.
78
76
  //
79
77
  // A string key, not a Symbol: bundled-features imports this module through
80
78
  // the package's "@cosmicdrift/kumiko-framework/engine" subpath (symlinked
@@ -65,9 +65,10 @@ export async function clearPendingRebuilds(db, rows) {
65
65
  }
66
66
  }
67
67
  // buildProjectionTableIndex maps both single-stream and multi-stream projection
68
- // tables to their name, so the rebuild has to pick the matching path. Queue
69
- // drains are automatic rebuilds: a failed MSP replay rolls back, stays queued
70
- // and must not park the still-intact live consumer as dead.
68
+ // tables to their name, so the rebuild has to pick the matching path. Callers
69
+ // (queue drains, inline enqueue, the manual rebuild job) all rethrow the error,
70
+ // so it is recorded there; a failed MSP replay rolls back and must not park
71
+ // the still-intact live consumer as dead.
71
72
  export async function rebuildProjectionOrMultiStream(name, deps) {
72
73
  const { db, registry, signal, errorPolicy } = deps;
73
74
  if (registry.getAllMultiStreamProjections().has(name)) {
@@ -24,8 +24,8 @@ export async function runBatch(ctx, commands, user, requestId, inheritedOrigin)
24
24
  if (!current?.signal) {
25
25
  return runBatchBody(ctx, commands, user, requestId, inheritedOrigin);
26
26
  }
27
- // Strip the signal: a disconnect would roll back the tx, idempotency would
28
- // cache a 500 for the uncommitted write and afterCommit effects would be lost.
27
+ // Strip the signal: a client disconnect must not roll back a write whose
28
+ // afterCommit effects would then be lost.
29
29
  const { signal: _signal, ...withoutSignal } = current;
30
30
  return requestContext.run(withoutSignal, () => runBatchBody(ctx, commands, user, requestId, inheritedOrigin));
31
31
  }
@@ -67,7 +67,7 @@ export function createDispatcher(registry, context, options = {}) {
67
67
  if (isDeprecatedCrossTenantHandler(def.handler)) {
68
68
  context.log?.warn(DEPRECATED_CROSS_TENANT_SIGNAL, {
69
69
  handler: def.name,
70
- migration: 'replace crossTenant: true with escapeHatch: { reason: "<why this operator handler reads/writes every tenant>" } (bun scripts/codemod/migrate-cross-tenant.ts)',
70
+ migration: 'replace crossTenant: true with escapeHatch: { reason: "<why this operator handler reads/writes every tenant>" } (bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/migrate-cross-tenant.ts)',
71
71
  });
72
72
  }
73
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.336.0",
3
+ "version": "0.336.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>",
@@ -206,8 +206,8 @@
206
206
  "./package.json": "./package.json"
207
207
  },
208
208
  "dependencies": {
209
- "@cosmicdrift/kumiko-http": "0.336.0",
210
- "@cosmicdrift/kumiko-types": "0.336.0",
209
+ "@cosmicdrift/kumiko-http": "0.336.1",
210
+ "@cosmicdrift/kumiko-types": "0.336.1",
211
211
  "bullmq": "^5.76.7",
212
212
  "bun-types": "^1.3.13",
213
213
  "hono": "^4.13.1",
@@ -223,7 +223,7 @@
223
223
  "zod": "^4.4.3"
224
224
  },
225
225
  "devDependencies": {
226
- "@cosmicdrift/kumiko-dispatcher-live": "0.336.0",
226
+ "@cosmicdrift/kumiko-dispatcher-live": "0.336.1",
227
227
  "bun-types": "^1.3.13",
228
228
  "pino-pretty": "^13.1.3"
229
229
  },
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.336.1",
4
+ "type": "fix",
5
+ "title": "Cross-tenant deprecation warning names a runnable codemod path",
6
+ "detail": "The `deprecation:entity-handler-cross-tenant` boot warning now names the codemod path that runs from a consumer repo. The `reason` strings on the `passwordHash`, PAT `tokenHash` and PAT `prefix` fields no longer carry an issue-number suffix. The `pii-personal-migration` codemod now skips `.d.ts` files in `--report-stance` mode as well."
7
+ },
2
8
  {
3
9
  "version": "0.336.0",
4
10
  "type": "fix",
@@ -21,7 +21,9 @@
21
21
  // packages/types/src/fields.ts). Never guesses: anything outside this
22
22
  // table is reported (file:line) instead of transformed.
23
23
  //
24
- // Usage: bun scripts/codemod/pii-personal-migration.ts <targetDir> [--dry-run]
24
+ // Usage: bun scripts/codemod/pii-personal-migration.ts <targetDir> [--dry-run | --report-stance]
25
+ // --report-stance is read-only: it writes nothing and classifies fields
26
+ // that carry no personal stance.
25
27
 
26
28
  import { readFileSync } from "node:fs";
27
29
  import { relative, resolve } from "node:path";
@@ -423,14 +425,12 @@ function applyTransform(
423
425
  obj.insertPropertyAssignments(insertIndex, newProps);
424
426
  }
425
427
 
426
- function findTargetFiles(rootDir: string): string[] {
428
+ function findSourceFiles(rootDir: string, extraExcludes: readonly string[] = []): string[] {
427
429
  const glob = new Glob("**/*.{ts,tsx}");
428
- // engine/factories.ts constructs raw ResolvedPiiFlags literals as the
429
- // *implementation* of expandPersonalAnnotations (personal -> flags) —
430
- // not an authored override, so it's not a migration target.
431
- const EXCLUDE = ["/node_modules/", "/dist/", "/build/", "/engine/factories.ts"];
430
+ const EXCLUDE = ["/node_modules/", "/dist/", "/build/", ...extraExcludes];
432
431
  const files: string[] = [];
433
432
  for (const file of glob.scanSync({ cwd: rootDir, dot: false })) {
433
+ if (file.endsWith(".d.ts")) continue;
434
434
  const abs = resolve(rootDir, file);
435
435
  if (EXCLUDE.some((p) => abs.includes(p))) continue;
436
436
  files.push(abs);
@@ -666,19 +666,6 @@ function collectStanceSites(sourceFile: SourceFile): StanceSite[] {
666
666
  return sites;
667
667
  }
668
668
 
669
- function findReportStanceFiles(rootDir: string): string[] {
670
- const glob = new Glob("**/*.{ts,tsx}");
671
- const EXCLUDE = ["/node_modules/", "/dist/", "/build/"];
672
- const files: string[] = [];
673
- for (const file of glob.scanSync({ cwd: rootDir, dot: false })) {
674
- if (file.endsWith(".d.ts")) continue;
675
- const abs = resolve(rootDir, file);
676
- if (EXCLUDE.some((p) => abs.includes(p))) continue;
677
- files.push(abs);
678
- }
679
- return files.sort();
680
- }
681
-
682
669
  function formatHintSuffix(site: StanceSite): string {
683
670
  if (!site.hint) return "";
684
671
  return site.nearMissOf
@@ -701,7 +688,7 @@ function reportStance(rootDir: string): void {
701
688
  let total = 0;
702
689
 
703
690
  const project = createReportStanceProject();
704
- for (const file of findReportStanceFiles(rootDir)) {
691
+ for (const file of findSourceFiles(rootDir)) {
705
692
  const sites = reportStanceForSource(readFileSync(file, "utf8"), file, project);
706
693
  if (sites.length === 0) continue;
707
694
 
@@ -741,7 +728,10 @@ async function main(): Promise<void> {
741
728
  }
742
729
 
743
730
  const dryRun = process.argv.includes("--dry-run");
744
- const files = findTargetFiles(rootDir);
731
+ // engine/factories.ts constructs raw ResolvedPiiFlags literals as the
732
+ // *implementation* of expandPersonalAnnotations (personal -> flags) —
733
+ // not an authored override, so it's not a migration target.
734
+ const files = findSourceFiles(rootDir, ["/engine/factories.ts"]);
745
735
  const project = new Project({
746
736
  skipAddingFilesFromTsConfig: true,
747
737
  skipFileDependencyResolution: true,