@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,3 +1,4 @@
1
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  import { Hono } from "hono";
2
3
  import { bodyLimit } from "hono/body-limit";
3
4
  import { getUser } from "../api/auth-middleware.js";
@@ -18,6 +19,12 @@ import { RecordWriteDecisions } from "./record-write-check.js";
18
19
  import { neutralFileNameForMimeType } from "./resolve-content-type.js";
19
20
  import { parseMaxSize, resolveServedContentType } from "./types.js";
20
21
  import { createFileUploadLedger } from "./upload-ledger.js";
22
+ // Exactly the httpStatus values declared by KumikoError subclasses across packages/*.
23
+ const ERROR_STATUSES = [400, 401, 403, 404, 409, 422, 429, 500, 503, 504];
24
+ export function deleteFailureResponseFor(error) {
25
+ const status = isOneOf(ERROR_STATUSES, error.httpStatus) ? error.httpStatus : 500;
26
+ return { status, code: status === 404 ? "not_found" : error.code };
27
+ }
21
28
  const DEFAULT_PRIVILEGED_ROLES = ["Admin", "SystemAdmin"];
22
29
  // 15 minutes — long enough for a download to start, short enough that a
23
30
  // leaked URL (e.g. from a browser history screenshot) isn't a long-lived
@@ -281,7 +288,7 @@ export function createFileRoutes(options) {
281
288
  // GET /files/:id/variant/:name — a named derived version of the file.
282
289
  // Same tenant + guard gate as the download above; the original stays
283
290
  // reachable only through that route. The public, anonymous counterpart is
284
- // a separate cut (#1951) and does not go through createFileRoutes.
291
+ // a separate cut and does not go through createFileRoutes.
285
292
  api.get("/files/:id/variant/:name", async (c) => {
286
293
  const user = getUser(c);
287
294
  const id = c.req.param("id");
@@ -358,9 +365,8 @@ export function createFileRoutes(options) {
358
365
  // pattern from the access-deny path above. Everything else (version
359
366
  // conflict 409, ownership 403, validation 422, internal 500) gets the
360
367
  // real status off the error so callers can distinguish recoverable
361
- // from terminal — pauschales 404 hier hat genau das verschleiert.
362
- const status = result.error.httpStatus;
363
- const code = status === 404 ? "not_found" : result.error.code;
368
+ // from terminal — a blanket 404 here had obscured exactly that.
369
+ const { status, code } = deleteFailureResponseFor(result.error);
364
370
  return c.json({ error: code }, status);
365
371
  }
366
372
  return c.json({ ok: true });
@@ -411,7 +417,7 @@ export function createFileRoutes(options) {
411
417
  return c.json({ url, expiresAt });
412
418
  });
413
419
  // Metadata without the bytes. Unlike the byte-serving routes, this returns
414
- // fileName as JSON — and fileName is `personal: "self"` (fw#2423), so it must
420
+ // fileName as JSON — and fileName is `personal: "self"`, so it must
415
421
  // go through the entity executor's detail() to decrypt instead of
416
422
  // loadFileForTenant's raw, still-encrypted read.
417
423
  async function loadReadableFileMeta(id, user) {
@@ -12,9 +12,8 @@ export function createInMemoryFileProvider() {
12
12
  store.set(key, { data: new Uint8Array(data), mimeType });
13
13
  },
14
14
  async writeStream(key, source, options) {
15
- // In-Memory hat kein "Streaming" im real-physikalischen Sinn —
16
- // wir collecten chunks in einen Uint8Array. Test-Tauglich, kein
17
- // Production-Pfad.
15
+ // In-memory storage does not provide real streaming; it collects chunks
16
+ // into a Uint8Array. Suitable for tests, not a production path.
18
17
  const chunks = [];
19
18
  let total = 0;
20
19
  for await (const chunk of source) {
@@ -36,12 +35,12 @@ export function createInMemoryFileProvider() {
36
35
  return new Uint8Array(entry.data);
37
36
  },
38
37
  readStream(key) {
39
- // In-Memory hat technisch keine Chunks, aber das Surface muss
40
- // identisch zu echten Streamern (Local/S3) sein damit Test-Code
41
- // den Pfad genauso geht. Wir yielden die Bytes als single-chunk.
42
- // Map-lookup ist O(1), also kein Verlust durch eager-resolve.
43
- // Throw passiert beim ersten chunk-pull — gleicher Lazy-Pattern
44
- // wie S3 (request abgesetzt beim ersten Iterator-Step).
38
+ // In-memory storage has no chunks, but the surface must match real
39
+ // streamers (Local/S3) so test code exercises the same path. Yield the
40
+ // bytes as a single chunk.
41
+ // The O(1) map lookup avoids a cost from eager resolution.
42
+ // Errors are thrown on the first chunk pull, matching S3's lazy pattern
43
+ // (the request starts on the first iterator step).
45
44
  const entry = store.get(key);
46
45
  const captured = entry ? new Uint8Array(entry.data) : null;
47
46
  return {
@@ -33,16 +33,12 @@ export function createLocalProvider(basePath) {
33
33
  await writeFile(filePath, data);
34
34
  },
35
35
  async writeStream(key, source, _options) {
36
- // Atomar via tmp-File + rename: ein Reader der den finalen Pfad
37
- // sieht, sieht entweder die alte Version (falls vorhanden) oder
38
- // die vollstaendige neue. Niemals einen halb-fertigen Stream.
39
- // Falls der Stream mid-write bricht, ist das tmp-Cleanup
40
- // best-effort — je nach OS-Race im stream-destroy-Pfad kann
41
- // das `.tmp`-File kurz liegen bleiben. Kein Korrektheits-
42
- // Problem (kein Reader sucht `*.tmp`-Patterns), nur Operations-
43
- // Hygiene; ein periodischer cron-cleanup auf alten `.tmp`-Files
44
- // ist die saubere Loesung wenn das in Production realistisch
45
- // greift.
36
+ // Atomic via a temporary file and rename: readers see either the previous version (if any) or
37
+ // the complete new version, never a partially written stream. If the stream fails during
38
+ // writing, temporary-file cleanup is best-effort; an OS race in stream destruction can leave
39
+ // the `.tmp` file briefly. This affects operational hygiene only, because readers never
40
+ // search `*.tmp` patterns. A periodic cleanup of old `.tmp` files is appropriate if this runs
41
+ // in production.
46
42
  const filePath = resolveContainedPath(key);
47
43
  const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
48
44
  await mkdir(dirname(filePath), { recursive: true });
@@ -51,8 +47,8 @@ export function createLocalProvider(basePath) {
51
47
  await rename(tmpPath, filePath);
52
48
  }
53
49
  catch (e) {
54
- // Best-effort tmp-Cleanup; wenn das auch failt, hat der
55
- // Filesystem ein Problem das nicht in unserem Scope liegt.
50
+ // Best-effort temporary-file cleanup; if that also fails, the filesystem problem is outside
51
+ // this component's scope.
56
52
  await unlink(tmpPath).catch(() => { });
57
53
  throw e;
58
54
  }
@@ -62,24 +58,21 @@ export function createLocalProvider(basePath) {
62
58
  return readFile(filePath);
63
59
  },
64
60
  readStream(key) {
65
- // node:fs createReadStream ist ein AsyncIterable<Buffer>; Buffer
66
- // extends Uint8Array. Wir bauen einen kleinen Adapter weil
67
- // @types/node das asyncIterator als AsyncIterableIterator<any>
68
- // typt — Adapter sichert die Surface auf Uint8Array.
69
- // Default-highWaterMark = 64KB Chunks, was fuer ZIP-Stream-Konsum
70
- // gut ist. Errors landen im for-await-Loop des Konsumenten
71
- // (z.B. ENOENT bei Missing-File faellt erst beim ersten chunk-
72
- // pull, nicht beim readStream-Aufruf — gleiches Lazy-Verhalten
73
- // wie inmemory + S3).
61
+ // node:fs createReadStream is an AsyncIterable<Buffer>; Buffer extends Uint8Array. This
62
+ // adapter preserves a Uint8Array surface because @types/node types the async iterator as
63
+ // AsyncIterableIterator<any>. The default highWaterMark produces 64KB chunks, which suits ZIP
64
+ // stream consumption. Errors reach the consumer's for-await loop; for example, a missing file
65
+ // raises ENOENT on the first chunk pull rather than when readStream is called, matching the
66
+ // lazy behavior of in-memory and S3 providers.
74
67
  const filePath = resolveContainedPath(key);
75
68
  const stream = createReadStream(filePath);
76
69
  return {
77
70
  async *[Symbol.asyncIterator]() {
78
71
  for await (const chunk of stream) {
79
- // Stream ohne encoding liefert Buffer. Buffer extends Uint8Array,
80
- // aber @types/node typt asyncIterator als string|Buffer. View
81
- // ohne copy auf dasselbe ArrayBuffer; runtime-check schliesst
82
- // den string-Branch aus (Stream wurde nicht mit encoding= gesetzt).
72
+ // A stream without an encoding yields Buffer. Buffer extends Uint8Array, but
73
+ // @types/node types the async iterator as string|Buffer. Take a zero-copy view
74
+ // onto the same ArrayBuffer; the runtime check rules out the string branch
75
+ // because the stream was not opened with an encoding.
83
76
  if (typeof chunk === "string") {
84
77
  throw new Error("local-provider readStream: unexpected string chunk (encoding leaked)");
85
78
  }
@@ -36,13 +36,13 @@ export type FileProviderContext = {
36
36
  * file-provider-inmemory, ...) registers an implementation via
37
37
  * `r.useExtension(EXT_FILE_PROVIDER, "<name>", { build })`.
38
38
  *
39
- * **Plugin-Author-Warnung:** `ctx` ist EXPLIZIT ein FileProviderContext, nicht
40
- * ein voller HandlerContext. Felder ausserhalb der schmalen Surface (z.B.
41
- * `ctx.tx`, `ctx.actor`, `ctx.signal`) sind im Worker-Pfad NICHT vorhanden.
42
- * Cast `ctx as unknown as HandlerContext` fliegt zur Runtime im Worker — der
43
- * Crash kommt erst in production mit dem ersten S3-Tenant. Braucht ein Plugin
44
- * Felder ausserhalb FileProviderContext: lieber FileProviderContext explizit
45
- * erweitern (sichtbarer breaking change) als ctx-cast.
39
+ * **Plugin-Author-Warning:** `ctx` is EXPLICITLY a FileProviderContext, not
40
+ * a full HandlerContext. Fields outside the narrow surface (e.g.
41
+ * `ctx.tx`, `ctx.actor`, `ctx.signal`) are NOT available on the worker path.
42
+ * Casting `ctx as unknown as HandlerContext` fails at runtime on the worker —
43
+ * the crash only appears in production with the first S3 tenant. If a plugin
44
+ * needs fields outside FileProviderContext, extend FileProviderContext
45
+ * explicitly (a visible breaking change) rather than casting `ctx`.
46
46
  */
47
47
  export type FileProviderPlugin = {
48
48
  readonly build: (ctx: FileProviderContext, tenantId: string) => Promise<FileStorageProvider>;
@@ -1,7 +1,7 @@
1
1
  // Tenant storage usage — counts bytes + files per tenant from the event log.
2
2
  //
3
- // Tracking-only for Phase 1: no hard limit, no upload gatekeeping. Apps read
4
- // the row to decide what to do (show a warning, soft-throttle, bill, …).
3
+ // Tracking-only: no hard limit, no upload gatekeeping. Apps read the row to
4
+ // decide what to do (show a warning, soft-throttle, bill, …).
5
5
  // Enforcement is a conscious deferred call — we want production numbers
6
6
  // before picking thresholds (see "Storage tracking: counted now, enforced
7
7
  // later" in core-files.md).
@@ -1,31 +1,24 @@
1
1
  import { getTemporal } from "../time/index.ts";
2
2
  export interface ZipEntry {
3
- /** Pfad im ZIP, slash-separated. ASCII (kein UTF-8-Flag gesetzt). */
3
+ /** Path inside the ZIP, slash-separated and UTF-8 encoded (general-purpose flag bit 11). */
4
4
  readonly path: string;
5
- /** Body als AsyncIterable — kann lazy generated sein (kein Upfront-Memory). */
5
+ /** Body as an AsyncIterable; it may be generated lazily to avoid upfront memory use. */
6
6
  readonly data: AsyncIterable<Uint8Array>;
7
7
  /**
8
- * Optional Modification-Time. Default = Now. ZIP nutzt MS-DOS-Format
9
- * (2-Sek-Aufloesung). Wer Audit-Trail-Zeitpunkte haben will, setzt
10
- * den Generation-Timestamp ueber alle Entries.
8
+ * Optional modification time. Defaults to the current time and uses the ZIP
9
+ * MS-DOS format with two-second precision. Set the generation timestamp on
10
+ * every entry when consistent audit-trail timestamps are required.
11
11
  */
12
12
  readonly mtime?: InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
13
13
  }
14
14
  /**
15
- * Erzeugt einen ZIP-Stream als `AsyncIterable<Uint8Array>`. Caller (z.B.
16
- * `FileStorageProvider.writeStream`) konsumiert chunk-fuer-chunk, kein
17
- * Entry haengt im Memory bis fertig.
15
+ * Creates a ZIP stream as `AsyncIterable<Uint8Array>`. The caller, such as
16
+ * `FileStorageProvider.writeStream`, consumes the output chunk by chunk.
18
17
  *
19
- * Streaming-Lifecycle pro Entry:
20
- * 1. Local File Header (mit CRC32+size=0 als Placeholder; wir nutzen
21
- * KEINE Streaming-Data-Descriptor weil Storage-Provider Random-
22
- * Access nicht garantiert — wir collecten den Body in Memory pro
23
- * Entry um CRC + size **vor** dem Header zu kennen.
24
- * Trade-off: 1 Entry-Body im Memory at-a-time; bei <100 MB Files
25
- * OK, bei >1 GB single-Files braeuchte ZIP64+Streaming-Descriptor).
26
- * 2. File Body (raw bytes)
27
- * Am Ende:
28
- * 3. Central Directory (eine Liste-Entry pro File mit CRC + size)
29
- * 4. EOCD (End Of Central Directory)
18
+ * Each entry's body is collected in memory before its local file header is
19
+ * emitted because non-seekable storage providers cannot use a streaming data
20
+ * descriptor, and the header must contain the CRC32 and size. This limits
21
+ * memory use to one entry body at a time; ZIP64 plus streaming descriptors
22
+ * would be needed for larger single files.
30
23
  */
31
24
  export declare function createZipStream(entries: AsyncIterable<ZipEntry>): AsyncIterable<Uint8Array>;
@@ -1,27 +1,22 @@
1
1
  import { getTemporal } from "../time/index.js";
2
- // Streaming-ZIP-Builder (S2.U3 Atom 3a) — pure-JS, dependency-frei.
2
+ // Pure-JS streaming ZIP builder without dependencies.
3
3
  //
4
- // **Format-Wahl: STORE (kein DEFLATE).** Compression-frei + minimal —
5
- // fuer User-Data-Export-Bundles akzeptabel weil:
6
- // - Storage-Provider (S3/R2) komprimiert eh on-the-wire (HTTP gzip)
7
- // - JSON-Snippets sind klein, File-Binaries sind oft schon komprimiert
8
- // (PNG/JPG/PDF/MP4/...)
9
- // - DEFLATE braucht zlib + buffering pro Entry, killt das Streaming-
10
- // Property
4
+ // **Format: STORE (no DEFLATE).** Avoiding compression keeps the implementation minimal:
5
+ // - Storage providers (S3/R2) compress data on the wire (HTTP gzip).
6
+ // - JSON snippets are small, and file binaries are often already compressed
7
+ // (PNG/JPG/PDF/MP4/...).
8
+ // - DEFLATE requires zlib and per-entry buffering, which defeats streaming.
11
9
  //
12
- // **Streaming-Property:** Caller gibt `AsyncIterable<ZipEntry>`, jeder
13
- // Entry hat `data: AsyncIterable<Uint8Array>`. ZIP-Bytes werden
14
- // chunk-fuer-chunk yieldet — kein Entry haengt im Memory bis er fertig
15
- // ist. Storage-Provider's writeStream konsumiert direkt durch.
10
+ // **Streaming:** The caller provides `AsyncIterable<ZipEntry>`, and each entry
11
+ // has `data: AsyncIterable<Uint8Array>`. ZIP bytes are yielded chunk by chunk.
12
+ // The storage provider's writeStream consumes them directly.
16
13
  //
17
- // **ZIP-Standard:** APPNOTE.TXT v6.3.10 (April 2022). STORE-Method,
18
- // kein ZIP64 (max 4 GB pro Entry, max 65535 Entries — fuer User-Daten-
19
- // Exports ausreichend; ZIP64-Support kommt wenn ein realer User mit
20
- // >4 GB single-File auftaucht). Kein UTF-8-Flag (filenames sind ASCII
21
- // in unserem Use-Case: "profile.json", "files/<id>.<ext>").
14
+ // **ZIP standard:** APPNOTE.TXT v6.3.10 (April 2022). STORE method, no ZIP64
15
+ // (maximum 4 GB per entry and 65535 entries). ZIP64 support is needed before
16
+ // exporting a single file larger than 4 GB. The UTF-8 flag is enabled because
17
+ // filenames may contain non-ASCII characters.
22
18
  //
23
- // **CRC32:** klassisches IEEE-802.3-Polynom (0xEDB88320). Lookup-Table
24
- // einmal initialisiert + cached.
19
+ // **CRC32:** Classic IEEE-802.3 polynomial (0xEDB88320), with a cached lookup table.
25
20
  const ZIP_VERSION_NEEDED = 20; // 2.0 = STORE
26
21
  const ZIP_METHOD_STORE = 0;
27
22
  // Local file header signature: 0x04034b50 = "PK\x03\x04"
@@ -31,50 +26,38 @@ const CENTRAL_DIR_HEADER_SIG = 0x02014b50;
31
26
  // End of central directory record signature: 0x06054b50 = "PK\x05\x06"
32
27
  const EOCD_SIG = 0x06054b50;
33
28
  const VERSION_MADE_BY = 0x031e; // 0x03 = UNIX, 0x1e = ZIP 3.0
34
- // General Purpose Flag Bit 11 (0x0800) = UTF-8 filename + comment encoding.
35
- // Wird default-on gesetzt: ASCII ist gueltiges UTF-8, also Win-Win.
36
- // Ohne Flag interpretieren aeltere ZIP-Tools filenames als CP437 — bei
37
- // Umlauten ("Bügel.pdf") wird das Mojibake.
29
+ // General Purpose Flag Bit 11 (0x0800) enables UTF-8 encoding for filenames and comments.
30
+ // ASCII is valid UTF-8, while omitting the flag makes older ZIP tools interpret
31
+ // filenames as CP437, producing mojibake for non-ASCII characters such as umlauts.
38
32
  const GENERAL_PURPOSE_FLAGS = 0x0800;
39
- // External-Attrs (UNIX-Mode in High-Bytes) fuer regulaere Dateien mit
40
- // 0644-Permissions: `(S_IFREG | 0644) << 16 = 0o100644 << 16 = 0x81a40000`.
41
- // S_IFREG = 0o100000 (regular file), 0o644 = rw-r--r--. Ohne diese
42
- // Bits entpackt Info-ZIP mit Mode 0000 (EACCES beim Read).
33
+ // Store UNIX file permissions in the high bytes of the external attributes.
34
+ // Without `(S_IFREG | 0644) << 16 = 0o100644 << 16 = 0x81a40000`, Info-ZIP
35
+ // extracts regular files with mode 0000, causing EACCES when they are read.
43
36
  const UNIX_REGULAR_FILE_0644 = 0o100644 << 16; // 0x81a40000
44
- // ZIP-Format-Limits ohne ZIP64-Extension. Atom 3a-Scope ist STORE-only;
45
- // ZIP64 kommt wenn ein realer User-Export diese Caps reisst.
37
+ // STORE without ZIP64 is limited to 4 GB per entry and 65535 entries.
38
+ // ZIP64 support is required before these limits can be exceeded.
46
39
  const ZIP_MAX_ENTRY_SIZE = 0xffffffff; // uint32 → 4 GB
47
40
  const ZIP_MAX_ENTRIES = 0xffff; // uint16 → 65535
48
41
  /**
49
- * Erzeugt einen ZIP-Stream als `AsyncIterable<Uint8Array>`. Caller (z.B.
50
- * `FileStorageProvider.writeStream`) konsumiert chunk-fuer-chunk, kein
51
- * Entry haengt im Memory bis fertig.
42
+ * Creates a ZIP stream as `AsyncIterable<Uint8Array>`. The caller, such as
43
+ * `FileStorageProvider.writeStream`, consumes the output chunk by chunk.
52
44
  *
53
- * Streaming-Lifecycle pro Entry:
54
- * 1. Local File Header (mit CRC32+size=0 als Placeholder; wir nutzen
55
- * KEINE Streaming-Data-Descriptor weil Storage-Provider Random-
56
- * Access nicht garantiert — wir collecten den Body in Memory pro
57
- * Entry um CRC + size **vor** dem Header zu kennen.
58
- * Trade-off: 1 Entry-Body im Memory at-a-time; bei <100 MB Files
59
- * OK, bei >1 GB single-Files braeuchte ZIP64+Streaming-Descriptor).
60
- * 2. File Body (raw bytes)
61
- * Am Ende:
62
- * 3. Central Directory (eine Liste-Entry pro File mit CRC + size)
63
- * 4. EOCD (End Of Central Directory)
45
+ * Each entry's body is collected in memory before its local file header is
46
+ * emitted because non-seekable storage providers cannot use a streaming data
47
+ * descriptor, and the header must contain the CRC32 and size. This limits
48
+ * memory use to one entry body at a time; ZIP64 plus streaming descriptors
49
+ * would be needed for larger single files.
64
50
  */
65
51
  export async function* createZipStream(entries) {
66
52
  const centralDirRecords = [];
67
- let offset = 0; // Bytes-Counter fuer central-dir entry's local-header-offset
53
+ let offset = 0; // Tracks the local-header offset for each central-directory entry
68
54
  for await (const entry of entries) {
69
- // Body in Memory collecten — wir brauchen CRC32 + size VOR dem
70
- // local-file-header (kein streaming-data-descriptor weil
71
- // file-storage-providers nicht zwingend seek-able sind, aber wir
72
- // wollen vermeiden dass der konsumierende Storage zwei-pass laesst).
55
+ // Collect the body before writing the local file header: non-seekable storage
56
+ // providers cannot use a streaming data descriptor, and a two-pass read
57
+ // would be required otherwise.
73
58
  const body = await collectBody(entry.data);
74
- // Hard-Limit: STORE-Mode ohne ZIP64-Extension cappt bei uint32 (4 GB).
75
- // Bei Ueberschreitung wuerde der Integer-Wrap silent ein korruptes
76
- // ZIP produzieren (Decoder lesen Muell-Sizes). Lieber early-fail mit
77
- // klarer Begruendung — Worker (Atom 3b) fängt das + setzt Job=failed.
59
+ // STORE without ZIP64 uses uint32 sizes. Fail early instead of allowing integer
60
+ // wraparound to produce a corrupt ZIP with incorrect sizes.
78
61
  if (body.byteLength > ZIP_MAX_ENTRY_SIZE) {
79
62
  throw new Error(`ZIP-Entry "${entry.path}" exceeds 4 GB limit (${body.byteLength} bytes). ` +
80
63
  `STORE-mode without ZIP64-extension caps at uint32. ` +
@@ -105,8 +88,7 @@ export async function* createZipStream(entries) {
105
88
  lfh.set(filenameBytes, 30);
106
89
  yield lfh;
107
90
  yield body;
108
- // Central-Directory-Eintrag fuer diesen Entry — wir collecten ihn,
109
- // emittieren ihn am Ende.
91
+ // Keep central-directory entries until all file bodies have been emitted.
110
92
  const cdh = new Uint8Array(46 + filenameBytes.byteLength);
111
93
  const cdhView = new DataView(cdh.buffer);
112
94
  cdhView.setUint32(0, CENTRAL_DIR_HEADER_SIG, true);
@@ -130,7 +112,7 @@ export async function* createZipStream(entries) {
130
112
  centralDirRecords.push(cdh);
131
113
  offset += lfh.byteLength + body.byteLength;
132
114
  }
133
- // Central Directory: alle entries hintereinander
115
+ // Central directory: all entries back to back.
134
116
  const centralDirStart = offset;
135
117
  let centralDirSize = 0;
136
118
  for (const record of centralDirRecords) {
@@ -189,15 +171,14 @@ function crc32(data) {
189
171
  }
190
172
  return (crc ^ 0xffffffff) >>> 0;
191
173
  }
192
- // MS-DOS Date+Time encoding fuer ZIP-Header.
193
- // Date: bits 9-15=year-1980, 5-8=month, 0-4=day.
194
- // Time: bits 11-15=hour, 5-10=minute, 0-4=second/2.
174
+ // MS-DOS date and time encoding for ZIP headers.
175
+ // Date: bits 9-15 = year - 1980, 5-8 = month, 0-4 = day.
176
+ // Time: bits 11-15 = hour, 5-10 = minute, 0-4 = second / 2.
195
177
  //
196
- // **UTC** statt lokaler Zeitzone: in einem DSGVO-Kontext ist der
197
- // ZIP-mtime Teil des Auskunfts-Artefakts. Verschiedene Server-Zeitzonen
198
- // wuerden sonst verschiedene mtime-Werte fuer denselben Generation-
199
- // Instant produzieren — Audit-Drift. UTC ist der Standard fuer alle
200
- // server-side-Timestamps im Repo (Temporal.Instant ueberall).
178
+ // **UTC** instead of the local time zone: in a GDPR context the ZIP mtime is
179
+ // part of the data-access export. Different server time zones would otherwise produce
180
+ // different values for the same generation instant, causing audit drift. UTC is
181
+ // used for server-side timestamps throughout the repository.
201
182
  function toDosTime(i) {
202
183
  const dt = i.toZonedDateTimeISO("UTC");
203
184
  const year = dt.year;
@@ -16,7 +16,7 @@ export declare function booleanFacetOptionKeys(featureName: string, entityName:
16
16
  export declare function selectFacetOptionKey(featureName: string, entityName: string, fieldName: string, value: string): string;
17
17
  export declare function screenTitleKey(screenId: string): string;
18
18
  export type RequiredKeysOptions = {
19
- /** Bypass `isI18nKey`'s colon-only check for generated dot-form labels (fw#2260). */
19
+ /** Bypass `isI18nKey`'s colon-only check for generated dot-form labels. */
20
20
  readonly treatDotFormAsKey?: boolean;
21
21
  };
22
22
  export declare function requiredKeysFromScreen(featureName: string, screen: ScreenDefinition, options?: RequiredKeysOptions): readonly string[];
@@ -35,8 +35,8 @@ function isI18nKey(value) {
35
35
  /**
36
36
  * `treatAsKey` bypasses the colon-only `isI18nKey` check — used by the Settings-Hub
37
37
  * generator's dot-form labels (`${feature}.settings`, mask titles), which are
38
- * always i18n references by construction, never literal display text (fw#2260).
39
- * `isI18nKey` itself also accepts dot-form labels explicitly marked via `i18nKey()` (fw#2313).
38
+ * always i18n references by construction, never literal display text.
39
+ * `isI18nKey` itself also accepts dot-form labels explicitly marked via `i18nKey()`.
40
40
  */
41
41
  function pushKey(out, value, treatAsKey = false) {
42
42
  if (value !== undefined && (treatAsKey || isI18nKey(value)))
@@ -112,7 +112,7 @@ function pushDashboardScreenKeys(out, dashboard) {
112
112
  pushRowActionKeys(out, action);
113
113
  }
114
114
  function pushDashboardPanelKeys(out, panel) {
115
- // skip: custom-Panel übersetzt sich selbst, kein Key hier
115
+ // skip: custom panels translate themselves, so no key is collected here
116
116
  if (panel.kind === "custom")
117
117
  return;
118
118
  pushKey(out, panel.label);
@@ -251,7 +251,8 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
251
251
  }
252
252
  case "projectionList": {
253
253
  const list = screen;
254
- // Keine Entity → keine field-label-Fallbacks; nur explizite Column-Labels.
254
+ // No entity means there are no field-label fallbacks; only explicit column labels are
255
+ // collected.
255
256
  for (const col of list.columns) {
256
257
  const normalized = normalizeListColumn(col);
257
258
  if (normalized.label !== undefined)
@@ -454,9 +455,8 @@ function collectEntityListFilterKeys(feature, out) {
454
455
  const entity = feature.entities?.[screen.entity];
455
456
  if (!entity)
456
457
  continue;
457
- for (const [fieldName, rawDef] of Object.entries(entity.fields)) {
458
- const def = rawDef;
459
- if (def.filterable !== true)
458
+ for (const [fieldName, def] of Object.entries(entity.fields)) {
459
+ if (!("filterable" in def) || def.filterable !== true)
460
460
  continue;
461
461
  if (def.type === "boolean") {
462
462
  for (const key of booleanFacetOptionKeys(feature.name, screen.entity, fieldName)) {
@@ -513,7 +513,7 @@ export function buildEffectiveTranslationKeys(features) {
513
513
  for (const key of Object.keys(feature.translations ?? {})) {
514
514
  out.add(`${feature.name}:${key}`);
515
515
  // Blank form too — Settings-Hub group namespaces (`group: "tenant-settings"`)
516
- // require `${group}.settings` which is not `${feature}:${key}` (fw#2314).
516
+ // require `${group}.settings` which is not `${feature}:${key}`.
517
517
  out.add(key);
518
518
  }
519
519
  }
@@ -1,5 +1,5 @@
1
1
  export { createDbConnection } from "../db/connection.ts";
2
- export { createDatabase, databaseExists, dropDatabaseIfExists, truncateTablesRestartIdentity, } from "../db/queries/test-stack.ts";
2
+ export { acquireSessionAdvisoryLock, createDatabase, databaseExists, dropDatabaseIfExists, listDatabaseNamesWithPrefix, renameDatabase, truncateTablesRestartIdentity, } from "../db/queries/test-stack.ts";
3
3
  export { type AnyDb, isClosedConnectionError } from "../db/query.ts";
4
4
  export { columnNamesOf } from "../db/schema-inspection.ts";
5
5
  export { schemaTableName } from "../db/table-introspection.ts";
@@ -1,6 +1,6 @@
1
1
  // @runtime dev
2
2
  export { createDbConnection } from "../db/connection.js";
3
- export { createDatabase, databaseExists, dropDatabaseIfExists, truncateTablesRestartIdentity, } from "../db/queries/test-stack.js";
3
+ export { acquireSessionAdvisoryLock, createDatabase, databaseExists, dropDatabaseIfExists, listDatabaseNamesWithPrefix, renameDatabase, truncateTablesRestartIdentity, } from "../db/queries/test-stack.js";
4
4
  export { isClosedConnectionError } from "../db/query.js";
5
5
  export { columnNamesOf } from "../db/schema-inspection.js";
6
6
  export { schemaTableName } from "../db/table-introspection.js";
@@ -3,7 +3,7 @@ import type { ConfigAccessor } from "@cosmicdrift/kumiko-types/config";
3
3
  import type { TzContext } from "@cosmicdrift/kumiko-types/tz-context";
4
4
  import type { JobsOptions, Queue } from "bullmq";
5
5
  import type { TenantDb } from "../db/tenant-db.ts";
6
- import type { AppContext, EscapeHatchReporter, JobContext, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
6
+ import type { AppContext, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
7
7
  import type { Logger } from "../logging/types.ts";
8
8
  import type { Meter, SerializedTraceContext, Tracer } from "../observability/index.ts";
9
9
  import type { JobLogEntry, JobSubjectValue } from "./job-runner-types.ts";
@@ -14,7 +14,6 @@ export declare function serializeJobSubject(subject: Readonly<Record<string, Job
14
14
  export declare function jobSubjectKey(jobName: string, payload: Record<string, unknown>, fields: readonly string[] | undefined, declaration: "tenantVisibleFailure" | "tenantVisibleRun"): string | null;
15
15
  export declare function resolveJobTz(config: ConfigAccessor | undefined, context: Pick<AppContext, "geoTzProvider" | "clock">): Promise<TzContext>;
16
16
  export declare function missingDbProxy(jobName: string): TenantDb;
17
- export declare function createCrossTenantReads(systemModeDb: TenantDb, reason: string, report: EscapeHatchReporter): NonNullable<JobContext["crossTenantReads"]>;
18
17
  export declare function priorAttemptsOf(data: Record<string, unknown>): number;
19
18
  export declare function tenantFailureMessageKey(err: unknown, declaration: JobDefinition["tenantVisibleFailure"]): string | null;
20
19
  export declare const TRACE_CONTEXT_KEY = "_traceContext";
@@ -66,6 +66,12 @@ export function serializeJobSubject(subject) {
66
66
  .sort()
67
67
  .map((field) => [field, subject[field] ?? null]));
68
68
  }
69
+ function isJobSubjectValue(value) {
70
+ return (value === null ||
71
+ typeof value === "string" ||
72
+ typeof value === "number" ||
73
+ typeof value === "boolean");
74
+ }
69
75
  // Stable identity for the declared payload fields: sorted keys, so two runs
70
76
  // with the same subject values produce the same string and the later one
71
77
  // replaces the earlier record. Non-primitives would serialize into something
@@ -76,7 +82,7 @@ export function jobSubjectKey(jobName, payload, fields, declaration) {
76
82
  const subject = {};
77
83
  for (const field of fields) {
78
84
  const value = payload[field] ?? null;
79
- if (value !== null && typeof value === "object") {
85
+ if (!isJobSubjectValue(value)) {
80
86
  throw new Error(`Job "${jobName}": ${declaration}.subjectFields["${field}"] must be a primitive, got ${typeof value}`);
81
87
  }
82
88
  subject[field] = value;
@@ -103,22 +109,6 @@ export function missingDbProxy(jobName) {
103
109
  },
104
110
  });
105
111
  }
106
- export function createCrossTenantReads(systemModeDb, reason, report) {
107
- return {
108
- selectMany(table, where, options) {
109
- report("cross-tenant-read", reason);
110
- return systemModeDb.selectMany(table, where, options);
111
- },
112
- fetchOne(table, where) {
113
- report("cross-tenant-read", reason);
114
- return systemModeDb.fetchOne(table, where);
115
- },
116
- count(table, where) {
117
- report("cross-tenant-read", reason);
118
- return systemModeDb.count(table, where);
119
- },
120
- };
121
- }
122
112
  export function priorAttemptsOf(data) {
123
113
  const value = data["_priorAttempts"];
124
114
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 0;
@@ -266,7 +256,7 @@ export function payloadMatchesTriggerFilter(where, payload) {
266
256
  // on the children still existing in Redis when a wrapper retry re-derives
267
257
  // their ids. The margin here is generous — queue wait time isn't bounded by
268
258
  // backoff — to cover realistic windows. Audit trail lives in read_job_runs,
269
- // not BullMQ (fw#3199).
259
+ // not BullMQ.
270
260
  export const COMPLETED_JOB_RETENTION_AGE_SEC = 86_400; // 24h
271
261
  export const FAILED_JOB_RETENTION_AGE_SEC = 604_800; // 7d
272
262
  // Total wall-clock time BullMQ can hold a perTenant wrapper across all its
@@ -7,7 +7,7 @@ export declare const ACTIVE_TENANT_IDS_QUERY_NAME = "tenant:query:active-tenant-
7
7
  * BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
8
8
  * scheduler id push the segment count to ≥5, which BullMQ's legacy heuristic
9
9
  * treated as old repeatables — spawning a new scheduler entry every tick and
10
- * leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828, fw#1603 /
10
+ * leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
11
11
  * publicstatus Redis OOM). Strip `.` and `:` from the job QN.
12
12
  */
13
13
  export declare function schedulerIdForJobName(jobName: string): string;
@@ -19,13 +19,13 @@ export const ACTIVE_TENANT_IDS_QUERY_NAME = "tenant:query:active-tenant-ids";
19
19
  * BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
20
20
  * scheduler id push the segment count to ≥5, which BullMQ's legacy heuristic
21
21
  * treated as old repeatables — spawning a new scheduler entry every tick and
22
- * leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828, fw#1603 /
22
+ * leaking permanent `repeat:*` hashes (taskforcesh/bullmq#3828,
23
23
  * publicstatus Redis OOM). Strip `.` and `:` from the job QN.
24
24
  */
25
25
  export function schedulerIdForJobName(jobName) {
26
26
  return `scheduler-${jobName.replace(/[.:]/g, "-")}`;
27
27
  }
28
- // Same colon-in-BullMQ-id hazard as schedulerIdForJobName (fw#1603/#1604) —
28
+ // Same colon-in-BullMQ-id hazard as schedulerIdForJobName —
29
29
  // a QN like "publicstatus:job:uptime-probe" must not leave ":" in the id.
30
30
  export function bootJobIdForJobName(jobName) {
31
31
  return `boot-${jobName.replace(/[.:]/g, "-")}`;
@@ -97,8 +97,7 @@ export async function removeOrphanJobSchedulers(queue, expectedSchedulerIds, lan
97
97
  // duplicates — safe only as long as the child job hash outlives the
98
98
  // wrapper's retry window, enforced at construction against
99
99
  // COMPLETED_JOB_RETENTION_AGE_SEC (see the invariant check in
100
- // createJobRunner). Same colon-in-BullMQ-id hazard as schedulerIdForJobName
101
- // (fw#1603/#1604) — the wrapper id already contains ":", so strip
100
+ // createJobRunner). The wrapper id already contains ":", so strip
102
101
  // separators from both halves before joining instead of interpolating raw.
103
102
  export function perTenantChildJobId(wrapperJobId, tenantId) {
104
103
  const sanitize = (value) => value.replace(/[.:]/g, "-");