@cosmicdrift/kumiko-framework 0.352.0 → 0.354.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 (188) hide show
  1. package/dist/api/index.d.ts +1 -1
  2. package/dist/api/index.js +1 -1
  3. package/dist/api/pii-leak-guard.js +3 -0
  4. package/dist/api/redis-sse-broker.d.ts +1 -0
  5. package/dist/api/redis-sse-broker.js +16 -10
  6. package/dist/api/request-context.d.ts +1 -0
  7. package/dist/api/request-context.js +5 -0
  8. package/dist/api/route-registrars.d.ts +2 -1
  9. package/dist/api/route-registrars.js +8 -0
  10. package/dist/api/routes.js +4 -2
  11. package/dist/api/server.js +39 -6
  12. package/dist/api/sse-broker.d.ts +7 -3
  13. package/dist/api/sse-route.js +7 -1
  14. package/dist/api/token-expiry-timer.d.ts +1 -0
  15. package/dist/api/token-expiry-timer.js +10 -0
  16. package/dist/bun-db/query.js +36 -4
  17. package/dist/compliance/sub-processors.js +5 -0
  18. package/dist/crypto/event-pii.js +4 -2
  19. package/dist/crypto/kms-wiring.js +31 -5
  20. package/dist/crypto/pii-field-encryption.d.ts +2 -3
  21. package/dist/crypto/pii-field-encryption.js +2 -3
  22. package/dist/db/eagerload.d.ts +8 -3
  23. package/dist/db/eagerload.js +85 -81
  24. package/dist/db/entity-table-meta.js +11 -0
  25. package/dist/db/event-store-executor-context.d.ts +12 -0
  26. package/dist/db/event-store-executor-context.js +78 -42
  27. package/dist/db/event-store-executor-read.js +67 -25
  28. package/dist/db/event-store-executor-write.js +54 -3
  29. package/dist/db/index.d.ts +1 -1
  30. package/dist/db/migrate-generator.js +10 -2
  31. package/dist/db/queries/backfill-pii.d.ts +2 -0
  32. package/dist/db/queries/backfill-pii.js +24 -6
  33. package/dist/db/queries/event-consumer.d.ts +2 -3
  34. package/dist/db/queries/event-consumer.js +54 -43
  35. package/dist/db/queries/event-store.d.ts +4 -0
  36. package/dist/db/queries/event-store.js +40 -3
  37. package/dist/db/queries/pii-backfill-state.d.ts +8 -1
  38. package/dist/db/queries/pii-backfill-state.js +16 -5
  39. package/dist/db/queries/shadow-swap.js +37 -5
  40. package/dist/db/render-ddl.js +6 -6
  41. package/dist/db/table-builder.js +6 -7
  42. package/dist/db/tenant-db.d.ts +5 -2
  43. package/dist/db/tenant-db.js +47 -7
  44. package/dist/derivatives/variant-key.d.ts +1 -0
  45. package/dist/derivatives/variant-key.js +3 -0
  46. package/dist/engine/boot-validator/access-declarations.js +21 -1
  47. package/dist/engine/boot-validator/config-deps.js +3 -0
  48. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  49. package/dist/engine/boot-validator/entity-handler.js +53 -0
  50. package/dist/engine/boot-validator/handler-field-references.d.ts +2 -0
  51. package/dist/engine/boot-validator/handler-field-references.js +25 -0
  52. package/dist/engine/boot-validator/index.js +6 -1
  53. package/dist/engine/boot-validator/nav.js +3 -1
  54. package/dist/engine/boot-validator/ownership.d.ts +2 -1
  55. package/dist/engine/boot-validator/ownership.js +32 -1
  56. package/dist/engine/boot-validator/pii-retention.js +8 -1
  57. package/dist/engine/boot-validator/query-output-columns.js +9 -9
  58. package/dist/engine/boot-validator/screens.js +136 -15
  59. package/dist/engine/boot-validator/select-option-tone.d.ts +2 -0
  60. package/dist/engine/boot-validator/select-option-tone.js +4 -0
  61. package/dist/engine/boot-validator/transfer-graph.d.ts +2 -1
  62. package/dist/engine/boot-validator/transfer-graph.js +8 -63
  63. package/dist/engine/boot-validator/url-prefill-fields.d.ts +1 -0
  64. package/dist/engine/boot-validator/url-prefill-fields.js +35 -0
  65. package/dist/engine/build-app-schema.js +33 -2
  66. package/dist/engine/build-config-feature-schema.js +6 -0
  67. package/dist/engine/changeset-changes.d.ts +1 -0
  68. package/dist/engine/changeset-changes.js +49 -33
  69. package/dist/engine/dedupe-features.js +17 -1
  70. package/dist/engine/define-feature.js +11 -0
  71. package/dist/engine/entity-handlers.d.ts +3 -3
  72. package/dist/engine/entity-handlers.js +70 -40
  73. package/dist/engine/escape-hatch-reason.d.ts +2 -0
  74. package/dist/engine/escape-hatch-reason.js +8 -0
  75. package/dist/engine/extension-names.d.ts +8 -9
  76. package/dist/engine/extension-names.js +8 -9
  77. package/dist/engine/extensions/tenant-data.d.ts +3 -1
  78. package/dist/engine/extensions/tenant-resource.d.ts +9 -1
  79. package/dist/engine/factories.d.ts +1 -1
  80. package/dist/engine/feature-builder-state.d.ts +2 -7
  81. package/dist/engine/feature-changelog.d.ts +2 -0
  82. package/dist/engine/feature-changelog.js +6 -0
  83. package/dist/engine/feature-config-events-jobs.js +44 -4
  84. package/dist/engine/feature-entity-handlers.js +1 -1
  85. package/dist/engine/handler-field-references.d.ts +3 -0
  86. package/dist/engine/handler-field-references.js +31 -0
  87. package/dist/engine/i18n-key.js +13 -3
  88. package/dist/engine/index.d.ts +9 -3
  89. package/dist/engine/index.js +7 -2
  90. package/dist/engine/ownership.js +3 -4
  91. package/dist/engine/pipeline.js +18 -1
  92. package/dist/engine/project-app-schema-for-roles.js +11 -3
  93. package/dist/engine/registry-ingest.d.ts +1 -0
  94. package/dist/engine/registry-ingest.js +8 -3
  95. package/dist/engine/role-assignment.d.ts +2 -0
  96. package/dist/engine/role-assignment.js +2 -0
  97. package/dist/engine/run-pipeline.js +12 -1
  98. package/dist/engine/schema-builder.js +8 -0
  99. package/dist/engine/screen-helpers.d.ts +13 -0
  100. package/dist/engine/screen-helpers.js +31 -0
  101. package/dist/engine/soft-delete-cleanup.js +3 -2
  102. package/dist/engine/steps/_read-source.d.ts +4 -0
  103. package/dist/engine/steps/_read-source.js +22 -0
  104. package/dist/engine/steps/describe-workflow-step-error.js +3 -1
  105. package/dist/engine/steps/read-find-many.js +2 -1
  106. package/dist/engine/steps/read-find-one.js +2 -1
  107. package/dist/engine/steps/workflow-step-error.d.ts +5 -0
  108. package/dist/engine/steps/workflow-step-error.js +13 -0
  109. package/dist/engine/transfer-adjacency.d.ts +24 -0
  110. package/dist/engine/transfer-adjacency.js +107 -0
  111. package/dist/engine/where-rule-lint.js +6 -3
  112. package/dist/entrypoint/index.d.ts +2 -0
  113. package/dist/entrypoint/index.js +2 -0
  114. package/dist/env/index.d.ts +4 -0
  115. package/dist/env/index.js +49 -23
  116. package/dist/errors/classes.d.ts +8 -0
  117. package/dist/errors/classes.js +13 -0
  118. package/dist/errors/index.d.ts +2 -1
  119. package/dist/errors/index.js +2 -1
  120. package/dist/errors/reasons.d.ts +1 -0
  121. package/dist/errors/reasons.js +3 -0
  122. package/dist/errors/subject-erased.d.ts +2 -0
  123. package/dist/errors/subject-erased.js +9 -0
  124. package/dist/es-ops/context.js +19 -1
  125. package/dist/event-store/admin-api.js +14 -10
  126. package/dist/event-store/event-store.js +11 -9
  127. package/dist/event-store/events-schema.js +2 -1
  128. package/dist/files/file-handle.d.ts +1 -1
  129. package/dist/files/file-handle.js +30 -6
  130. package/dist/files/file-ref-entity.d.ts +6 -0
  131. package/dist/files/file-ref-table.d.ts +6 -0
  132. package/dist/files/file-routes.js +13 -7
  133. package/dist/files/resolve-content-type.d.ts +2 -0
  134. package/dist/files/resolve-content-type.js +8 -0
  135. package/dist/files/types.js +21 -1
  136. package/dist/i18n/required-surface-keys.d.ts +2 -0
  137. package/dist/i18n/required-surface-keys.js +8 -0
  138. package/dist/jobs/index.d.ts +1 -1
  139. package/dist/jobs/job-runner.d.ts +13 -0
  140. package/dist/jobs/job-runner.js +128 -22
  141. package/dist/migrations/pii-event-backfill.js +46 -25
  142. package/dist/observability/index.d.ts +1 -1
  143. package/dist/observability/index.js +1 -1
  144. package/dist/observability/metrics-wiring.d.ts +1 -1
  145. package/dist/observability/metrics-wiring.js +2 -2
  146. package/dist/observability/standard-metrics.d.ts +4 -0
  147. package/dist/observability/standard-metrics.js +12 -0
  148. package/dist/pipeline/dispatch-shared.d.ts +2 -0
  149. package/dist/pipeline/dispatch-shared.js +74 -9
  150. package/dist/pipeline/dispatch-stream.d.ts +5 -1
  151. package/dist/pipeline/dispatch-stream.js +19 -12
  152. package/dist/pipeline/dispatcher.d.ts +3 -1
  153. package/dist/pipeline/dispatcher.js +10 -15
  154. package/dist/pipeline/event-consumer-state.js +0 -26
  155. package/dist/pipeline/event-dispatcher-admin.js +17 -11
  156. package/dist/pipeline/event-dispatcher-delivery.d.ts +1 -1
  157. package/dist/pipeline/event-dispatcher-delivery.js +3 -2
  158. package/dist/pipeline/event-dispatcher.js +48 -7
  159. package/dist/pipeline/index.d.ts +2 -0
  160. package/dist/pipeline/index.js +1 -0
  161. package/dist/pipeline/member-read-only-transaction.js +25 -5
  162. package/dist/pipeline/member-reader.js +1 -0
  163. package/dist/pipeline/msp-rebuild.js +3 -3
  164. package/dist/pipeline/pending-gap-ranges.d.ts +1 -0
  165. package/dist/pipeline/pending-gap-ranges.js +22 -0
  166. package/dist/pipeline/system-hooks.js +4 -4
  167. package/dist/pipeline/system-identity-switch.d.ts +1 -1
  168. package/dist/pipeline/system-identity-switch.js +5 -4
  169. package/dist/pipeline/tenant-timezone-cache.js +5 -3
  170. package/dist/redis/cache-sync-topics.d.ts +1 -0
  171. package/dist/redis/cache-sync-topics.js +1 -0
  172. package/dist/redis/client.d.ts +1 -0
  173. package/dist/redis/client.js +7 -0
  174. package/dist/redis/index.d.ts +1 -1
  175. package/dist/redis/index.js +1 -1
  176. package/dist/redis/pubsub-signal.d.ts +1 -0
  177. package/dist/redis/pubsub-signal.js +3 -0
  178. package/dist/search/meilisearch-adapter.js +34 -4
  179. package/dist/stack/test-stack.d.ts +4 -1
  180. package/dist/stack/test-stack.js +9 -2
  181. package/dist/testing/e2e-generator.js +7 -2
  182. package/dist/testing/shared-entities.d.ts +10 -0
  183. package/dist/ui-types/index.d.ts +3 -3
  184. package/dist/ui-types/index.js +2 -2
  185. package/dist/upgrade-cli.js +7 -2
  186. package/package.json +12 -4
  187. package/src/changes.json +228 -0
  188. package/src/scripts/codemod/migrate-cross-tenant.ts +2 -1
