@cosmicdrift/kumiko-framework 0.385.0 → 0.387.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 (298) hide show
  1. package/LICENSE +21 -57
  2. package/README.md +2 -2
  3. package/agents/widgets.md +39 -39
  4. package/dist/api/api-constants.js +2 -2
  5. package/dist/api/auth-middleware.js +7 -7
  6. package/dist/api/auth-rate-limiters.js +7 -3
  7. package/dist/api/auth-routes-mfa.js +1 -1
  8. package/dist/api/auth-routes-session.js +1 -1
  9. package/dist/api/auth-routes-shared.js +7 -3
  10. package/dist/api/auth-routes-signup.js +1 -1
  11. package/dist/api/client-ip.js +3 -1
  12. package/dist/api/extra-route.js +1 -1
  13. package/dist/api/pii-leak-guard.js +4 -2
  14. package/dist/api/readiness.js +5 -5
  15. package/dist/api/redis-sse-broker.js +8 -6
  16. package/dist/api/request-context.d.ts +2 -0
  17. package/dist/api/request-context.js +16 -7
  18. package/dist/api/routes.js +2 -2
  19. package/dist/api/server.js +34 -33
  20. package/dist/api/sse-broker.js +1 -1
  21. package/dist/api/sse-route.d.ts +13 -13
  22. package/dist/api/sse-route.js +15 -15
  23. package/dist/auth/index.js +4 -4
  24. package/dist/auth/roles.d.ts +4 -4
  25. package/dist/auth/roles.js +24 -24
  26. package/dist/compliance/duration-spec.js +18 -20
  27. package/dist/compliance/index.js +2 -3
  28. package/dist/compliance/override-schema.d.ts +4 -4
  29. package/dist/compliance/override-schema.js +19 -22
  30. package/dist/compliance/profiles.d.ts +26 -29
  31. package/dist/compliance/profiles.js +62 -69
  32. package/dist/compliance/sub-processors.d.ts +28 -30
  33. package/dist/compliance/sub-processors.js +17 -21
  34. package/dist/consumer-cli.js +4 -4
  35. package/dist/crypto/blind-index.js +18 -17
  36. package/dist/crypto/ciphertext-pattern.js +1 -1
  37. package/dist/crypto/event-pii.js +3 -3
  38. package/dist/crypto/kek-source.d.ts +1 -1
  39. package/dist/crypto/kek-source.js +3 -2
  40. package/dist/crypto/kms-adapter.d.ts +1 -1
  41. package/dist/crypto/kms-adapter.js +4 -6
  42. package/dist/crypto/kms-wiring.d.ts +1 -2
  43. package/dist/crypto/kms-wiring.js +2 -2
  44. package/dist/crypto/pg-kms-adapter.js +2 -6
  45. package/dist/crypto/pii-field-encryption.js +4 -4
  46. package/dist/crypto/subject-resolver.d.ts +1 -1
  47. package/dist/crypto/subject-resolver.js +2 -2
  48. package/dist/db/apply-entity-event.d.ts +6 -6
  49. package/dist/db/apply-entity-event.js +55 -55
  50. package/dist/db/blind-index-cleanup.js +7 -7
  51. package/dist/db/collect-table-metas.js +10 -11
  52. package/dist/db/compound-types.d.ts +1 -1
  53. package/dist/db/compound-types.js +12 -12
  54. package/dist/db/cross-tenant-reads.d.ts +8 -0
  55. package/dist/db/cross-tenant-reads.js +23 -0
  56. package/dist/db/cursor.js +3 -4
  57. package/dist/db/dialect.d.ts +2 -2
  58. package/dist/db/dialect.js +2 -2
  59. package/dist/db/eagerload.d.ts +3 -3
  60. package/dist/db/eagerload.js +21 -12
  61. package/dist/db/entity-table-meta.d.ts +2 -2
  62. package/dist/db/entity-table-meta.js +4 -4
  63. package/dist/db/event-store-executor-context.d.ts +1 -1
  64. package/dist/db/event-store-executor-context.js +13 -13
  65. package/dist/db/event-store-executor-read.js +27 -28
  66. package/dist/db/event-store-executor-write.js +65 -70
  67. package/dist/db/located-timestamp.d.ts +11 -11
  68. package/dist/db/located-timestamp.js +29 -30
  69. package/dist/db/migrate-generator.d.ts +1 -1
  70. package/dist/db/migrate-generator.js +26 -19
  71. package/dist/db/migrate-runner.js +25 -25
  72. package/dist/db/money.d.ts +6 -6
  73. package/dist/db/money.js +29 -27
  74. package/dist/db/queries/backfill-pii.js +5 -5
  75. package/dist/db/queries/ddl.js +1 -1
  76. package/dist/db/queries/es-ops.d.ts +1 -1
  77. package/dist/db/queries/es-ops.js +1 -1
  78. package/dist/db/queries/event-consumer.js +3 -3
  79. package/dist/db/queries/event-store.js +4 -2
  80. package/dist/db/queries/projection-rebuild.js +3 -3
  81. package/dist/db/queries/raw-sql.d.ts +2 -2
  82. package/dist/db/queries/raw-sql.js +2 -2
  83. package/dist/db/queries/shadow-swap.js +19 -19
  84. package/dist/db/queries/test-stack.d.ts +4 -1
  85. package/dist/db/queries/test-stack.js +15 -2
  86. package/dist/db/query.d.ts +1 -1
  87. package/dist/db/query.js +6 -6
  88. package/dist/db/rebuild-marker.js +20 -21
  89. package/dist/db/replay-migration-sql.js +2 -2
  90. package/dist/db/schema-inspection.js +4 -5
  91. package/dist/db/table-builder.d.ts +1 -2
  92. package/dist/db/table-builder.js +82 -82
  93. package/dist/db/tenant-db.d.ts +2 -2
  94. package/dist/db/tenant-db.js +9 -2
  95. package/dist/derivatives/delete-stored-file.js +5 -2
  96. package/dist/derivatives/derivatives-context.js +1 -1
  97. package/dist/engine/boot-validator/access-roles.js +4 -3
  98. package/dist/engine/boot-validator/api-ext.js +14 -14
  99. package/dist/engine/boot-validator/config-deps.js +7 -6
  100. package/dist/engine/boot-validator/detail-screens.js +3 -3
  101. package/dist/engine/boot-validator/entity-handler.js +60 -61
  102. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  103. package/dist/engine/boot-validator/i18n-keys.js +1 -1
  104. package/dist/engine/boot-validator/index.d.ts +2 -2
  105. package/dist/engine/boot-validator/index.js +20 -20
  106. package/dist/engine/boot-validator/nav.js +5 -5
  107. package/dist/engine/boot-validator/ownership.js +12 -9
  108. package/dist/engine/boot-validator/parent-ref.js +1 -1
  109. package/dist/engine/boot-validator/pii-retention.js +50 -57
  110. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  111. package/dist/engine/boot-validator/query-refs.js +1 -1
  112. package/dist/engine/boot-validator/screens-actions.js +15 -16
  113. package/dist/engine/boot-validator/screens-forms.js +10 -11
  114. package/dist/engine/boot-validator/screens-lists.js +14 -15
  115. package/dist/engine/boot-validator/screens.js +34 -40
  116. package/dist/engine/boot-validator/security-baseline.js +3 -2
  117. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  118. package/dist/engine/boot-validator/workspaces.js +3 -3
  119. package/dist/engine/bridge-stub.js +3 -4
  120. package/dist/engine/build-app-schema.js +75 -75
  121. package/dist/engine/build-config-feature-schema.js +50 -50
  122. package/dist/engine/build-target.js +20 -23
  123. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  124. package/dist/engine/config-helpers.js +4 -4
  125. package/dist/engine/constants.js +2 -2
  126. package/dist/engine/create-app.js +2 -2
  127. package/dist/engine/define-feature.js +12 -11
  128. package/dist/engine/define-handler.js +12 -11
  129. package/dist/engine/define-step.js +1 -1
  130. package/dist/engine/define-workflow.d.ts +5 -5
  131. package/dist/engine/define-workflow.js +6 -6
  132. package/dist/engine/entity-handlers.js +13 -14
  133. package/dist/engine/event-def.js +2 -2
  134. package/dist/engine/extension-names.d.ts +16 -17
  135. package/dist/engine/extension-names.js +37 -38
  136. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  137. package/dist/engine/extensions/tenant-data.js +2 -2
  138. package/dist/engine/extensions/user-data.d.ts +23 -24
  139. package/dist/engine/factories.d.ts +14 -13
  140. package/dist/engine/factories.js +38 -37
  141. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  142. package/dist/engine/feature-ast/extractors/handlers.js +1 -1
  143. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  144. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  145. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  146. package/dist/engine/feature-ast/parse.js +13 -13
  147. package/dist/engine/feature-ast/patch.js +1 -1
  148. package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -0
  149. package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
  150. package/dist/engine/feature-ast/patterns.d.ts +2 -2
  151. package/dist/engine/feature-ast/render.js +2 -2
  152. package/dist/engine/feature-config-events-jobs.js +15 -12
  153. package/dist/engine/feature-entity-handlers.js +9 -5
  154. package/dist/engine/feature-manifest.d.ts +9 -9
  155. package/dist/engine/feature-manifest.js +6 -7
  156. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  157. package/dist/engine/feature-ui-extensions.js +30 -22
  158. package/dist/engine/field-access.js +5 -6
  159. package/dist/engine/index.d.ts +1 -0
  160. package/dist/engine/index.js +4 -4
  161. package/dist/engine/object-form.d.ts +1 -0
  162. package/dist/engine/object-form.js +14 -4
  163. package/dist/engine/ownership.js +3 -3
  164. package/dist/engine/parse-ref-target.js +5 -7
  165. package/dist/engine/pattern-library/index.js +2 -2
  166. package/dist/engine/pattern-library/library.js +2 -2
  167. package/dist/engine/pattern-library/types.js +3 -3
  168. package/dist/engine/pipeline.js +2 -2
  169. package/dist/engine/project-app-schema-for-roles.js +1 -1
  170. package/dist/engine/projection-helpers.js +9 -9
  171. package/dist/engine/qualified-name.js +2 -2
  172. package/dist/engine/registry-facade.js +1 -1
  173. package/dist/engine/registry-ingest.js +12 -12
  174. package/dist/engine/registry-state.js +17 -19
  175. package/dist/engine/registry-validate.js +12 -12
  176. package/dist/engine/run-in.js +2 -2
  177. package/dist/engine/run-pipeline.js +7 -0
  178. package/dist/engine/schema-builder.js +40 -38
  179. package/dist/engine/screen-filter-ops.js +17 -20
  180. package/dist/engine/screen-helpers.js +4 -3
  181. package/dist/engine/state-machine.d.ts +12 -12
  182. package/dist/engine/state-machine.js +7 -7
  183. package/dist/engine/steps/_resolver-utils.js +4 -3
  184. package/dist/engine/steps/for-each.js +4 -4
  185. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  186. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  187. package/dist/engine/steps/wait-for-event.js +4 -4
  188. package/dist/engine/steps/workflow-system-events.js +3 -3
  189. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  190. package/dist/engine/tier-resolver-extension.js +26 -26
  191. package/dist/engine/transfer-adjacency.js +2 -2
  192. package/dist/engine/validate-projection-allowlist.js +7 -27
  193. package/dist/engine/walk-step-instances.d.ts +2 -0
  194. package/dist/engine/walk-step-instances.js +38 -0
  195. package/dist/engine/where-rule-lint.js +1 -1
  196. package/dist/engine/write-handler-qn-extract.js +1 -1
  197. package/dist/entrypoint/index.js +3 -3
  198. package/dist/env/index.d.ts +7 -6
  199. package/dist/env/index.js +10 -9
  200. package/dist/errors/transition-details.js +7 -7
  201. package/dist/errors/write-error-info.d.ts +6 -6
  202. package/dist/errors/write-error-info.js +6 -6
  203. package/dist/es-ops/context.d.ts +9 -9
  204. package/dist/es-ops/context.js +28 -28
  205. package/dist/es-ops/index.js +7 -9
  206. package/dist/es-ops/operations-schema.js +4 -10
  207. package/dist/es-ops/runner.d.ts +11 -11
  208. package/dist/es-ops/runner.js +49 -53
  209. package/dist/es-ops/types.d.ts +55 -55
  210. package/dist/es-ops/types.js +11 -11
  211. package/dist/event-store/admin-api.js +6 -7
  212. package/dist/event-store/errors.js +1 -1
  213. package/dist/event-store/event-store.js +2 -2
  214. package/dist/event-store/rebuild-dead-letter.js +1 -1
  215. package/dist/files/feature.js +9 -10
  216. package/dist/files/file-handle.js +5 -6
  217. package/dist/files/file-ref-entity.js +15 -15
  218. package/dist/files/file-ref-table.js +7 -8
  219. package/dist/files/file-routes.d.ts +7 -0
  220. package/dist/files/file-routes.js +11 -5
  221. package/dist/files/in-memory-provider.js +8 -9
  222. package/dist/files/local-provider.js +18 -25
  223. package/dist/files/provider-resolver.d.ts +7 -7
  224. package/dist/files/storage-tracking.js +2 -2
  225. package/dist/files/zip-stream.d.ts +12 -19
  226. package/dist/files/zip-stream.js +44 -63
  227. package/dist/i18n/required-surface-keys.d.ts +1 -1
  228. package/dist/i18n/required-surface-keys.js +8 -8
  229. package/dist/internal/stack-support.d.ts +1 -1
  230. package/dist/internal/stack-support.js +1 -1
  231. package/dist/jobs/job-runner-helpers.d.ts +1 -2
  232. package/dist/jobs/job-runner-helpers.js +8 -18
  233. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  234. package/dist/jobs/job-runner-schedulers.js +3 -4
  235. package/dist/jobs/job-runner.js +17 -18
  236. package/dist/lifecycle/lifecycle.js +3 -5
  237. package/dist/lifecycle/signal-handlers.js +5 -4
  238. package/dist/logging/utils.d.ts +2 -1
  239. package/dist/logging/utils.js +38 -10
  240. package/dist/migrations/index.js +1 -1
  241. package/dist/migrations/kumiko-drift.js +14 -13
  242. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  243. package/dist/migrations/pending-rebuilds.js +27 -27
  244. package/dist/migrations/projection-table-index.d.ts +3 -3
  245. package/dist/migrations/projection-table-index.js +8 -8
  246. package/dist/observability/metric-validator.js +1 -1
  247. package/dist/observability/metrics-handle.js +3 -3
  248. package/dist/pipeline/append-event-core.js +11 -2
  249. package/dist/pipeline/cascade-handler.d.ts +1 -1
  250. package/dist/pipeline/dispatch-batch.js +2 -2
  251. package/dist/pipeline/dispatch-query.js +10 -10
  252. package/dist/pipeline/dispatch-shared.d.ts +1 -0
  253. package/dist/pipeline/dispatch-shared.js +42 -19
  254. package/dist/pipeline/dispatch-stream.js +1 -1
  255. package/dist/pipeline/dispatch-write.js +3 -3
  256. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  257. package/dist/pipeline/dispatcher-utils.js +1 -1
  258. package/dist/pipeline/event-consumer-state.js +2 -2
  259. package/dist/pipeline/event-dispatcher-delivery.js +1 -1
  260. package/dist/pipeline/event-dispatcher.js +17 -28
  261. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  262. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  263. package/dist/pipeline/member-read-only-transaction.js +1 -3
  264. package/dist/pipeline/msp-rebuild.js +1 -1
  265. package/dist/pipeline/projection-rebuild.js +13 -12
  266. package/dist/pipeline/system-hooks.js +39 -41
  267. package/dist/pipeline/system-identity-switch.js +1 -3
  268. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  269. package/dist/random/generate.d.ts +29 -29
  270. package/dist/random/generate.js +30 -30
  271. package/dist/random/words.js +20 -20
  272. package/dist/rate-limit/middleware.js +9 -5
  273. package/dist/redis/client.js +2 -2
  274. package/dist/redis/pubsub-signal.js +14 -17
  275. package/dist/schema-cli.js +116 -22
  276. package/dist/search/index.js +4 -4
  277. package/dist/search/purge-subject.js +4 -5
  278. package/dist/search/reindex-entity.js +6 -9
  279. package/dist/time/boot-tz-warning.d.ts +2 -2
  280. package/dist/time/boot-tz-warning.js +8 -8
  281. package/dist/time/iana.js +10 -10
  282. package/dist/time/index.js +9 -9
  283. package/dist/time/legacy-date.d.ts +2 -2
  284. package/dist/time/legacy-date.js +6 -8
  285. package/dist/time/polyfill.js +1 -1
  286. package/dist/time/tz-context.d.ts +1 -1
  287. package/dist/time/tz-context.js +1 -1
  288. package/dist/ui-types/app-schema.js +7 -7
  289. package/dist/ui-types/list-row-meta.js +4 -4
  290. package/dist/upgrade-cli.js +10 -9
  291. package/dist/utils/compare.js +1 -1
  292. package/package.json +4 -4
  293. package/src/changes.json +54 -0
  294. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  295. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  296. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  297. package/src/scripts/codemod/pii-personal-migration.ts +17 -8
  298. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -1,18 +1,16 @@
1
1
  import { RECORD_ENTITY_PATTERN, subjectIdToKey, } from "@cosmicdrift/kumiko-types/kms-adapter-types";
2
2
  import * as z from "zod";
3
3
  export * from "@cosmicdrift/kumiko-types/kms-adapter-types";
4
- // The KMS error classes live here and not in kumiko-types (#1629): callers
4
+ // The KMS error classes live here and not in kumiko-types: callers
5
5
  // branch on them with `instanceof`, which needs a single copy of the class.
6
6
  // `satisfies z.ZodType<SubjectId>` plus the reverse check in subject-id-schema.test.ts bind this to the TS type
7
- // at compile time (fw#2801) — mint (subjectKeyForRecord) and shred (forget-subject) can no
8
- // longer drift apart on which subjects are valid, the way #2809 happened.
7
+ // at compile time — mint (subjectKeyForRecord) and shred (forget-subject) can no
8
+ // longer drift apart on which subjects are valid, the way it happened.
9
9
  export const subjectIdSchema = z.discriminatedUnion("kind", [
10
10
  z.object({ kind: z.literal("user"), userId: z.uuid() }),
11
11
  z.object({
12
12
  kind: z.literal("tenant"),
13
- // z.uuid() only proves string shape; TenantId is a nominal brand on top
14
- // of that, minted here right after validation (@cast-boundary).
15
- tenantId: z.uuid().transform((value) => value),
13
+ tenantId: z.uuid(),
16
14
  }),
17
15
  z.object({
18
16
  kind: z.literal("record"),
@@ -19,8 +19,7 @@ export type PgKmsRotationEnv = KmsWiringEnv & {
19
19
  * plaintext fallback) so a short-lived process can `finally { await
20
20
  * wiring.close() }` without narrowing first. Skipping it leaves the adapter's
21
21
  * connection pool holding the event loop open and the process never exits —
22
- * a long-running server is supposed to hold it, every ops script is not
23
- * (fw#2551). */
22
+ * a long-running server is supposed to hold it, every ops script is not. */
24
23
  export type KmsWiringRelease = {
25
24
  readonly close: () => Promise<void>;
26
25
  };
@@ -1,5 +1,5 @@
1
1
  // Boot-time env validation for the subject-keys KMS, shared by every app that
2
- // mounts crypto-shredding. Lived copy-pasted in four apps before fw#1617; the
2
+ // mounts crypto-shredding. It lived copy-pasted in four apps; the
3
3
  // copies drifted apart in their parsers, which is the failure mode this module
4
4
  // exists to end.
5
5
  //
@@ -16,7 +16,7 @@ const DEFAULT_PLAINTEXT_REASON = "local dev without subject-keys KMS (fw#818)";
16
16
  // the integer kek_version column never hits that slot, silently leaving
17
17
  // rotation-window rows unreadable. Same class: "0x10" -> 16, " 2 " trimmed,
18
18
  // "-1"/"0" accepted as versions, values past MAX_SAFE_INTEGER rounded.
19
- // A digits-only regex closes all of these at once (phronexsis#323).
19
+ // A digits-only regex closes all of these at once.
20
20
  function parseKekVersion(raw, fieldName) {
21
21
  if (!/^[0-9]+$/.test(raw)) {
22
22
  throw new Error(`${fieldName} must be a positive integer, got "${raw}".`);
@@ -179,12 +179,8 @@ export async function rewrapSubjectKeys(options) {
179
179
  }
180
180
  const batchSize = options.batchSize ?? 200;
181
181
  const sql = postgres(options.databaseUrl, { max: 2, onnotice: () => { } });
182
- const result = {
183
- scanned: 0,
184
- rewrapped: 0,
185
- skippedErased: 0,
186
- failures: [],
187
- };
182
+ const failures = [];
183
+ const result = { scanned: 0, rewrapped: 0, skippedErased: 0, failures };
188
184
  try {
189
185
  let cursor = "";
190
186
  for (;;) {
@@ -1,13 +1,13 @@
1
- // PII field encryption with per-subject DEKs (crypto-shredding, #724 phase C).
1
+ // PII field encryption with per-subject DEKs (crypto-shredding).
2
2
  // Same executor hook points as `encrypted: true`, but the key belongs to the
3
3
  // erase subject — kms.eraseKey(subject) makes every value unreadable at once.
4
4
  // Storage format is a sniffable string that fits existing text columns and
5
5
  // names its subject inline, so decrypt needs no schema change and no resolver:
6
6
  // kumiko-pii:v2:<subjectKey>:<base64(iv|tag|ciphertext)>
7
- // v2 GCM-binds the ciphertext to `subjectKey|field` as AAD (#1263) — subject
7
+ // v2 GCM-binds the ciphertext to `subjectKey|field` as AAD — subject
8
8
  // selects the DEK, field stops a cut-and-paste between two fields of the
9
9
  // SAME subject (same DEK, so key selection alone can't catch that) from
10
- // decrypting silently. v1 (no AAD) stays decrypt-only for pre-#1263 rows;
10
+ // decrypting silently. v1 (no AAD) stays decrypt-only for older rows;
11
11
  // every new write emits v2.
12
12
  // v3 = v2 for jsonb fields: the plaintext is JSON.stringify(value) and decrypt
13
13
  // JSON.parses it back, so a jsonb string/number/object round-trips by type.
@@ -187,7 +187,7 @@ export async function decryptPiiFieldValues(row, piiFields, kms, kmsCtx) {
187
187
  for (const name of piiFields) {
188
188
  const value = out[name];
189
189
  // Pre-engine plaintext rows pass through unchanged (mixed-state reads
190
- // work during rollout; backfill is tracked in kumiko-framework#799).
190
+ // work during rollout).
191
191
  if (!isPiiCiphertext(value))
192
192
  continue;
193
193
  const { subject, blob, hasAad, isJson } = parseCiphertext(value);
@@ -33,5 +33,5 @@ export declare function isRecordKeyedField(field: EntityDefinition["fields"][str
33
33
  export declare function collectPiiSubjectFields(entity: EntityDefinition): readonly string[];
34
34
  export declare function collectPiiEncryptedFields(entity: EntityDefinition): readonly string[];
35
35
  export declare function collectPiiFileReferenceFields(entity: EntityDefinition): readonly string[];
36
- /** Subject-annotated fields that may be plaintext in the derived search index (#1610). */
36
+ /** Subject-annotated fields that may be plaintext in the derived search index. */
37
37
  export declare function collectSearchableSubjectFields(entity: EntityDefinition): readonly string[];
@@ -107,7 +107,7 @@ function resolveEnvelopeRecordSubject(fieldName, envelope) {
107
107
  // backfill catalog path (backfillEventPiiEncryption) — the reason the two
108
108
  // can never encrypt the same field under different subjects. A "user"
109
109
  // subject whose owner field is empty falls back to the declared whenAbsent
110
- // stance and throws when none is declared (fw#2776); "tenant"/"self" throw
110
+ // stance and throws when none is declared; "tenant"/"self" throw
111
111
  // on an empty envelope, since their facts are structural.
112
112
  export function resolveEventSubject(fieldName, spec, payload, envelope) {
113
113
  const normalized = normalizeEventPiiSubject(spec);
@@ -177,7 +177,7 @@ export function collectPiiFileReferenceFields(entity) {
177
177
  .filter(([, field]) => hasSubjectAnnotation(field) && isFileReferenceField(field))
178
178
  .map(([name]) => name);
179
179
  }
180
- /** Subject-annotated fields that may be plaintext in the derived search index (#1610). */
180
+ /** Subject-annotated fields that may be plaintext in the derived search index. */
181
181
  export function collectSearchableSubjectFields(entity) {
182
182
  return Object.entries(entity.fields)
183
183
  .filter(([, field]) => {
@@ -3,7 +3,7 @@ import type { EntityDefinition } from "../engine/types/index.ts";
3
3
  import type { StoredEvent } from "../event-store/index.ts";
4
4
  import type { DbRow } from "./connection.ts";
5
5
  import type { TableColumns } from "./dialect.ts";
6
- type Table = TableColumns<any>;
6
+ type Table = TableColumns;
7
7
  export type AutoVerb = "created" | "updated" | "deleted" | "restored" | "forgotten";
8
8
  export type ApplyResult = {
9
9
  readonly kind: "applied";
@@ -12,11 +12,11 @@ export type ApplyResult = {
12
12
  } | {
13
13
  readonly kind: "skipped";
14
14
  };
15
- /** Parsed event.type → AutoVerb wenn das Event eines der Auto-Verben
16
- * auf dem gegebenen Aggregate ist. null sonst (Domain-Event). */
15
+ /** Parses event.type into an AutoVerb when the event is an auto-verb
16
+ * for the given aggregate; otherwise returns null (a domain event). */
17
17
  export declare function parseAutoVerb(event: StoredEvent): AutoVerb | null;
18
- /** Idempotente Anwendung eines Auto-Events auf die Entity-Tabelle.
19
- * Wird sowohl beim Live-Append (innerhalb der Write-TX) als auch beim
20
- * Rebuild (innerhalb der Rebuild-TX) gerufen — identische Logik. */
18
+ /** Applies an auto-event idempotently to the entity table.
19
+ * Called both during live append (inside the write TX) and during rebuild
20
+ * (inside the rebuild TX), using identical logic. */
21
21
  export declare function applyEntityEvent(event: StoredEvent, table: Table, entity: EntityDefinition, tx: ExecutorManagedRunner): Promise<ApplyResult>;
22
22
  export {};
@@ -1,17 +1,17 @@
1
- // applyEntityEvent — die EINZIGE Schreib-Logik für r.entity-Tabellen aus
2
- // Stored-Events. Beide Aufrufer benutzen sie:
1
+ // applyEntityEvent — the ONLY write logic for r.entity tables from
2
+ // StoredEvents. Both callers use it:
3
3
  //
4
- // - createEventStoreExecutor (live, im Write-TX) — übergibt das gerade
5
- // appendete StoredEvent direkt; sensitive Felder liegen als Tabellen-
6
- // Ciphertext im Payload (boot-validiert pii/encrypted, #967).
7
- // - rebuildProjection via ImplicitProjection (replay, im Rebuild-TX) —
8
- // übergibt dasselbe StoredEvent; apply kopiert Ciphertext byte-gleich
9
- // zurück, bidx wird unten neu berechnet.
4
+ // - createEventStoreExecutor (live, in the write TX) passes the just-appended
5
+ // StoredEvent directly; sensitive fields are table ciphertext in the payload
6
+ // (validated at boot as pii/encrypted).
7
+ // - rebuildProjection via ImplicitProjection (replay, in the rebuild TX)
8
+ // passes the same StoredEvent; apply copies ciphertext byte-for-byte,
9
+ // while bidx is recalculated below.
10
10
  //
11
- // Live==Rebuild-Equivalence ist damit by-construction für ALLE Felder —
12
- // eine geänderte Schreib-Logik muss nur an EINER Stelle gepflegt werden,
13
- // kein Sync-Contract mehr. Einzige legitime Divergenz: Crypto-Shredding
14
- // (DEK erased → Wert unlesbar, bidx NULL). Load-bearing Test:
11
+ // Live==Rebuild equivalence therefore holds by construction for ALL fields —
12
+ // changed write logic needs to be maintained in only ONE place, with no sync
13
+ // contract. The only legitimate divergence is crypto-shredding
14
+ // (DEK erased → value unreadable, bidx NULL). Load-bearing test:
15
15
  // db/__tests__/implicit-projection-equivalence.integration.test.ts.
16
16
  //
17
17
  // Tenant isolation: applyEntityEvent expects an ExecutorManagedRunner (tx or pool), NOT
@@ -23,26 +23,27 @@
23
23
  // the TenantDb wrapper default.
24
24
  // The missing TenantDb wrapper in this function is therefore not a hole.
25
25
  //
26
- // Auto-Verben:
26
+ // Auto-verbs:
27
27
  // <entity>.created → INSERT
28
28
  // <entity>.updated → UPDATE WHERE id=aggregateId
29
- // <entity>.deleted → soft-delete-UPDATE wenn entity.softDelete, sonst hard-DELETE
30
- // <entity>.restored → undelete-UPDATE (nur bei softDelete sinnvoll)
31
- // <entity>.forgotten → hard-DELETE immer (Art.17-Purge, auch bei softDelete);
32
- // via executor.forget(). Rebuild replayt created→forgotten
33
- // → Row weg, rebuild-safe (was ein direktes deleteMany nicht ist).
29
+ // <entity>.deleted → soft-delete-UPDATE if entity.softDelete, otherwise hard-DELETE
30
+ // <entity>.restored → undelete-UPDATE (meaningful only for softDelete)
31
+ // <entity>.forgotten → always hard-DELETE (Art.17 purge, including softDelete);
32
+ // via executor.forget(). Rebuild replays created→forgotten
33
+ // → the row is gone, making this rebuild-safe (unlike a
34
+ // direct deleteMany).
34
35
  //
35
- // Domain-Events (r.defineEvent) auf demselben Aggregate werden hier NICHT
36
- // behandelt — die liefen im Live-Pfad nie durch den Executor und müssen
37
- // von expliziten r.projection-apply-Handlern oder r.multiStreamProjection
38
- // behandelt werden. ImplicitProjection registriert die Auto-Verben.
36
+ // Domain events (r.defineEvent) on the same aggregate are NOT handled here —
37
+ // they never went through the Executor in the live path and must be handled by
38
+ // explicit r.projection-apply handlers or r.multiStreamProjection.
39
+ // ImplicitProjection registers the auto-verbs.
39
40
  //
40
- // Return-Shape: ApplyResult mit `kind` + optionaler `row`.
41
- // - "applied" → Schreibung lief durch. `row` enthält die geschriebene
42
- // Row für create/update/soft-delete/restore. Bei hard-delete ist
43
- // `row` null (DELETE-Statements geben keine returning-Row her).
44
- // - "skipped" → Event ist kein Auto-Verb (Domain-Event auf demselben
45
- // Aggregate). Caller no-op.
41
+ // Return shape: ApplyResult with `kind` and an optional `row`.
42
+ // - "applied" → the write completed. `row` contains the written row for
43
+ // create/update/soft-delete/restore. For hard-delete, `row` is null
44
+ // (DELETE statements do not provide a returning row).
45
+ // - "skipped" → the event is not an auto-verb (a domain event on the same
46
+ // aggregate). Caller no-op.
46
47
  import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
47
48
  import { requestContext } from "../api/request-context.js";
48
49
  import { collectLookupableFields, computeBlindIndexValues } from "../crypto/blind-index.js";
@@ -51,8 +52,8 @@ import { collectPiiFileReferenceFields } from "../crypto/subject-resolver.js";
51
52
  import { deleteMany, insertOne, updateMany } from "../db/query.js";
52
53
  import { InternalError } from "../errors/index.js";
53
54
  const AUTO_VERBS = ["created", "updated", "deleted", "restored", "forgotten"];
54
- /** Parsed event.type → AutoVerb wenn das Event eines der Auto-Verben
55
- * auf dem gegebenen Aggregate ist. null sonst (Domain-Event). */
55
+ /** Parses event.type into an AutoVerb when the event is an auto-verb
56
+ * for the given aggregate; otherwise returns null (a domain event). */
56
57
  export function parseAutoVerb(event) {
57
58
  const prefix = `${event.aggregateType}.`;
58
59
  if (!event.type.startsWith(prefix))
@@ -81,9 +82,9 @@ async function decryptFileReferenceCiphertext(entity, values) {
81
82
  }
82
83
  return decrypted;
83
84
  }
84
- /** Idempotente Anwendung eines Auto-Events auf die Entity-Tabelle.
85
- * Wird sowohl beim Live-Append (innerhalb der Write-TX) als auch beim
86
- * Rebuild (innerhalb der Rebuild-TX) gerufen — identische Logik. */
85
+ /** Applies an auto-event idempotently to the entity table.
86
+ * Called both during live append (inside the write TX) and during rebuild
87
+ * (inside the rebuild TX), using identical logic. */
87
88
  export async function applyEntityEvent(event, table, entity, tx) {
88
89
  const verb = parseAutoVerb(event);
89
90
  if (verb === null)
@@ -91,17 +92,17 @@ export async function applyEntityEvent(event, table, entity, tx) {
91
92
  const softDelete = entity.softDelete ?? false;
92
93
  switch (verb) {
93
94
  case "created": {
94
- // tenantId-Resolution explizit, nicht via Spread-Reihenfolge:
95
- // Live-Pfad nutzt tx=tenantDbRunner(db) (kein TenantDb-Wrapper-Auto-Inject),
96
- // beim Replay erst recht keiner. Default = event.tenantId; payload
97
- // gewinnt NUR wenn gültig string mit length > 0 (seedTenantMembership-
98
- // Pfad: Operator schreibt im Ziel-Tenant, Event im Operator-Tenant).
99
- // Pinst durch db/__tests__/apply-entity-event-tenant.integration.ts.
95
+ // Resolve tenantId explicitly rather than through spread order:
96
+ // the live path uses tx=tenantDbRunner(db) (with no TenantDb wrapper
97
+ // auto-injection), and replay has no wrapper either. The default is
98
+ // event.tenantId; the payload wins ONLY when it is a non-empty string
99
+ // (seedTenantMembership path: the operator writes in the target tenant,
100
+ // while the event belongs to the operator tenant).
100
101
  //
101
- // Fail-loud wenn payload.tenantId gesetzt aber invalid (leer/null/
102
- // non-string): das ist tenant-isolation-kritisch — silent fallback
103
- // auf event.tenantId würde eine Bug-payload in den Operator-Tenant
104
- // schreiben statt zu failen, was Cross-Tenant-Datendrift erzeugt.
102
+ // Fail loudly when payload.tenantId is set but invalid (empty/null/
103
+ // non-string): this is critical for tenant isolation. Silently falling
104
+ // back to event.tenantId would write a bad payload into the operator
105
+ // tenant and cause cross-tenant data drift.
105
106
  const payloadTenantId = event.payload["tenantId"];
106
107
  let tenantId;
107
108
  if (payloadTenantId === undefined) {
@@ -117,10 +118,10 @@ export async function applyEntityEvent(event, table, entity, tx) {
117
118
  }
118
119
  const row = await insertOne(tx, table, {
119
120
  ...(await decryptFileReferenceCiphertext(entity, event.payload)),
120
- // Blind-Index-Spalten aus dem Payload-Wert (ciphertext → decrypt →
121
- // HMAC, plaintext → HMAC, erased → NULL). Hier statt im Executor,
122
- // damit Live-Write und Rebuild denselben Wert produzieren und der
123
- // deterministische HMAC nie im Event-Log landet.
121
+ // Compute blind-index columns from the payload value (ciphertext → decrypt →
122
+ // HMAC, plaintext → HMAC, erased → NULL). This is done here rather
123
+ // than in the Executor so live writes and rebuilds produce the same
124
+ // value and the deterministic HMAC never enters the event log.
124
125
  ...(await computeBlindIndexValues(event.payload, collectLookupableFields(entity))),
125
126
  tenantId,
126
127
  id: event.aggregateId,
@@ -131,7 +132,7 @@ export async function applyEntityEvent(event, table, entity, tx) {
131
132
  return { kind: "applied", verb, row: row ?? null };
132
133
  }
133
134
  case "updated": {
134
- // payload-Shape: { changes, previous } — siehe event-store-executor.ts.
135
+ // Payload shape: { changes, previous } — see event-store-executor.ts.
135
136
  const rawChanges = (event.payload["changes"] ?? {}); // @cast-boundary engine-payload
136
137
  const changes = {};
137
138
  for (const [key, value] of Object.entries(await decryptFileReferenceCiphertext(entity, rawChanges))) {
@@ -146,7 +147,7 @@ export async function applyEntityEvent(event, table, entity, tx) {
146
147
  }
147
148
  const rows = await updateMany(tx, table, {
148
149
  ...changes,
149
- // bidx nur für die geänderten lookupable-Felder (siehe created).
150
+ // Recompute bidx only for the lookupable fields that changed (see created).
150
151
  ...(await computeBlindIndexValues(changes, collectLookupableFields(entity))),
151
152
  version: event.version,
152
153
  modifiedAt: event.createdAt,
@@ -166,16 +167,15 @@ export async function applyEntityEvent(event, table, entity, tx) {
166
167
  }, { id: event.aggregateId });
167
168
  return { kind: "applied", verb, row: rows[0] ?? null };
168
169
  }
169
- // Hard-Delete: DELETE-Statement gibt keine returning-Row her und
170
- // der Live-Pfad nutzt eh `existing` (pre-delete-Snapshot) für die
171
- // Response. Beim Replay ist das fine, der Caller braucht die Row
172
- // nicht weiter.
170
+ // Hard-delete: the DELETE statement provides no returning row, and
171
+ // the live path already uses `existing` (the pre-delete snapshot) for
172
+ // the response. During replay, the caller does not need the row.
173
173
  await deleteMany(tx, table, { id: event.aggregateId });
174
174
  return { kind: "applied", verb, row: null };
175
175
  }
176
176
  case "restored": {
177
- // Restore ist nur bei softDelete sinnvoll. Hard-Delete-Entities sollten
178
- // keine restored-Events erhalten — falls doch, defensive skip.
177
+ // Restore is meaningful only for softDelete. Hard-delete entities should
178
+ // not receive restored events; if they do, defensively skip them.
179
179
  if (!softDelete)
180
180
  return { kind: "skipped" };
181
181
  const rows = await updateMany(tx, table, {
@@ -1,4 +1,4 @@
1
- // Immediate blind-index nulling after a subject erase (#818).
1
+ // Immediate blind-index nulling after a subject erase.
2
2
  //
3
3
  // After kms.eraseKey the ciphertext is unreadable, but the deterministic
4
4
  // bidx column would stay matchable until the next write/rebuild — a
@@ -33,9 +33,9 @@ export async function nullBlindIndexesForSubject(db, features, subjectKey) {
33
33
  }
34
34
  }
35
35
  // Skip entities whose projection was never migrated — same class as
36
- // subjectRowExistsInTenant (fw#2348). A throw here runs AFTER eraseKey
36
+ // subjectRowExistsInTenant. A throw here runs AFTER eraseKey
37
37
  // in forget-subject and would leave deterministic bidx columns still
38
- // linkable (fw#2550). One batched existence query instead of one
38
+ // linkable. One batched existence query instead of one
39
39
  // roundtrip per entity.
40
40
  const existing = await existingTableNames(db, candidates.map((c) => c.tableName));
41
41
  for (const { tableName, lookupable } of candidates) {
@@ -59,7 +59,7 @@ async function existingTableNames(db, tableNames) {
59
59
  const rows = await executeRawQueryRead(db, `SELECT t.name FROM unnest($1::text[]) AS t(name) WHERE to_regclass('public.' || quote_ident(t.name)) IS NOT NULL`, [toPostgresArrayLiteral(tableNames)]);
60
60
  return new Set(rows.map((r) => r.name));
61
61
  }
62
- // Tenant-scope oracle for crypto-shredding's forget-subject (mh#349): a
62
+ // Tenant-scope oracle for crypto-shredding's forget-subject: a
63
63
  // "user"-kind subject id is often not a real user (share-token recipient,
64
64
  // email subscriber, ...) — those entities self-own their PII (`pii: true`,
65
65
  // i.e. their own row id IS the subject) and carry a real tenant_id,
@@ -71,14 +71,14 @@ async function existingTableNames(db, tableNames) {
71
71
  // Invariant: `id` must never be client-settable on self-PII entities — the
72
72
  // framework create path strips client ids; app write paths MUST do the same
73
73
  // or a DPO could plant a foreign subject id in their tenant and pass this
74
- // oracle (#2348). Upgrade path: require an event-store provenance check
74
+ // oracle. Upgrade path: require an event-store provenance check
75
75
  // (`aggregate_id = subjectId` under the actor tenant) when apps need
76
76
  // client-supplied ids.
77
77
  export async function subjectRowExistsInTenant(db, features, subjectId, tenantId) {
78
78
  // Prefetch which self-PII projection tables actually exist — probing a
79
79
  // missing relation used to throw (and get swallowed), which both hid
80
80
  // real schema bugs and risked poisoning the driver connection for the
81
- // rest of the forget TX (fw#2348 / framework#356).
81
+ // rest of the forget TX.
82
82
  const candidateTables = [];
83
83
  for (const feature of features.values()) {
84
84
  for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
@@ -121,7 +121,7 @@ function findEntityByExactName(features, entityName) {
121
121
  //
122
122
  // Invariant: sound only because `id` is a single-column PK per entity
123
123
  // table (globally unique, unspoofable via a client-supplied explicitId
124
- // at create) — a composite PK or shared table would break this (#2348).
124
+ // at create) — a composite PK or shared table would break this.
125
125
  export async function recordRowExistsInTenant(db, features, entityName, recordId, tenantId) {
126
126
  const entity = findEntityByExactName(features, entityName);
127
127
  if (!entity)
@@ -1,15 +1,14 @@
1
1
  // Canonical table source for both `kumiko schema generate` and setupTestStack
2
2
  // (entities, storeTables, projections, multiStreamProjections). One shared
3
- // source so the test-DB push and migrations cannot drift apart again (#255, #3102).
3
+ // source so the test-DB push and migrations cannot drift apart again.
4
4
  import { compareByCodepoint } from "../utils/index.js";
5
5
  import { assertBackingTableSuperset, deriveEntityTableMeta, } from "./entity-table-meta.js";
6
6
  import { enumerateFeatureTableSources } from "./feature-table-sources.js";
7
7
  import { asEntityTableMeta } from "./query.js";
8
8
  function canonicalColumnsKey(meta) {
9
- // Spalten-Identität unabhängig von Deklarations-Reihenfolge und
10
- // Objekt-Key-Order. Indexes bleiben außen vor: eine Projection-Table
11
- // (buildEntityTable ohne relations) trägt legitim weniger FK-Indexes
12
- // als das Entity-Meta derselben Tabelle.
9
+ // Column identity independent of declaration order and object-key order.
10
+ // Indexes are excluded: a projection table (buildEntityTable without relations)
11
+ // legitimately has fewer FK indexes than the entity metadata for the same table.
13
12
  return [...meta.columns]
14
13
  .sort((a, b) => compareByCodepoint(a.name, b.name))
15
14
  .map((c) => `${c.name}|${c.pgType}|${c.notNull}|${c.defaultSql ?? ""}|${c.primaryKey ?? false}|${c.identity ?? false}|${c.bigintJsMode ?? ""}`)
@@ -27,8 +26,8 @@ function registerPass1Meta(metas, byName, meta, origin) {
27
26
  export function collectTableMetas(features) {
28
27
  const metas = [];
29
28
  const byName = new Map();
30
- // Pass 1: kanonische Schema-Quellen, identisch zum bisherigen Template-
31
- // Verhalten (gleiche Reihenfolge, gleiche deriveEntityTableMeta-Optionen).
29
+ // Pass 1: canonical schema sources, matching the previous template behavior
30
+ // (same order, same deriveEntityTableMeta options).
32
31
  for (const feature of features) {
33
32
  for (const [name, ent] of Object.entries(feature.entities ?? {})) {
34
33
  const fieldMeta = deriveEntityTableMeta(name, ent, { relations: feature.relations[name] });
@@ -52,10 +51,10 @@ export function collectTableMetas(features) {
52
51
  registerPass1Meta(metas, byName, entry.meta, `storeTable "${entry.name}" (${feature.name})`);
53
52
  }
54
53
  }
55
- // Pass 2: table-tragende Registrierungen. Zwei Pässe, damit eine Entity-
56
- // Tabelle aus Feature B gegen eine gleichnamige Projection-Table aus
57
- // Feature A gewinnt — Entity-Metas sind die reichere Quelle (FK-Indexes
58
- // aus relations).
54
+ // Pass 2: table-bearing registrations. Two passes ensure that an entity table
55
+ // from Feature B takes precedence over a same-named projection table from
56
+ // Feature A—entity metadata is the richer source because of FK indexes from
57
+ // relations.
59
58
  for (const feature of features) {
60
59
  for (const { table, origin } of enumerateFeatureTableSources(feature)) {
61
60
  const meta = asEntityTableMeta(table);
@@ -4,6 +4,6 @@ import type { EntityDefinition } from "../engine/types/index.ts";
4
4
  */
5
5
  export declare function flattenCompoundTypes(payload: Record<string, unknown>, entity: EntityDefinition): Record<string, unknown>;
6
6
  /**
7
- * DB-Form (flat) → API-Form (combined). Wird nach jedem Read aufgerufen.
7
+ * DB form (flat) → API form (combined). Called after every read.
8
8
  */
9
9
  export declare function rehydrateCompoundTypes(row: Record<string, unknown>, entity: EntityDefinition): Record<string, unknown>;
@@ -1,20 +1,20 @@
1
- // Compound-Type Pipeline für den Executor.
1
+ // Compound-type pipeline for the executor.
2
2
  //
3
- // Ein Compound-Type ist ein Field das aus EINEM API-Object und MEHREREN
4
- // DB-Spalten besteht. Beispiele:
3
+ // A compound type is a field represented by ONE API object and MULTIPLE
4
+ // DB columns. Examples:
5
5
  // - locatedTimestamp: { at, tz, utc } ↔ <name>Utc + <name>Tz
6
6
  // - money: { amount, currency } ↔ <name> + <name>Currency
7
- // - (kommt) address: { street, zip, city } ↔ <name>Street + <name>Zip + <name>City
7
+ // - (upcoming) address: { street, zip, city } ↔ <name>Street + <name>Zip + <name>City
8
8
  //
9
- // Statt jeden Helper im Executor an 4 Stellen verschachtelt aufzurufen,
10
- // pipeline-iert diese Funktion alle Compound-Type-Konvertierungen in
11
- // einem Pass. Beim Hinzufügen eines neuen Compound-Types nur EINE Stelle
12
- // erweitern (das Array hier), nicht alle Executor-Aufrufe.
9
+ // Instead of nesting every helper in the executor at four different points,
10
+ // this function pipelines all compound-type conversions in one pass. Adding
11
+ // a new compound type requires extending only ONE place (this array), rather
12
+ // than every executor call.
13
13
  import { flattenLocatedTimestamp, rehydrateLocatedTimestamp } from "./located-timestamp.js";
14
14
  import { flattenMoney, rehydrateMoney } from "./money.js";
15
- // Reihenfolge ist egal solange die Konverter sich nicht gegenseitig
16
- // überlappen (z.B. money darf nicht ein Feld berühren das locatedTimestamp
17
- // schon erzeugt hat). Aktuell überlappen sie nicht — types sind disjunkt.
15
+ // Order is irrelevant as long as the converters do not overlap (for example,
16
+ // money must not touch a field already produced by locatedTimestamp). They do
17
+ // not currently overlap—types are disjoint.
18
18
  const FLATTENERS = [flattenLocatedTimestamp, flattenMoney];
19
19
  const REHYDRATORS = [rehydrateLocatedTimestamp, rehydrateMoney];
20
20
  /**
@@ -24,7 +24,7 @@ export function flattenCompoundTypes(payload, entity) {
24
24
  return FLATTENERS.reduce((acc, fn) => fn(acc, entity), payload);
25
25
  }
26
26
  /**
27
- * DB-Form (flat) → API-Form (combined). Wird nach jedem Read aufgerufen.
27
+ * DB form (flat) → API form (combined). Called after every read.
28
28
  */
29
29
  export function rehydrateCompoundTypes(row, entity) {
30
30
  return REHYDRATORS.reduce((acc, fn) => fn(acc, entity), row);
@@ -0,0 +1,8 @@
1
+ import type { CrossTenantReads } from "@cosmicdrift/kumiko-types/tenant-db-types";
2
+ import type { EscapeHatchReporter } from "../engine/types/index.ts";
3
+ import { type CreateTenantDbOptions, type TenantDb } from "./tenant-db.ts";
4
+ export declare function createCrossTenantReads(systemModeDb: TenantDb, reason: string, report: EscapeHatchReporter): CrossTenantReads;
5
+ export declare function createHandlerCrossTenantReads(options: Omit<CreateTenantDbOptions, "mode"> & {
6
+ readonly reason: string;
7
+ readonly report: EscapeHatchReporter;
8
+ }): CrossTenantReads;
@@ -0,0 +1,23 @@
1
+ import { createTenantDb } from "./tenant-db.js";
2
+ export function createCrossTenantReads(systemModeDb, reason, report) {
3
+ return {
4
+ selectMany(table, where, options) {
5
+ report("cross-tenant-read", reason);
6
+ return systemModeDb.selectMany(table, where, options);
7
+ },
8
+ fetchOne(table, where) {
9
+ report("cross-tenant-read", reason);
10
+ return systemModeDb.fetchOne(table, where);
11
+ },
12
+ count(table, where) {
13
+ report("cross-tenant-read", reason);
14
+ return systemModeDb.count(table, where);
15
+ },
16
+ };
17
+ }
18
+ // Own system-mode TenantDb without unsafeRaw/globalWrites: the handler gets the read surface only.
19
+ export function createHandlerCrossTenantReads(options) {
20
+ const { reason, report, ...dbOptions } = options;
21
+ const systemModeDb = createTenantDb({ ...dbOptions, mode: "system" });
22
+ return createCrossTenantReads(systemModeDb, reason, report);
23
+ }
package/dist/db/cursor.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
- // String-basiert damit UUIDs (Default seit Sprint F) + Integer-Auto-Increment
3
- // durch denselben Cursor-Pfad laufen. UUIDv7 erfüllt die lex-Monotonie
4
- // (time-ordered Prefix); UUIDv4 nicht — wer den nutzt, kriegt inkorrekte
5
- // cursor-Reihenfolge (Default ist v7).
2
+ // Uses strings so UUIDs and integer auto-increment IDs can share the same cursor path.
3
+ // UUIDv7 preserves lexicographic monotonicity through its time-ordered prefix; UUIDv4 does not,
4
+ // so UUIDv4 produces incorrect cursor ordering. The default is v7.
6
5
  export function encodeCursor(id) {
7
6
  return Buffer.from(String(id)).toString("base64url");
8
7
  }
@@ -3,8 +3,8 @@ import { type ColumnHandle, type SchemaTable } from "@cosmicdrift/kumiko-types/s
3
3
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
4
  import type { PgType } from "./entity-table-meta.ts";
5
5
  export type { ColumnHandle, SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
6
- export type TableColumns<_T = any> = SchemaTable;
7
- export type SelectQuery = any;
6
+ export type TableColumns<_T = unknown> = SchemaTable;
7
+ export type SelectQuery = unknown;
8
8
  export declare function pgTypeToSqlType(pgType: PgType): string;
9
9
  type ColumnFinal = {
10
10
  readonly sqlName: string;
@@ -15,7 +15,7 @@
15
15
  //
16
16
  // Static import of the shared Temporal module, not the ambient global: the
17
17
  // global may be absent (Hermes/Safari) or torn down in tests, and instantToDriver
18
- // crashed on timestamptz writes without it (#1480).
18
+ // crashed on timestamptz writes without it.
19
19
  import { KUMIKO_COLUMNS_SYMBOL, KUMIKO_META_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
20
20
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
21
  import { brandWritable, schemaTableName } from "./table-introspection.js";
@@ -190,7 +190,7 @@ export function instant(name, opts) {
190
190
  // PG `date` — no time-of-day, no timezone. Calendar-day fields (invoice
191
191
  // date, lease term) round-trip as Temporal.PlainDate, never through an
192
192
  // Instant — that detour is exactly what made `type:"date"` TZ-dependent on
193
- // both read and write (kumiko-framework#1924).
193
+ // both read and write.
194
194
  export function plainDate(name) {
195
195
  return buildColumn(name, "date");
196
196
  }
@@ -15,9 +15,9 @@ export type EagerLoadViewer = {
15
15
  readonly user: SessionUser;
16
16
  readonly parentVisibility: ParentVisibilityOption;
17
17
  };
18
- /** Eagerload für eine Liste von Rows. Mutiert nicht — gibt eine
19
- * flache Kopie der Rows mit hinzugefügtem `_refs`-Property zurück. */
18
+ /** Eagerloads a list of rows without mutating them; returns shallow copies
19
+ * with an added `_refs` property. */
20
20
  export declare function enrichWithReferences(rows: ReadonlyArray<Record<string, unknown>>, entity: EntityDefinition, resolveEntity: EagerLoadEntityResolver, db: TenantDb, viewer: EagerLoadViewer): Promise<Array<Record<string, unknown>>>;
21
- /** Single-Row-Variante für detail-Calls. */
21
+ /** Single-row variant for detail calls. */
22
22
  export declare function enrichRowWithReferences(row: Record<string, unknown>, entity: EntityDefinition, resolveEntity: EagerLoadEntityResolver, db: TenantDb, viewer: EagerLoadViewer): Promise<Record<string, unknown>>;
23
23
  export {};