@@ -189,6 +189,8 @@ export async function setupTestStack(options) {
189
189
  getActiveTenantIds: options.jobs.getActiveTenantIds,
190
190
  }),
191
191
  ...(runLogger?.onJobStart !== undefined && { onJobStart: runLogger.onJobStart }),
192
+ ...(runLogger?.onJobQueued !== undefined && { onJobQueued: runLogger.onJobQueued }),
193
+ ...(runLogger?.onJobDropped !== undefined && { onJobDropped: runLogger.onJobDropped }),
192
194
  // Tracker fires in finally: drainJobs wakes on it, so the log row must
193
195
  // be written first, and a throwing logger must not swallow the signal.
194
196
  onJobComplete: async (jobName, jobId, ...rest) => {
@@ -268,8 +270,13 @@ export async function setupTestStack(options) {
268
270
  // `rateLimit` key, so reading it back needs the same relaxed boundary
269
271
  // extraContext itself is typed with.
270
272
  const explicitRateLimit = appContext.rateLimit;
271
- const rateLimitFallback = appContext.redis === undefined && explicitRateLimit === undefined
272
- ? createRateLimitResolver({ redis: testRedis.redis })
273
+ const needsOwnRateLimitResolver = explicitRateLimit === undefined &&
274
+ (appContext.redis === undefined || options.rateLimitNowMs !== undefined);
275
+ const rateLimitFallback = needsOwnRateLimitResolver
276
+ ? createRateLimitResolver({
277
+ redis: testRedis.redis,
278
+ ...(options.rateLimitNowMs ? { nowMs: options.rateLimitNowMs } : {}),
279
+ })
273
280
  : undefined;
274
281
  const server = buildServer({
275
282
  registry,
@@ -325,8 +325,13 @@ function fieldToFixture(name, field) {
325
325
  return `e2e ${name} (long-form content)`;
326
326
  case "boolean":
327
327
  return true;
328
- case "select":
329
- return field.options[0] ?? "";
328
+ case "select": {
329
+ const conditional = new Set(field.conditionalOptions?.flatMap((rule) => rule.options));
330
+ return (field.default ??
331
+ field.options.find((option) => !conditional.has(option)) ??
332
+ field.options[0] ??
333
+ "");
334
+ }
330
335
  case "multiSelect": {
331
336
  const first = field.options[0];
332
337
  return first ? [first] : [];
@@ -3,6 +3,7 @@ export declare const sharedWidgetEntity: import("@cosmicdrift/kumiko-types/field
3
3
  readonly type: "text";
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
6
+ readonly minLength?: number;
6
7
  readonly required?: boolean;
7
8
  readonly searchable?: boolean;
8
9
  readonly sortable?: boolean;
@@ -26,6 +27,7 @@ export declare const sharedWidgetTable: import("../db/table-builder.js").EntityT
26
27
  readonly type: "text";
27
28
  readonly description?: string;
28
29
  readonly maxLength?: number;
30
+ readonly minLength?: number;
29
31
  readonly required?: boolean;
30
32
  readonly searchable?: boolean;
31
33
  readonly sortable?: boolean;
@@ -49,6 +51,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
49
51
  readonly type: "text";
50
52
  readonly description?: string;
51
53
  readonly maxLength?: number;
54
+ readonly minLength?: number;
52
55
  readonly required?: boolean;
53
56
  readonly searchable?: boolean;
54
57
  readonly sortable?: boolean;
@@ -70,6 +73,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
70
73
  readonly type: "text";
71
74
  readonly description?: string;
72
75
  readonly maxLength?: number;
76
+ readonly minLength?: number;
73
77
  readonly required?: boolean;
74
78
  readonly searchable?: boolean;
75
79
  readonly sortable?: boolean;
@@ -91,6 +95,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
91
95
  readonly type: "text";
92
96
  readonly description?: string;
93
97
  readonly maxLength?: number;
98
+ readonly minLength?: number;
94
99
  readonly required?: boolean;
95
100
  readonly searchable?: boolean;
96
101
  readonly sortable?: boolean;
@@ -117,6 +122,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
117
122
  readonly type: "text";
118
123
  readonly description?: string;
119
124
  readonly maxLength?: number;
125
+ readonly minLength?: number;
120
126
  readonly required?: boolean;
121
127
  readonly searchable?: boolean;
122
128
  readonly sortable?: boolean;
@@ -138,6 +144,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
138
144
  readonly type: "text";
139
145
  readonly description?: string;
140
146
  readonly maxLength?: number;
147
+ readonly minLength?: number;
141
148
  readonly required?: boolean;
142
149
  readonly searchable?: boolean;
143
150
  readonly sortable?: boolean;
@@ -159,6 +166,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
159
166
  readonly type: "text";
160
167
  readonly description?: string;
161
168
  readonly maxLength?: number;
169
+ readonly minLength?: number;
162
170
  readonly required?: boolean;
163
171
  readonly searchable?: boolean;
164
172
  readonly sortable?: boolean;
@@ -185,6 +193,7 @@ export declare const sharedItemEntity: import("@cosmicdrift/kumiko-types/fields"
185
193
  readonly type: "text";
186
194
  readonly description?: string;
187
195
  readonly maxLength?: number;
196
+ readonly minLength?: number;
188
197
  readonly required?: boolean;
189
198
  readonly searchable?: boolean;
190
199
  readonly sortable?: boolean;
@@ -227,6 +236,7 @@ export declare const sharedItemTable: import("../db/table-builder.js").EntityTab
227
236
  readonly type: "text";
228
237
  readonly description?: string;
229
238
  readonly maxLength?: number;
239
+ readonly minLength?: number;
230
240
  readonly required?: boolean;
231
241
  readonly searchable?: boolean;
232
242
  readonly sortable?: boolean;
@@ -3,8 +3,8 @@ export type { DerivedCellRoundingTarget } from "../engine/embedded-derived.js";
3
3
  export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedded-derived.js";
4
4
  export type { ParsedRefTarget } from "../engine/parse-ref-target.js";
5
5
  export { parseRefTarget } from "../engine/parse-ref-target.js";
6
- export type { FieldsOrGroupsSection } from "../engine/screen-helpers.js";
7
- export { evalFieldCondition, explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
6
+ export type { FieldsOrGroupsSection, UnavailableSelectOption } from "../engine/screen-helpers.js";
7
+ export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
8
8
  export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefinition, FieldDefinition, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, SelectFieldDef, SelectOptionTone, TextFieldDef, } from "../engine/types/fields.js";
9
9
  export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
10
10
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
@@ -14,7 +14,7 @@ export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "
14
14
  export type { TargetRef } from "../engine/types/target-ref.js";
15
15
  export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
16
16
  export type { WorkspaceDefinition } from "../engine/types/workspace.js";
17
- export { ACTION_FORM_ENTITY, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
17
+ export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
18
18
  export type { AppSchema, FeatureSchema, QualifiedContentCollection, WorkspaceSchema, } from "./app-schema.js";
19
19
  export { type Formality, formalLocaleTag } from "./formality.js";
20
20
  export type { ListRowMetaColumnType, ListRowMetaReference, ReferenceLookupSource, SystemReferenceLabel, } from "./list-row-meta.js";
@@ -22,10 +22,10 @@
22
22
  export { NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-types/fields";
23
23
  export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedded-derived.js";
24
24
  export { parseRefTarget } from "../engine/parse-ref-target.js";
25
- export { evalFieldCondition, explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
25
+ export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
26
26
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
27
27
  export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
28
- export { ACTION_FORM_ENTITY, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
28
+ export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
29
29
  export { formalLocaleTag } from "./formality.js";
30
30
  export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "./list-row-meta.js";
31
31
  export { resolveTranslationValue, translationValueOtherText, } from "./plural.js";
@@ -257,6 +257,9 @@ async function runCodemodScript(scriptPath, targetDir, repoRoot, dryRun) {
257
257
  function hasCodemod(e) {
258
258
  return typeof e.codemod === "string" && e.codemod.length > 0;
259
259
  }
260
+ function needsManualMigration(e) {
261
+ return !hasCodemod(e) || e.manualAfterCodemod === true;
262
+ }
260
263
  const MANUAL_ID_HASH_LENGTH = 8;
261
264
  const MAX_RESOLVE_REASON_LENGTH = 500;
262
265
  // The hash tells apart entries that share a version (studio has several at 0.341.0).
@@ -438,9 +441,11 @@ markerVersion, previous) {
438
441
  const codemodEntries = breaking
439
442
  .filter(hasCodemod)
440
443
  .sort((a, b) => compareVersions(a.version, b.version));
441
- const manualEntries = breaking.filter((e) => !hasCodemod(e));
444
+ const manualEntries = breaking.filter(needsManualMigration);
442
445
  for (const e of manualEntries) {
443
- out.log(` ⚠ ${e.version} · ${e.title} — no codemod, manual migration required`);
446
+ out.log(hasCodemod(e)
447
+ ? ` ⚠ ${e.version} · ${e.title} — codemod covers only part, manual migration still required`
448
+ : ` ⚠ ${e.version} · ${e.title} — no codemod, manual migration required`);
444
449
  }
445
450
  if (codemodEntries.length === 0) {
446
451
  out.log(breaking.length > 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.352.0",
3
+ "version": "0.354.0",
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>",
@@ -187,6 +187,10 @@
187
187
  "types": "./dist/testing/real-providers.d.ts",
188
188
  "default": "./dist/testing/real-providers.js"
189
189
  },
190
+ "./internal/request-context": {
191
+ "types": "./dist/api/request-context.d.ts",
192
+ "default": "./dist/api/request-context.js"
193
+ },
190
194
  "./testing/e2e-generator": {
191
195
  "types": "./dist/testing/e2e-generator.d.ts",
192
196
  "default": "./dist/testing/e2e-generator.js"
@@ -206,8 +210,8 @@
206
210
  "./package.json": "./package.json"
207
211
  },
208
212
  "dependencies": {
209
- "@cosmicdrift/kumiko-http": "0.352.0",
210
- "@cosmicdrift/kumiko-types": "0.352.0",
213
+ "@cosmicdrift/kumiko-http": "0.354.0",
214
+ "@cosmicdrift/kumiko-types": "0.354.0",
211
215
  "bullmq": "^5.76.7",
212
216
  "bun-types": "^1.3.13",
213
217
  "hono": "^4.13.1",
@@ -223,7 +227,7 @@
223
227
  "zod": "^4.4.3"
224
228
  },
225
229
  "devDependencies": {
226
- "@cosmicdrift/kumiko-dispatcher-live": "0.352.0",
230
+ "@cosmicdrift/kumiko-dispatcher-live": "0.354.0",
227
231
  "bun-types": "^1.3.13",
228
232
  "pino-pretty": "^13.1.3"
229
233
  },
@@ -387,6 +391,10 @@
387
391
  "types": "./dist/testing/real-providers.d.ts",
388
392
  "default": "./dist/testing/real-providers.js"
389
393
  },
394
+ "./internal/request-context": {
395
+ "types": "./dist/api/request-context.d.ts",
396
+ "default": "./dist/api/request-context.js"
397
+ },
390
398
  "./testing/e2e-generator": {
391
399
  "types": "./dist/testing/e2e-generator.d.ts",
392
400
  "default": "./dist/testing/e2e-generator.js"
package/src/changes.json CHANGED
@@ -1,4 +1,232 @@
1
1
  [
2
+ {
3
+ "version": "0.354.0",
4
+ "type": "fix",
5
+ "title": "detail() no longer pairs an old row with a newer stream version, so update({ version: detail.version }) cannot overwrite a concurrent write",
6
+ "detail": "`detail()` reads the row and the stream version in one statement and validates entity-cache hits against the stream version\nBefore, a concurrent update between the row read and the version read handed out the old row with the new version, so `update({ version: detail.version })` passed the optimistic lock and overwrote the newer write. A cached row whose version differs from the stream version now counts as a cache miss."
7
+ },
8
+ {
9
+ "version": "0.354.0",
10
+ "type": "fix",
11
+ "title": "_refs respect access.read, the parentRef gate and field-level read of the referenced entity; unreadable refs are omitted and the renderer falls back to the UUID",
12
+ "detail": "`_refs` only carry what the caller may read on the referenced entity\nEagerload used to resolve every referenced row of the tenant, so a reference field pointing at a team- or row-scoped entity leaked rows the caller could not read themselves. It now applies the target entity's `access.read` (row/team scope), its parentRef gate and its field-level read access, still with one batched lookup per reference field. A reference the caller may not read is omitted exactly like a missing one (no existence oracle); the renderer falls back to the UUID. The handler-level `access` of the target's detail handler is not evaluated, the declarative `access.read` of the target entity is authoritative (same model as reference search and sort).\n`enrichWithReferences` and `enrichRowWithReferences` take a required fifth argument `viewer: { user, parentVisibility }`. The old strip of PII/encrypted fields on ownership-scoped targets is gone: readable rows are decrypted, unreadable rows are not returned at all."
13
+ },
14
+ {
15
+ "version": "0.354.0",
16
+ "type": "breaking",
17
+ "title": "enrichWithReferences and enrichRowWithReferences require a viewer { user, parentVisibility }",
18
+ "migration": "Custom handlers pass `{ user: query.user, parentVisibility: { entities: ctx.registry.getAllEntities() } }` as the fifth argument. No direct callers are known in the workspace apps. Apps can now point reference fields at team- or row-scoped entities (for example solon unitId/propertyId)."
19
+ },
20
+ {
21
+ "version": "0.354.0",
22
+ "type": "breaking",
23
+ "title": "crossTenant removed from entity convention handlers; write handlers with escapeHatch need SystemAdmin-only access",
24
+ "migration": "Replace crossTenant: true with escapeHatch: { reason } (run bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/migrate-cross-tenant.ts, then replace its placeholder reason, which is rejected otherwise). A create/update/delete/restore handler with escapeHatch must declare access: { roles: [\"SystemAdmin\"] }; list and detail handlers keep their access."
25
+ },
26
+ {
27
+ "version": "0.354.0",
28
+ "type": "fix",
29
+ "title": "ctx.queryAsMember read-only context now allowlists searchAdapter (search), redis (read commands) and entityCache (get, mget); other methods are denied"
30
+ },
31
+ {
32
+ "version": "0.354.0",
33
+ "type": "improvement",
34
+ "title": "ctx.queryAsMember reads run under a statement timeout (DispatcherOptions.memberReadTimeoutMs, default 10000) and fail with member_read_timeout"
35
+ },
36
+ {
37
+ "version": "0.354.0",
38
+ "type": "fix",
39
+ "title": "ctx.queryProjection with unsafeAllTenants no longer demands a grant on projections without a tenant column"
40
+ },
41
+ {
42
+ "version": "0.354.0",
43
+ "type": "improvement",
44
+ "title": "defineProjectionQueryHandler accepts unsafeAllTenants: { reason } as grant and audit reason"
45
+ },
46
+ {
47
+ "version": "0.354.0",
48
+ "type": "fix",
49
+ "title": "Event append without a transaction is atomic; ctx.outsideTransaction; pending-gap ranges merge and are exported as a gauge; seed scan starts at the highest consumer cursor"
50
+ },
51
+ {
52
+ "version": "0.354.0",
53
+ "type": "improvement",
54
+ "title": "Failed job attempts are logged via the runner logger (job, jobId, attempt, final, error)"
55
+ },
56
+ {
57
+ "version": "0.354.0",
58
+ "type": "breaking",
59
+ "title": "Field-level access.read with a where-rule now fails the boot (also on embedded sub-fields)",
60
+ "migration": "Replace the where-rule on the field with a from()-rule (e.g. from(\"user:id\", \"ownerId\")) or move the where-rule to entity access.read. Field read access is evaluated in memory and could never grant a where-rule, so it only hid the field."
61
+ },
62
+ {
63
+ "version": "0.354.0",
64
+ "type": "improvement",
65
+ "title": "kumiko_events gets a partial index over payload tenantId for tenant-membership streams",
66
+ "detail": "The forget-cleanup membership-history queries filter on payload->>'tenantId' and scanned all of kumiko_events. createEventsTable now also ensures events_membership_payload_tenant_idx ON kumiko_events ((payload->>'tenantId')) WHERE aggregate_type = 'tenant-membership', built CONCURRENTLY with the same INVALID-rebuild and race handling as the idempotency index. No migration: the index is created on the next boot or schema apply. On a large event history the first build takes a while but does not block appends."
67
+ },
68
+ {
69
+ "version": "0.354.0",
70
+ "type": "improvement",
71
+ "title": "changes.json entries can set manualAfterCodemod to stay pending manual after the codemod ran",
72
+ "detail": "A codemod entry with `manualAfterCodemod: true` is recorded in `pendingManual` of `.kumiko/upgrade-state.json` in addition to the codemod run. Set it with `kumiko changes add --codemod <path> --manual-after-codemod`."
73
+ },
74
+ {
75
+ "version": "0.354.0",
76
+ "type": "improvement",
77
+ "title": "read.findOne and read.findMany log a warning when a foreign where.tenantId is narrowed to the caller's tenant scope"
78
+ },
79
+ {
80
+ "version": "0.354.0",
81
+ "type": "breaking",
82
+ "title": "requestContext.run and runAsDirectCallEntry removed from the public /api barrel; requestContext there is read-only",
83
+ "migration": "Import run and runAsDirectCallEntry from @cosmicdrift/kumiko-framework/internal/request-context; reserved for agent-tools and server runtime wiring, do not use to lift a handler past the risk floor. requestContext.get() from /api is unchanged."
84
+ },
85
+ {
86
+ "version": "0.354.0",
87
+ "type": "fix",
88
+ "title": "SSE broadcasts, toggle sync, stream expiry, page-head abort, PII backfill failures and preSave output are now handled strictly instead of failing silently",
89
+ "detail": "Review hardening: confirmed SSE publish awaited by consumers, feature-toggle cache invalidation over the cache-sync bus, stream expiry timer, page-head resolver abort signal, PII backfill `failed_event_ids`, preSave output validation, read-only proxy `then`/symbol guard, no orphan-row delete in event-consumer state, exported `DEFAULT_HTML` of dev-server and server-runtime"
90
+ },
91
+ {
92
+ "version": "0.354.0",
93
+ "type": "breaking",
94
+ "title": "TenantDestroyHookResult: done:false now requires processed",
95
+ "migration": "Tenant destroy hooks that return { done: false } must also return processed (number of items handled this tick). A tick with processed 0 now counts as a failed stage attempt."
96
+ },
97
+ {
98
+ "version": "0.354.0",
99
+ "type": "improvement",
100
+ "title": "Handler titles and field labels as i18n keys, plus the references Zod meta",
101
+ "detail": "`handlerTitleKey(qn)` (`<qn>:title`) and `handlerFieldLabelKey(qn, field)` (`<qn>:field:<field>`) define where a handler's title and input labels live; a feature declares the full keys (e.g. `channel-texts:write:generate:title`, built with `handlerTitleKey(qn)`) in `r.translations`, because the client resolves keys without a feature prefix. Mark a handler input field that points at an entity with `vehicleId: z.string().meta({ references: \"vehicle\" })` (or `\"feature:entity\"`); boot throws when the entity is unknown and `handlerFieldReference(schema, field)` reads it. `findHandlerTranslationGaps` reports agent-exposed write handlers without an entity or actionForm screen and without these keys as `handler-without-translation`. `kumiko agent lint` lists them as warnings without affecting the exit code, and the boot prints one summary line. `findAgentDocGaps` stays unchanged on purpose, since the agent-manifest guard fails on every gap it returns."
102
+ },
103
+ {
104
+ "version": "0.354.0",
105
+ "type": "fix",
106
+ "title": "list search no longer drops hits beyond the 50th candidate",
107
+ "detail": "event-store-executor list: the search path now asks the SearchAdapter for up to 1000 candidate ids instead of the adapter's default 50, so hits beyond the 50th are no longer silently dropped. When the adapter returns the full 1000 (Meilisearch's maxTotalHits cap), the list result carries `searchTruncated: true` (new optional field on `CursorResult`) so clients can tell the match set may be incomplete."
108
+ },
109
+ {
110
+ "version": "0.354.0",
111
+ "type": "fix",
112
+ "title": "The blockDelete-without-anonymize boot warning names the retention-cron risk instead of a Forget error",
113
+ "detail": "The boot warning for `retention.strategy: \"blockDelete\"` without a field `anonymize` now names the real risk: once keepFor expires, the data-retention cron anonymizes nothing and the row keeps its PII"
114
+ },
115
+ {
116
+ "version": "0.354.0",
117
+ "type": "breaking",
118
+ "title": "db.global(table) writes on executor-managed entity tables are rejected",
119
+ "detail": "Database, event-store consumer, search and Redis review fixes\n`db.global(table)` writes are rejected on executor-managed entity tables, and `unsafeRaw` handles derived through `begin`, `transaction` or `reserve` keep the personal-data gate. Reference sorting and search re-check label and read access plus ownership, and a stale search index can no longer surface another tenant's row. A cross-tenant convention handler reports one audit event per touched row with that row as target.\nA failing dispatcher pass no longer records its error on a consumer whose cursor moved in the meantime and skips a row another pass holds locked. A projection rebuild aborts when the configured blind-index key differs from the one used to build the live table. An array `ne` filter on a jsonb column means \"does not contain all of these\", matching the entity list filter. `date` columns that still hold a timestamptz log a warning once per column.\n`kumiko_event_dispatcher_listen_connected` only drops to 0 on a real connection-loss error and is restored when the pre-check recovers. A consumer logs once when it tracks more than 1000 pending gap ranges. The lazy Meilisearch default config no longer overwrites settings stored by an earlier `configure()` after a restart.\nAccess invalidation publishes through `PubSubSignal.publishConfirmed`, so a Redis failure fails the consumer and the event is redelivered instead of being dropped (`SseBroker.publishAccessInvalidation` may return a promise). `KUMIKO_REDIS_CHANNEL_PREFIX` namespaces the SSE and feature-toggle Pub/Sub channels for apps sharing one Redis; `createRedisToggleSyncSignal` takes the prefix as an optional third argument. Types that mention `Temporal` now carry the `temporal-polyfill/global` reference into their `.d.ts` files.",
120
+ "migration": "Write entities through the entity executor and keep db.global for hand-written unmanaged tables."
121
+ },
122
+ {
123
+ "version": "0.354.0",
124
+ "type": "fix",
125
+ "title": "unsafeRaw handles derived through begin, transaction or reserve keep the personal-data gate"
126
+ },
127
+ {
128
+ "version": "0.354.0",
129
+ "type": "fix",
130
+ "title": "A stale search index can no longer surface another tenant's row"
131
+ },
132
+ {
133
+ "version": "0.354.0",
134
+ "type": "fix",
135
+ "title": "A projection rebuild aborts when the blind-index key differs from the one used to build the live table"
136
+ },
137
+ {
138
+ "version": "0.354.0",
139
+ "type": "fix",
140
+ "title": "An array ne filter on a jsonb column means \"does not contain all of these\""
141
+ },
142
+ {
143
+ "version": "0.354.0",
144
+ "type": "fix",
145
+ "title": "Access invalidation publishes with confirmation so a Redis failure redelivers the event"
146
+ },
147
+ {
148
+ "version": "0.354.0",
149
+ "type": "breaking",
150
+ "title": "defineFeature throws when dedupeOptions holds a nested object",
151
+ "migration": "Flatten dedupeOptions to primitive values."
152
+ },
153
+ {
154
+ "version": "0.354.0",
155
+ "type": "breaking",
156
+ "title": "Boot validation rejects an object-form redirect with idFrom on a screen that has no id",
157
+ "migration": "Point the redirect at a screen with an id or drop idFrom."
158
+ },
159
+ {
160
+ "version": "0.354.0",
161
+ "type": "breaking",
162
+ "title": "A list column absent from the query output schema must set virtual: true",
163
+ "migration": "Add virtual: true to computed columns that are not part of the query output."
164
+ },
165
+ {
166
+ "version": "0.354.0",
167
+ "type": "fix",
168
+ "title": "Session-only httpRoutes now pass the global IP rate limit"
169
+ },
170
+ {
171
+ "version": "0.354.0",
172
+ "type": "breaking",
173
+ "title": "Boot validator rejects inert sortable, authored urlPrefillFields and an unknown listScreenId",
174
+ "migration": "Remove sortable from entityList columns and sortable true from projectionList columns (declare sorting on the entity field), remove urlPrefillFields from form screens (buildAppSchema derives it from navigate params), and point listScreenId at a mounted screen short id."
175
+ },
176
+ {
177
+ "version": "0.354.0",
178
+ "type": "breaking",
179
+ "title": "TreeAction is screen XOR target at the type level",
180
+ "migration": "Give every createAction and actions[] entry either screen or target, not both and not neither; the boot validator already rejected the other forms."
181
+ },
182
+ {
183
+ "version": "0.354.0",
184
+ "type": "improvement",
185
+ "title": "Row actions can open a secretMint or actionForm that submits without edits",
186
+ "detail": "actionForm and secretMint screens gain `submitPrefilled`: a row action that navigates in with `params` can submit the form right away, with the record id carried in a hidden or read-only field."
187
+ },
188
+ {
189
+ "version": "0.354.0",
190
+ "type": "improvement",
191
+ "title": "Select field options can depend on another field",
192
+ "detail": "Select fields gain `conditionalOptions`: options that are only available while a condition on a sibling field holds, filtered in the form and enforced on write."
193
+ },
194
+ {
195
+ "version": "0.354.0",
196
+ "type": "improvement",
197
+ "title": "setupTestStack accepts rateLimitNowMs to freeze the rate-limit clock in tests",
198
+ "detail": "setupTestStack accepts `rateLimitNowMs`, the time source of the stack's rate-limit resolver, so tests can freeze the token-bucket clock instead of racing the refill window"
199
+ },
200
+ {
201
+ "version": "0.353.0",
202
+ "type": "improvement",
203
+ "title": "Boot validation rejects public-intake handlers without rate limit and serial-id unmanaged entities with recordOwned fields",
204
+ "detail": "Boot validation rejects `personalData: \"public-intake\"` handlers with `rateLimit: { disabled: true }` and unmanaged `idType: \"serial\"` entities with `recordOwned` fields\nBoth combinations were silently accepted before: the first left an anonymous personal-data write without its only abuse protection, the second produced record-owned ciphertext that `forgetSubject` can never shred."
205
+ },
206
+ {
207
+ "version": "0.353.0",
208
+ "type": "improvement",
209
+ "title": "r.crud accepts per-verb agent hints and dedupeFeatures no longer drops a same-named feature that registers different handlers",
210
+ "detail": "`r.crud` takes per-verb `agents` hints, and same-named features only dedupe when their registered surface matches\n`registerEntityCrud` accepts `agents: { delete: { risk: \"high\" } }` so one irreversible verb can be raised without raising create/update. `dedupeFeatures` additionally requires equal entity and handler key sets before collapsing two same-named features with equal `dedupeOptions`; otherwise it throws the existing duplicate-feature error instead of silently dropping the second one. `i18nKey()` marks live on a `globalThis` registry so two installed framework copies agree."
211
+ },
212
+ {
213
+ "version": "0.353.0",
214
+ "type": "fix",
215
+ "title": "Event piiFields reject owner field \"id\" instead of encrypting under a phantom user key",
216
+ "detail": "`defineEvent` rejects `personal: { of: \"id\" }` in `piiFields` at boot\nOn entities `of: \"id\"` means record-owned, but on events it encrypted under a user key named after the payload id, which a forget never reaches, so crypto-shredding silently did nothing. Declare a record-owned event field with `personal: \"self\"` instead."
217
+ },
218
+ {
219
+ "version": "0.353.0",
220
+ "type": "fix",
221
+ "title": "Webhook signature 401 hides verify() error details; boot rejects silently dropped drawer prefills, colliding secret field ids and unlintable where-rule tables",
222
+ "detail": "Signature extra routes no longer return the verify() error text in the 401 body, drawer prefill and secret field ids fail at boot when they would silently misbehave\nA non-`ExtraRouteRejection` throw from a signature route's `verify()` now answers with the generic message \"signature verification failed\" and logs the detail server-side. Boot now rejects a drawer `params` prefill that targets a sensitive or password field or a field the target layout does not render, a secrets screen whose field ids collide across hyphenated feature names, and a where-rule subquery on a table whose column set cannot be derived."
223
+ },
224
+ {
225
+ "version": "0.353.0",
226
+ "type": "fix",
227
+ "title": "Unknown status tones fail boot and secretMint confirm actions respect role gating",
228
+ "detail": "Stricter boot checks and role projection for screens and handlers\nInline handler registration without `options.access` now throws immediately, a section with `fields: []` and `groups: []` fails boot, and unknown tones in `header.statusTones` or select `optionTones` fail boot instead of dropping the badge colour. The renderer falls back to the value heuristic for an unknown tone, and `statusToneForOptionTone` now returns `undefined` for it. `secretMint` confirm-step actions are stripped for roles that cannot see the target screen.\nEntity convention `create` handlers keep tenant-filtered lookups even when the handler declares `escapeHatch`/`crossTenant`. Duplicate `waitForEvent` steps on the same `awaits` event are rejected when the workflow pipeline is built. An empty `PROMETHEUS_METRICS_TOKEN` counts as unset instead of failing boot.\n`GET /api/sse` now closes itself when the JWT it was opened with expires. File uploads must attach to a registered file field (a non-file field answers 400 `unresolvable_field`), and a `.docx` upload must be a ZIP containing `word/` entries. Signature extra routes outside `/api` get the request-body cap (`maxRequestBytes`) before their body is read. `EXT_USER_DATA` registrations with neither an export nor a delete hook fail boot. The dashboard updated-at stamp follows live refetches and retries.\nText fields accept `minLength` (enforced by the generated write schema), `enumOption` renders array values per entry, list columns can opt out of sorting with `sortable: false`, and `BUILT_IN_MEMBERSHIP_ROLES` exposes the ranked membership roles. The PAT list translates its scopes column, MFA code fields enforce their minimum length, plan checkout no longer repeats the billing-enabled and active-subscription gates."
229
+ },
2
230
  {
3
231
  "version": "0.352.0",
4
232
  "type": "improvement",
@@ -15,6 +15,7 @@ import {
15
15
  type PropertyAssignment,
16
16
  SyntaxKind,
17
17
  } from "ts-morph";
18
+ import { CODEMOD_PLACEHOLDER_REASON_MARKER } from "../../engine/escape-hatch-reason.js";
18
19
  import { collectSourceFiles } from "./collect-source-files.js";
19
20
 
20
21
  export interface Rewrite {
@@ -151,7 +152,7 @@ function deriveBestEffortHandlerName(assignment: PropertyAssignment): string | u
151
152
 
152
153
  function buildEscapeHatchReason(target: AutoRewriteTarget): string {
153
154
  const verbKind = READ_VERBS.has(target.verb) ? "reads" : "writes";
154
- return `${target.handlerName} ${verbKind} ${target.entityName} rows across every tenant (migrated from crossTenant: true; state the operator use case here)`;
155
+ return `${target.handlerName} ${verbKind} ${target.entityName} rows across every tenant (migrated from crossTenant: true; ${CODEMOD_PLACEHOLDER_REASON_MARKER})`;
155
156
  }
156
157
 
157
158
  export function migrateCrossTenantSource(