@cosmicdrift/kumiko-framework 0.2.2 → 0.3.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 (191) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +124 -38
  3. package/src/__tests__/full-stack.integration.ts +2 -2
  4. package/src/api/auth-routes.ts +5 -5
  5. package/src/api/jwt.ts +2 -2
  6. package/src/api/route-registrars.ts +1 -1
  7. package/src/api/routes.ts +3 -3
  8. package/src/api/server.ts +6 -7
  9. package/src/auth/__tests__/roles.test.ts +24 -0
  10. package/src/auth/index.ts +7 -0
  11. package/src/auth/roles.ts +42 -0
  12. package/src/compliance/__tests__/duration-spec.test.ts +72 -0
  13. package/src/compliance/__tests__/profiles.test.ts +308 -0
  14. package/src/compliance/__tests__/sub-processors.test.ts +139 -0
  15. package/src/compliance/duration-spec.ts +44 -0
  16. package/src/compliance/index.ts +31 -0
  17. package/src/compliance/override-schema.ts +136 -0
  18. package/src/compliance/profiles.ts +427 -0
  19. package/src/compliance/sub-processors.ts +152 -0
  20. package/src/db/__tests__/big-int-field.test.ts +131 -0
  21. package/src/db/assert-exists-in.ts +2 -2
  22. package/src/db/cursor.ts +3 -3
  23. package/src/db/event-store-executor.ts +19 -13
  24. package/src/db/located-timestamp.ts +1 -1
  25. package/src/db/money.ts +12 -2
  26. package/src/db/pg-error.ts +1 -1
  27. package/src/db/row-helpers.ts +1 -1
  28. package/src/db/table-builder.ts +20 -5
  29. package/src/db/tenant-db.ts +9 -9
  30. package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
  31. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
  32. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
  33. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
  34. package/src/engine/__tests__/build-target.test.ts +135 -0
  35. package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
  36. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  37. package/src/engine/__tests__/event-helpers.test.ts +4 -4
  38. package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
  39. package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
  40. package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
  41. package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
  42. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
  43. package/src/engine/__tests__/raw-table.test.ts +2 -2
  44. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
  45. package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
  46. package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
  47. package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
  48. package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
  49. package/src/engine/__tests__/steps-read.test.ts +142 -0
  50. package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
  51. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
  52. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
  53. package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
  54. package/src/engine/__tests__/steps-workflow.test.ts +198 -0
  55. package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
  56. package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
  57. package/src/engine/boot-validator/api-ext.ts +77 -0
  58. package/src/engine/boot-validator/config-deps.ts +163 -0
  59. package/src/engine/boot-validator/entity-handler.ts +466 -0
  60. package/src/engine/boot-validator/index.ts +159 -0
  61. package/src/engine/boot-validator/ownership.ts +198 -0
  62. package/src/engine/boot-validator/pii-retention.ts +155 -0
  63. package/src/engine/boot-validator/screens-nav.ts +624 -0
  64. package/src/engine/boot-validator.ts +1 -1528
  65. package/src/engine/build-app-schema.ts +1 -1
  66. package/src/engine/build-target.ts +99 -0
  67. package/src/engine/codemod/index.ts +15 -0
  68. package/src/engine/codemod/pipeline-codemod.ts +641 -0
  69. package/src/engine/config-helpers.ts +9 -19
  70. package/src/engine/constants.ts +1 -1
  71. package/src/engine/define-feature.ts +127 -9
  72. package/src/engine/define-handler.ts +89 -3
  73. package/src/engine/define-roles.ts +2 -2
  74. package/src/engine/define-step.ts +28 -0
  75. package/src/engine/define-workflow.ts +110 -0
  76. package/src/engine/entity-handlers.ts +10 -9
  77. package/src/engine/event-helpers.ts +4 -4
  78. package/src/engine/extension-names.ts +105 -0
  79. package/src/engine/extensions/user-data.ts +106 -0
  80. package/src/engine/factories.ts +26 -16
  81. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
  82. package/src/engine/feature-ast/extractors/index.ts +74 -0
  83. package/src/engine/feature-ast/extractors/round1.ts +110 -0
  84. package/src/engine/feature-ast/extractors/round2.ts +253 -0
  85. package/src/engine/feature-ast/extractors/round3.ts +471 -0
  86. package/src/engine/feature-ast/extractors/round4.ts +1365 -0
  87. package/src/engine/feature-ast/extractors/round5.ts +72 -0
  88. package/src/engine/feature-ast/extractors/round6.ts +66 -0
  89. package/src/engine/feature-ast/extractors/shared.ts +177 -0
  90. package/src/engine/feature-ast/parse.ts +13 -0
  91. package/src/engine/feature-ast/patch.ts +9 -1
  92. package/src/engine/feature-ast/patcher.ts +10 -3
  93. package/src/engine/feature-ast/patterns.ts +71 -1
  94. package/src/engine/feature-ast/render.ts +31 -1
  95. package/src/engine/index.ts +66 -2
  96. package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
  97. package/src/engine/pattern-library/library.ts +78 -2
  98. package/src/engine/pipeline.ts +88 -0
  99. package/src/engine/projection-helpers.ts +1 -1
  100. package/src/engine/read-claim.ts +1 -1
  101. package/src/engine/registry.ts +30 -2
  102. package/src/engine/resolve-config-or-param.ts +4 -0
  103. package/src/engine/run-pipeline.ts +162 -0
  104. package/src/engine/schema-builder.ts +10 -4
  105. package/src/engine/state-machine.ts +1 -1
  106. package/src/engine/steps/_drizzle-boundary.ts +19 -0
  107. package/src/engine/steps/_duration-utils.ts +33 -0
  108. package/src/engine/steps/_no-return-guard.ts +21 -0
  109. package/src/engine/steps/_resolver-utils.ts +42 -0
  110. package/src/engine/steps/_step-dispatch-constants.ts +38 -0
  111. package/src/engine/steps/aggregate-append-event.ts +56 -0
  112. package/src/engine/steps/aggregate-create.ts +56 -0
  113. package/src/engine/steps/aggregate-update.ts +68 -0
  114. package/src/engine/steps/branch.ts +84 -0
  115. package/src/engine/steps/call-feature.ts +49 -0
  116. package/src/engine/steps/compute.ts +41 -0
  117. package/src/engine/steps/for-each.ts +111 -0
  118. package/src/engine/steps/mail-send.ts +44 -0
  119. package/src/engine/steps/read-find-many.ts +51 -0
  120. package/src/engine/steps/read-find-one.ts +58 -0
  121. package/src/engine/steps/retry.ts +87 -0
  122. package/src/engine/steps/return.ts +34 -0
  123. package/src/engine/steps/unsafe-projection-delete.ts +46 -0
  124. package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
  125. package/src/engine/steps/wait-for-event.ts +71 -0
  126. package/src/engine/steps/wait.ts +69 -0
  127. package/src/engine/steps/webhook-send.ts +71 -0
  128. package/src/engine/system-user.ts +1 -1
  129. package/src/engine/types/feature.ts +143 -1
  130. package/src/engine/types/fields.ts +134 -10
  131. package/src/engine/types/handlers.ts +18 -10
  132. package/src/engine/types/identifiers.ts +1 -0
  133. package/src/engine/types/index.ts +15 -1
  134. package/src/engine/types/step.ts +334 -0
  135. package/src/engine/types/target-ref.ts +21 -0
  136. package/src/engine/types/tree-node.ts +130 -0
  137. package/src/engine/types/workspace.ts +7 -0
  138. package/src/engine/validate-projection-allowlist.ts +161 -0
  139. package/src/event-store/snapshot.ts +1 -1
  140. package/src/event-store/upcaster-dead-letter.ts +1 -1
  141. package/src/event-store/upcaster.ts +1 -1
  142. package/src/files/__tests__/read-stream.test.ts +105 -0
  143. package/src/files/__tests__/write-stream.test.ts +233 -0
  144. package/src/files/__tests__/zip-stream.test.ts +357 -0
  145. package/src/files/file-routes.ts +1 -1
  146. package/src/files/in-memory-provider.ts +38 -0
  147. package/src/files/index.ts +3 -0
  148. package/src/files/local-provider.ts +58 -1
  149. package/src/files/types.ts +36 -8
  150. package/src/files/zip-stream.ts +251 -0
  151. package/src/jobs/job-runner.ts +10 -10
  152. package/src/lifecycle/lifecycle.ts +0 -3
  153. package/src/logging/index.ts +1 -0
  154. package/src/logging/pino-logger.ts +11 -7
  155. package/src/logging/utils.ts +24 -0
  156. package/src/observability/prometheus-meter.ts +7 -5
  157. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  158. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  159. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  160. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  161. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  162. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  163. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  164. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  165. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  166. package/src/pipeline/append-event-core.ts +22 -6
  167. package/src/pipeline/dispatcher-utils.ts +188 -0
  168. package/src/pipeline/dispatcher.ts +63 -283
  169. package/src/pipeline/distributed-lock.ts +1 -1
  170. package/src/pipeline/entity-cache.ts +2 -2
  171. package/src/pipeline/event-consumer-state.ts +0 -13
  172. package/src/pipeline/event-dispatcher.ts +4 -4
  173. package/src/pipeline/index.ts +0 -2
  174. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  175. package/src/pipeline/msp-rebuild.ts +5 -5
  176. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  177. package/src/pipeline/projection-rebuild.ts +2 -2
  178. package/src/pipeline/projection-state.ts +0 -12
  179. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  180. package/src/rate-limit/resolver.ts +1 -1
  181. package/src/search/in-memory-adapter.ts +1 -1
  182. package/src/search/meilisearch-adapter.ts +3 -3
  183. package/src/search/types.ts +1 -1
  184. package/src/secrets/leak-guard.ts +2 -2
  185. package/src/stack/request-helper.ts +9 -5
  186. package/src/stack/test-stack.ts +1 -1
  187. package/src/testing/handler-context.ts +4 -4
  188. package/src/testing/http-cookies.ts +1 -1
  189. package/src/time/tz-context.ts +1 -2
  190. package/src/ui-types/index.ts +4 -0
  191. package/src/engine/feature-ast/extractors.ts +0 -2562
@@ -0,0 +1,251 @@
1
+ import { getTemporal } from "../time";
2
+
3
+ // Streaming-ZIP-Builder (S2.U3 Atom 3a) — pure-JS, dependency-frei.
4
+ //
5
+ // **Format-Wahl: STORE (kein DEFLATE).** Compression-frei + minimal —
6
+ // fuer User-Data-Export-Bundles akzeptabel weil:
7
+ // - Storage-Provider (S3/R2) komprimiert eh on-the-wire (HTTP gzip)
8
+ // - JSON-Snippets sind klein, File-Binaries sind oft schon komprimiert
9
+ // (PNG/JPG/PDF/MP4/...)
10
+ // - DEFLATE braucht zlib + buffering pro Entry, killt das Streaming-
11
+ // Property
12
+ //
13
+ // **Streaming-Property:** Caller gibt `AsyncIterable<ZipEntry>`, jeder
14
+ // Entry hat `data: AsyncIterable<Uint8Array>`. ZIP-Bytes werden
15
+ // chunk-fuer-chunk yieldet — kein Entry haengt im Memory bis er fertig
16
+ // ist. Storage-Provider's writeStream konsumiert direkt durch.
17
+ //
18
+ // **ZIP-Standard:** APPNOTE.TXT v6.3.10 (April 2022). STORE-Method,
19
+ // kein ZIP64 (max 4 GB pro Entry, max 65535 Entries — fuer User-Daten-
20
+ // Exports ausreichend; ZIP64-Support kommt wenn ein realer User mit
21
+ // >4 GB single-File auftaucht). Kein UTF-8-Flag (filenames sind ASCII
22
+ // in unserem Use-Case: "profile.json", "files/<id>.<ext>").
23
+ //
24
+ // **CRC32:** klassisches IEEE-802.3-Polynom (0xEDB88320). Lookup-Table
25
+ // einmal initialisiert + cached.
26
+
27
+ const ZIP_VERSION_NEEDED = 20; // 2.0 = STORE
28
+ const ZIP_METHOD_STORE = 0;
29
+
30
+ // Local file header signature: 0x04034b50 = "PK\x03\x04"
31
+ const LOCAL_FILE_HEADER_SIG = 0x04034b50;
32
+ // Central directory file header signature: 0x02014b50 = "PK\x01\x02"
33
+ const CENTRAL_DIR_HEADER_SIG = 0x02014b50;
34
+ // End of central directory record signature: 0x06054b50 = "PK\x05\x06"
35
+ const EOCD_SIG = 0x06054b50;
36
+
37
+ const VERSION_MADE_BY = 0x031e; // 0x03 = UNIX, 0x1e = ZIP 3.0
38
+
39
+ // General Purpose Flag Bit 11 (0x0800) = UTF-8 filename + comment encoding.
40
+ // Wird default-on gesetzt: ASCII ist gueltiges UTF-8, also Win-Win.
41
+ // Ohne Flag interpretieren aeltere ZIP-Tools filenames als CP437 — bei
42
+ // Umlauten ("Bügel.pdf") wird das Mojibake.
43
+ const GENERAL_PURPOSE_FLAGS = 0x0800;
44
+
45
+ // External-Attrs (UNIX-Mode in High-Bytes) fuer regulaere Dateien mit
46
+ // 0644-Permissions: `(S_IFREG | 0644) << 16 = 0o100644 << 16 = 0x81a40000`.
47
+ // S_IFREG = 0o100000 (regular file), 0o644 = rw-r--r--. Ohne diese
48
+ // Bits entpackt Info-ZIP mit Mode 0000 (EACCES beim Read).
49
+ const UNIX_REGULAR_FILE_0644 = 0o100644 << 16; // 0x81a40000
50
+
51
+ // ZIP-Format-Limits ohne ZIP64-Extension. Atom 3a-Scope ist STORE-only;
52
+ // ZIP64 kommt wenn ein realer User-Export diese Caps reisst.
53
+ const ZIP_MAX_ENTRY_SIZE = 0xffffffff; // uint32 → 4 GB
54
+ const ZIP_MAX_ENTRIES = 0xffff; // uint16 → 65535
55
+
56
+ export interface ZipEntry {
57
+ /** Pfad im ZIP, slash-separated. ASCII (kein UTF-8-Flag gesetzt). */
58
+ readonly path: string;
59
+ /** Body als AsyncIterable — kann lazy generated sein (kein Upfront-Memory). */
60
+ readonly data: AsyncIterable<Uint8Array>;
61
+ /**
62
+ * Optional Modification-Time. Default = Now. ZIP nutzt MS-DOS-Format
63
+ * (2-Sek-Aufloesung). Wer Audit-Trail-Zeitpunkte haben will, setzt
64
+ * den Generation-Timestamp ueber alle Entries.
65
+ */
66
+ readonly mtime?: InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
67
+ }
68
+
69
+ /**
70
+ * Erzeugt einen ZIP-Stream als `AsyncIterable<Uint8Array>`. Caller (z.B.
71
+ * `FileStorageProvider.writeStream`) konsumiert chunk-fuer-chunk, kein
72
+ * Entry haengt im Memory bis fertig.
73
+ *
74
+ * Streaming-Lifecycle pro Entry:
75
+ * 1. Local File Header (mit CRC32+size=0 als Placeholder; wir nutzen
76
+ * KEINE Streaming-Data-Descriptor weil Storage-Provider Random-
77
+ * Access nicht garantiert — wir collecten den Body in Memory pro
78
+ * Entry um CRC + size **vor** dem Header zu kennen.
79
+ * Trade-off: 1 Entry-Body im Memory at-a-time; bei <100 MB Files
80
+ * OK, bei >1 GB single-Files braeuchte ZIP64+Streaming-Descriptor).
81
+ * 2. File Body (raw bytes)
82
+ * Am Ende:
83
+ * 3. Central Directory (eine Liste-Entry pro File mit CRC + size)
84
+ * 4. EOCD (End Of Central Directory)
85
+ */
86
+ export async function* createZipStream(
87
+ entries: AsyncIterable<ZipEntry>,
88
+ ): AsyncIterable<Uint8Array> {
89
+ const centralDirRecords: Uint8Array[] = [];
90
+ let offset = 0; // Bytes-Counter fuer central-dir entry's local-header-offset
91
+
92
+ for await (const entry of entries) {
93
+ // Body in Memory collecten — wir brauchen CRC32 + size VOR dem
94
+ // local-file-header (kein streaming-data-descriptor weil
95
+ // file-storage-providers nicht zwingend seek-able sind, aber wir
96
+ // wollen vermeiden dass der konsumierende Storage zwei-pass laesst).
97
+ const body = await collectBody(entry.data);
98
+
99
+ // Hard-Limit: STORE-Mode ohne ZIP64-Extension cappt bei uint32 (4 GB).
100
+ // Bei Ueberschreitung wuerde der Integer-Wrap silent ein korruptes
101
+ // ZIP produzieren (Decoder lesen Muell-Sizes). Lieber early-fail mit
102
+ // klarer Begruendung — Worker (Atom 3b) fängt das + setzt Job=failed.
103
+ if (body.byteLength > ZIP_MAX_ENTRY_SIZE) {
104
+ throw new Error(
105
+ `ZIP-Entry "${entry.path}" exceeds 4 GB limit (${body.byteLength} bytes). ` +
106
+ `STORE-mode without ZIP64-extension caps at uint32. ` +
107
+ `Add ZIP64-support before exporting >4 GB single-files.`,
108
+ );
109
+ }
110
+ if (centralDirRecords.length >= ZIP_MAX_ENTRIES) {
111
+ throw new Error(
112
+ `ZIP archive exceeds ${ZIP_MAX_ENTRIES}-entry limit. ` +
113
+ `Add ZIP64-support before exporting >${ZIP_MAX_ENTRIES} entries.`,
114
+ );
115
+ }
116
+
117
+ const crc = crc32(body);
118
+ const size = body.byteLength;
119
+ const filenameBytes = new TextEncoder().encode(entry.path);
120
+ const dosTime = toDosTime(entry.mtime ?? getTemporal().Now.instant());
121
+
122
+ // Local File Header
123
+ const lfh = new Uint8Array(30 + filenameBytes.byteLength);
124
+ const lfhView = new DataView(lfh.buffer);
125
+ lfhView.setUint32(0, LOCAL_FILE_HEADER_SIG, true);
126
+ lfhView.setUint16(4, ZIP_VERSION_NEEDED, true);
127
+ lfhView.setUint16(6, GENERAL_PURPOSE_FLAGS, true);
128
+ lfhView.setUint16(8, ZIP_METHOD_STORE, true);
129
+ lfhView.setUint16(10, dosTime.time, true);
130
+ lfhView.setUint16(12, dosTime.date, true);
131
+ lfhView.setUint32(14, crc, true);
132
+ lfhView.setUint32(18, size, true); // compressed size = uncompressed (STORE)
133
+ lfhView.setUint32(22, size, true);
134
+ lfhView.setUint16(26, filenameBytes.byteLength, true);
135
+ lfhView.setUint16(28, 0, true); // extra field length
136
+ lfh.set(filenameBytes, 30);
137
+
138
+ yield lfh;
139
+ yield body;
140
+
141
+ // Central-Directory-Eintrag fuer diesen Entry — wir collecten ihn,
142
+ // emittieren ihn am Ende.
143
+ const cdh = new Uint8Array(46 + filenameBytes.byteLength);
144
+ const cdhView = new DataView(cdh.buffer);
145
+ cdhView.setUint32(0, CENTRAL_DIR_HEADER_SIG, true);
146
+ cdhView.setUint16(4, VERSION_MADE_BY, true);
147
+ cdhView.setUint16(6, ZIP_VERSION_NEEDED, true);
148
+ cdhView.setUint16(8, GENERAL_PURPOSE_FLAGS, true);
149
+ cdhView.setUint16(10, ZIP_METHOD_STORE, true);
150
+ cdhView.setUint16(12, dosTime.time, true);
151
+ cdhView.setUint16(14, dosTime.date, true);
152
+ cdhView.setUint32(16, crc, true);
153
+ cdhView.setUint32(20, size, true);
154
+ cdhView.setUint32(24, size, true);
155
+ cdhView.setUint16(28, filenameBytes.byteLength, true);
156
+ cdhView.setUint16(30, 0, true); // extra field length
157
+ cdhView.setUint16(32, 0, true); // comment length
158
+ cdhView.setUint16(34, 0, true); // disk number start
159
+ cdhView.setUint16(36, 0, true); // internal file attrs
160
+ cdhView.setUint32(38, UNIX_REGULAR_FILE_0644, true);
161
+ cdhView.setUint32(42, offset, true); // local-header-offset
162
+ cdh.set(filenameBytes, 46);
163
+ centralDirRecords.push(cdh);
164
+
165
+ offset += lfh.byteLength + body.byteLength;
166
+ }
167
+
168
+ // Central Directory: alle entries hintereinander
169
+ const centralDirStart = offset;
170
+ let centralDirSize = 0;
171
+ for (const record of centralDirRecords) {
172
+ yield record;
173
+ centralDirSize += record.byteLength;
174
+ }
175
+
176
+ // EOCD (End Of Central Directory)
177
+ const eocd = new Uint8Array(22);
178
+ const eocdView = new DataView(eocd.buffer);
179
+ eocdView.setUint32(0, EOCD_SIG, true);
180
+ eocdView.setUint16(4, 0, true); // disk number
181
+ eocdView.setUint16(6, 0, true); // disk where central-dir starts
182
+ eocdView.setUint16(8, centralDirRecords.length, true); // entries on this disk
183
+ eocdView.setUint16(10, centralDirRecords.length, true); // total entries
184
+ eocdView.setUint32(12, centralDirSize, true);
185
+ eocdView.setUint32(16, centralDirStart, true);
186
+ eocdView.setUint16(20, 0, true); // .zip comment length
187
+ yield eocd;
188
+ }
189
+
190
+ async function collectBody(source: AsyncIterable<Uint8Array>): Promise<Uint8Array> {
191
+ const chunks: Uint8Array[] = [];
192
+ let total = 0;
193
+ for await (const chunk of source) {
194
+ chunks.push(chunk);
195
+ total += chunk.byteLength;
196
+ }
197
+ const out = new Uint8Array(total);
198
+ let off = 0;
199
+ for (const c of chunks) {
200
+ out.set(c, off);
201
+ off += c.byteLength;
202
+ }
203
+ return out;
204
+ }
205
+
206
+ // CRC32 — IEEE-802.3-Polynom (0xEDB88320, reversed). Lookup-Table einmal.
207
+ let CRC32_TABLE: Uint32Array | null = null;
208
+ function buildCrcTable(): Uint32Array {
209
+ const table = new Uint32Array(256);
210
+ for (let i = 0; i < 256; i++) {
211
+ let c = i;
212
+ for (let k = 0; k < 8; k++) {
213
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
214
+ }
215
+ table[i] = c >>> 0;
216
+ }
217
+ return table;
218
+ }
219
+
220
+ function crc32(data: Uint8Array): number {
221
+ if (!CRC32_TABLE) CRC32_TABLE = buildCrcTable();
222
+ let crc = 0xffffffff;
223
+ for (let i = 0; i < data.byteLength; i++) {
224
+ const byte = data[i] ?? 0;
225
+ const idx = (crc ^ byte) & 0xff;
226
+ crc = ((CRC32_TABLE[idx] ?? 0) ^ (crc >>> 8)) >>> 0;
227
+ }
228
+ return (crc ^ 0xffffffff) >>> 0;
229
+ }
230
+
231
+ // MS-DOS Date+Time encoding fuer ZIP-Header.
232
+ // Date: bits 9-15=year-1980, 5-8=month, 0-4=day.
233
+ // Time: bits 11-15=hour, 5-10=minute, 0-4=second/2.
234
+ //
235
+ // **UTC** statt lokaler Zeitzone: in einem DSGVO-Kontext ist der
236
+ // ZIP-mtime Teil des Auskunfts-Artefakts. Verschiedene Server-Zeitzonen
237
+ // wuerden sonst verschiedene mtime-Werte fuer denselben Generation-
238
+ // Instant produzieren — Audit-Drift. UTC ist der Standard fuer alle
239
+ // server-side-Timestamps im Repo (Temporal.Instant ueberall).
240
+ function toDosTime(i: InstanceType<ReturnType<typeof getTemporal>["Instant"]>): {
241
+ date: number;
242
+ time: number;
243
+ } {
244
+ const dt = i.toZonedDateTimeISO("UTC");
245
+ const year = dt.year;
246
+ const date =
247
+ (((Math.max(year - 1980, 0) & 0x7f) << 9) | ((dt.month & 0x0f) << 5) | (dt.day & 0x1f)) >>> 0;
248
+ const time =
249
+ (((dt.hour & 0x1f) << 11) | ((dt.minute & 0x3f) << 5) | ((dt.second >> 1) & 0x1f)) >>> 0;
250
+ return { date, time };
251
+ }
@@ -104,7 +104,7 @@ const TRACE_CONTEXT_KEY = "_traceContext";
104
104
  function readTraceContext(data: Record<string, unknown>): SerializedTraceContext | undefined {
105
105
  const raw = data[TRACE_CONTEXT_KEY];
106
106
  if (!raw || typeof raw !== "object") return undefined;
107
- const ctx = raw as Partial<SerializedTraceContext>;
107
+ const ctx = raw as Partial<SerializedTraceContext>; // @cast-boundary engine-payload
108
108
  if (!ctx.traceId || !ctx.spanId) return undefined;
109
109
  return { traceId: ctx.traceId, spanId: ctx.spanId };
110
110
  }
@@ -198,7 +198,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
198
198
  for (const list of results) {
199
199
  for (const j of list) {
200
200
  if (j.name !== jobName) continue;
201
- const t = (j.data as { _tenantId?: string } | undefined)?._tenantId;
201
+ const t = (j.data as { _tenantId?: string } | undefined)?._tenantId; // @cast-boundary dynamic-key
202
202
  if (t === tenantId) {
203
203
  count += 1;
204
204
  if (count >= max) return true;
@@ -274,8 +274,8 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
274
274
  // without peeking at BullMQ internals.
275
275
  const rawData = bullJob.data as DbRow;
276
276
  const meta: JobMeta = {
277
- triggeredById: rawData["_triggeredById"] as string | undefined,
278
- payload: rawData["_payload"] as string | undefined,
277
+ triggeredById: rawData["_triggeredById"] as string | undefined, // @cast-boundary dynamic-key
278
+ payload: rawData["_payload"] as string | undefined, // @cast-boundary dynamic-key
279
279
  attempt: bullJob.attemptsMade + 1,
280
280
  };
281
281
 
@@ -287,16 +287,16 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
287
287
 
288
288
  // Determine tenantId and triggeredBy from meta
289
289
  const tenantId =
290
- (rawData["_tenantId"] as string | undefined) ??
291
- (payload["tenantId"] as string | undefined) ??
290
+ (rawData["_tenantId"] as string | undefined) ?? // @cast-boundary dynamic-key
291
+ (payload["tenantId"] as string | undefined) ?? // @cast-boundary dynamic-key
292
292
  SYSTEM_TENANT_ID;
293
- const triggeredById = (rawData["_triggeredById"] as string | undefined) ?? null;
293
+ const triggeredById = (rawData["_triggeredById"] as string | undefined) ?? null; // @cast-boundary dynamic-key
294
294
 
295
295
  // _triggerName aus rawData übernehmen falls gesetzt — handleEvent
296
296
  // packt das beim Multi-Trigger-Dispatch rein (siehe unten). Über
297
297
  // jobContext.triggerName freigegeben damit der Handler nicht selbst
298
298
  // im rohen Payload kramen muss.
299
- const triggerName = rawData["_triggerName"] as string | undefined;
299
+ const triggerName = rawData["_triggerName"] as string | undefined; // @cast-boundary dynamic-key
300
300
  const jobContext: AppContext = {
301
301
  ...context,
302
302
  systemUser: createSystemUser(tenantId),
@@ -317,7 +317,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
317
317
  // so event writes during this job stamp the same correlation as the
318
318
  // request that scheduled it. Cron/boot jobs (no scheduler) start fresh
319
319
  // — correlationId = new requestId, no parent causation.
320
- const inheritedCorrelationId = (rawData["_correlationId"] as string | undefined) ?? undefined;
320
+ const inheritedCorrelationId = (rawData["_correlationId"] as string | undefined) ?? undefined; // @cast-boundary dynamic-key
321
321
  const jobRequestId = requestContext.generateId();
322
322
  const jobCorrelationId = inheritedCorrelationId ?? jobRequestId;
323
323
 
@@ -460,7 +460,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
460
460
  // dispatch). Fan-out children of perTenant jobs land here on their
461
461
  // recursive queue.add and DO carry _tenantId.
462
462
  if (jobDef.maxPerTenant !== undefined) {
463
- const tenantId = (payload as { _tenantId?: string } | undefined)?._tenantId;
463
+ const tenantId = (payload as { _tenantId?: string } | undefined)?._tenantId; // @cast-boundary dynamic-key
464
464
  if (
465
465
  tenantId !== undefined &&
466
466
  (await isOverPerTenantLimit(jobName, tenantId, jobDef.maxPerTenant))
@@ -146,9 +146,6 @@ export function createLifecycle(opts: LifecycleOptions = {}): Lifecycle {
146
146
  };
147
147
  }
148
148
 
149
- // Builds a single error-log closure once per lifecycle instance. Structured
150
- // logger wins when present; otherwise plain stderr via console.error so we
151
- // never eat a failure silently.
152
149
  function makeErrorLogger(
153
150
  logger: Pick<Logger, "error"> | undefined,
154
151
  ): (msg: string, err: unknown) => void {
@@ -1,3 +1,4 @@
1
1
  export type { LoggerOptions } from "./pino-logger";
2
2
  export { createLogger } from "./pino-logger";
3
3
  export type { Logger } from "./types";
4
+ export { createFallbackLogger } from "./utils";
@@ -11,7 +11,7 @@ export type LoggerOptions = {
11
11
  };
12
12
 
13
13
  export function createLogger(options: LoggerOptions = {}): Logger {
14
- const level = options.level ?? (process.env["LOG_LEVEL"] as LoggerOptions["level"]) ?? "info";
14
+ const level = options.level ?? (process.env["LOG_LEVEL"] as LoggerOptions["level"]) ?? "info"; // @cast-boundary dynamic-key
15
15
  const pretty = options.pretty ?? process.env["LOG_FORMAT"] === "pretty";
16
16
 
17
17
  const pinoConfig = {
@@ -43,22 +43,26 @@ function wrapPino(p: pino.Logger): Logger {
43
43
  return {
44
44
  info(msg, data) {
45
45
  const merged = mergeTraceFields(data);
46
- merged ? p.info(merged, msg) : p.info(msg);
46
+ if (merged) p.info(merged, msg);
47
+ else p.info(msg);
47
48
  },
48
49
  warn(msg, data) {
49
50
  const merged = mergeTraceFields(data);
50
- merged ? p.warn(merged, msg) : p.warn(msg);
51
+ if (merged) p.warn(merged, msg);
52
+ else p.warn(msg);
51
53
  },
52
54
  error(msg, data) {
53
55
  const merged = mergeTraceFields(data);
54
- merged ? p.error(merged, msg) : p.error(msg);
56
+ if (merged) p.error(merged, msg);
57
+ else p.error(msg);
55
58
  },
56
59
  debug(msg, data) {
57
60
  const merged = mergeTraceFields(data);
58
- merged ? p.debug(merged, msg) : p.debug(msg);
61
+ if (merged) p.debug(merged, msg);
62
+ else p.debug(msg);
59
63
  },
60
- child(context) {
61
- return wrapPino(p.child(context));
64
+ child(ctx) {
65
+ return wrapPino(p.child(ctx));
62
66
  },
63
67
  };
64
68
  }
@@ -0,0 +1,24 @@
1
+ import type { Logger } from "./types";
2
+
3
+ type FallbackLogger = {
4
+ error(msg: string, data?: Record<string, unknown>): void;
5
+ };
6
+
7
+ export function createFallbackLogger(
8
+ namespace: string,
9
+ logger?: Pick<Logger, "error"> | undefined,
10
+ ): FallbackLogger {
11
+ if (logger) {
12
+ return {
13
+ error(msg, data) {
14
+ logger.error(`[${namespace}] ${msg}`, data);
15
+ },
16
+ };
17
+ }
18
+ return {
19
+ error(msg, data) {
20
+ // biome-ignore lint/suspicious/noConsole: ops-visible fallback when no logger is wired
21
+ console.error(`[${namespace}] ${msg}:`, data);
22
+ },
23
+ };
24
+ }
@@ -244,21 +244,23 @@ export function serializeOpenMetrics(meter: PrometheusMeter): string {
244
244
  for (const name of names) {
245
245
  const entry = snap.get(name);
246
246
  if (!entry) continue;
247
- const { def, slots } = entry;
247
+ const { def } = entry;
248
248
  if (def.description) lines.push(`# HELP ${name} ${def.description}`);
249
249
  lines.push(`# TYPE ${name} ${def.type}`);
250
250
 
251
- // @cast-boundary engine-bridge — slots union narrows by def.type
252
251
  if (def.type === "counter") {
253
- for (const s of slots as CounterState[]) {
252
+ const counterSlots = entry.slots as CounterState[]; // @cast-boundary engine-bridge
253
+ for (const s of counterSlots) {
254
254
  lines.push(`${name}${renderLabels(s.labels)} ${s.value}`);
255
255
  }
256
256
  } else if (def.type === "gauge") {
257
- for (const s of slots as GaugeState[]) {
257
+ const gaugeSlots = entry.slots as GaugeState[]; // @cast-boundary engine-bridge
258
+ for (const s of gaugeSlots) {
258
259
  lines.push(`${name}${renderLabels(s.labels)} ${s.value}`);
259
260
  }
260
261
  } else {
261
- for (const s of slots as HistogramState[]) {
262
+ const histSlots = entry.slots as HistogramState[]; // @cast-boundary engine-bridge
263
+ for (const s of histSlots) {
262
264
  // Cumulative bucket counts + +Inf terminator + sum/count suffixes.
263
265
  let cumulative = 0;
264
266
  for (let i = 0; i < s.boundaries.length; i++) {
@@ -49,7 +49,7 @@ const archFeature = defineFeature("archtest", (r) => {
49
49
  "item:relabel",
50
50
  z.object({ id: z.uuid(), label: z.string() }),
51
51
  async (event, ctx) => {
52
- await ctx.appendEventUnsafe({
52
+ await ctx.unsafeAppendEvent({
53
53
  aggregateId: event.payload.id,
54
54
  aggregateType: "arch-item",
55
55
  type: labelChanged.name,
@@ -65,7 +65,7 @@ const causationFeature = defineFeature("causation", (r) => {
65
65
  async (event, ctx) => {
66
66
  const created = await orderExecutor.create({ item: event.payload.item }, event.user, ctx.db);
67
67
  if (!created.isSuccess) return created;
68
- await ctx.appendEventUnsafe({
68
+ await ctx.unsafeAppendEvent({
69
69
  aggregateId: String(created.data.id),
70
70
  aggregateType: "causation-order",
71
71
  type: placed.name,
@@ -105,7 +105,7 @@ const shippingFeature = defineFeature("shipping", (r) => {
105
105
  "shipment:bill",
106
106
  z.object({ id: z.uuid(), cost: z.number() }),
107
107
  async (event, ctx) => {
108
- await ctx.appendEventUnsafe({
108
+ await ctx.unsafeAppendEvent({
109
109
  aggregateId: event.payload.id,
110
110
  aggregateType: "domain-shipment",
111
111
  type: shipmentBilled.name,
@@ -137,7 +137,7 @@ const shippingFeature = defineFeature("shipping", (r) => {
137
137
  "shipment:bill-unregistered",
138
138
  z.object({ id: z.uuid() }),
139
139
  async (event, ctx) => {
140
- await ctx.appendEventUnsafe({
140
+ await ctx.unsafeAppendEvent({
141
141
  aggregateId: event.payload.id,
142
142
  aggregateType: "domain-shipment",
143
143
  type: "shipping:event:ghost", // never defined via r.defineEvent
@@ -152,7 +152,7 @@ const shippingFeature = defineFeature("shipping", (r) => {
152
152
  "shipment:bill-bad-payload",
153
153
  z.object({ id: z.uuid() }),
154
154
  async (event, ctx) => {
155
- await ctx.appendEventUnsafe({
155
+ await ctx.unsafeAppendEvent({
156
156
  aggregateId: event.payload.id,
157
157
  aggregateType: "domain-shipment",
158
158
  type: shipmentBilled.name,
@@ -49,7 +49,7 @@ const emitterFeature = defineFeature("emitter", (r) => {
49
49
  "emit:valid",
50
50
  z.object({ userId: z.uuid(), email: z.email() }),
51
51
  async (cmd, ctx) => {
52
- await ctx.appendEventUnsafe({
52
+ await ctx.unsafeAppendEvent({
53
53
  aggregateId: cmd.payload.userId,
54
54
  aggregateType: "user",
55
55
  type: welcome.name,
@@ -66,7 +66,7 @@ const emitterFeature = defineFeature("emitter", (r) => {
66
66
  async (cmd, ctx) => {
67
67
  // Deliberately NOT passing welcome.name — "emitter:event:not-registered"
68
68
  // was never registered. ctx.appendEvent must reject at the append site.
69
- await ctx.appendEventUnsafe({
69
+ await ctx.unsafeAppendEvent({
70
70
  aggregateId: cmd.payload.userId,
71
71
  aggregateType: "user",
72
72
  type: "emitter:event:not-registered",
@@ -82,7 +82,7 @@ const emitterFeature = defineFeature("emitter", (r) => {
82
82
  z.object({ userId: z.uuid() }),
83
83
  async (cmd, ctx) => {
84
84
  // userId is correct but email is missing / not an email string.
85
- await ctx.appendEventUnsafe({
85
+ await ctx.unsafeAppendEvent({
86
86
  aggregateId: cmd.payload.userId,
87
87
  aggregateType: "user",
88
88
  type: welcome.name,
@@ -100,7 +100,7 @@ const emitterFeature = defineFeature("emitter", (r) => {
100
100
  // "neighbor:event:neighbor-signal" is owned by the neighbor feature.
101
101
  // The ownership guard in appendDomainEventCore must reject this append
102
102
  // at emit-site — cross-feature emission silently breaks encapsulation.
103
- await ctx.appendEventUnsafe({
103
+ await ctx.unsafeAppendEvent({
104
104
  aggregateId: cmd.payload.userId,
105
105
  aggregateType: "user",
106
106
  type: foreignEventName,
@@ -66,7 +66,7 @@ const asOfFeature = defineFeature("asoftest", (r) => {
66
66
  "invoice:approve",
67
67
  z.object({ id: z.uuid(), amount: z.number().int(), approvedBy: z.string() }),
68
68
  async (event, ctx) => {
69
- await ctx.appendEventUnsafe({
69
+ await ctx.unsafeAppendEvent({
70
70
  aggregateId: event.payload.id,
71
71
  aggregateType: "asof-invoice",
72
72
  type: approved.name,
@@ -55,7 +55,7 @@ const mmhFeature = defineFeature("mmh", (r) => {
55
55
  async (event, ctx) => {
56
56
  const created = await orderExecutor.create({ item: event.payload.item }, event.user, ctx.db);
57
57
  if (!created.isSuccess) return created;
58
- await ctx.appendEventUnsafe({
58
+ await ctx.unsafeAppendEvent({
59
59
  aggregateId: String(created.data.id),
60
60
  aggregateType: "mmh-order",
61
61
  type: placed.name,
@@ -75,7 +75,7 @@ const mmhFeature = defineFeature("mmh", (r) => {
75
75
  if (!ctx) throw new Error("MSP-apply ctx missing — regression of C.2b wiring");
76
76
  const history = await ctx.loadAggregate(event.aggregateId);
77
77
  confirmLoadCounts.push(history.length);
78
- await ctx.appendEventUnsafe({
78
+ await ctx.unsafeAppendEvent({
79
79
  aggregateId: event.aggregateId,
80
80
  aggregateType: "mmh-order",
81
81
  type: confirmed.name,
@@ -91,7 +91,7 @@ const mmhFeature = defineFeature("mmh", (r) => {
91
91
  apply: {
92
92
  [confirmed.name]: async (event, _tx, ctx) => {
93
93
  if (!ctx) throw new Error("MSP-apply ctx missing — regression of C.2b wiring");
94
- await ctx.appendEventUnsafe({
94
+ await ctx.unsafeAppendEvent({
95
95
  aggregateId: event.aggregateId,
96
96
  aggregateType: "mmh-order",
97
97
  type: shipped.name,
@@ -139,7 +139,7 @@ const feature = defineFeature("mspreb", (r) => {
139
139
  apply: {
140
140
  [invoiceBilled.name]: async (event, _tx, ctx) => {
141
141
  const p = event.payload as { customer: string };
142
- await ctx.appendEventUnsafe({
142
+ await ctx.unsafeAppendEvent({
143
143
  aggregateId: p.customer,
144
144
  aggregateType: "msp-reb-invoice",
145
145
  type: escalationTriggered.name,
@@ -166,7 +166,7 @@ const feature = defineFeature("mspreb", (r) => {
166
166
  ctx.db,
167
167
  );
168
168
  if (!res.isSuccess) return res;
169
- await ctx.appendEventUnsafe({
169
+ await ctx.unsafeAppendEvent({
170
170
  aggregateId: String(res.data.id),
171
171
  aggregateType: "msp-reb-invoice",
172
172
  type: invoiceBilled.name,
@@ -187,7 +187,7 @@ const feature = defineFeature("mspreb", (r) => {
187
187
  ctx.db,
188
188
  );
189
189
  if (!res.isSuccess) return res;
190
- await ctx.appendEventUnsafe({
190
+ await ctx.unsafeAppendEvent({
191
191
  aggregateId: String(res.data.id),
192
192
  aggregateType: "msp-reb-payment",
193
193
  type: paymentReceived.name,
@@ -124,7 +124,7 @@ const mspFeature = defineFeature("msptest", (r) => {
124
124
  ctx.db,
125
125
  );
126
126
  if (!res.isSuccess) return res;
127
- await ctx.appendEventUnsafe({
127
+ await ctx.unsafeAppendEvent({
128
128
  aggregateId: String(res.data.id),
129
129
  aggregateType: "msp-shipment",
130
130
  type: shipmentBilled.name,
@@ -145,7 +145,7 @@ const mspFeature = defineFeature("msptest", (r) => {
145
145
  ctx.db,
146
146
  );
147
147
  if (!res.isSuccess) return res;
148
- await ctx.appendEventUnsafe({
148
+ await ctx.unsafeAppendEvent({
149
149
  aggregateId: String(res.data.id),
150
150
  aggregateType: "msp-refund",
151
151
  type: refundIssued.name,
@@ -96,10 +96,10 @@ const qpFeature = defineFeature("qp", (r) => {
96
96
 
97
97
  r.queryHandler(
98
98
  "widget:list-system",
99
- z.object({ allTenants: z.boolean().optional() }),
99
+ z.object({ unsafeAllTenants: z.boolean().optional() }),
100
100
  async (query, ctx) =>
101
101
  ctx.queryProjection("qp:projection:widget-audit", {
102
- allTenants: query.payload.allTenants ?? false,
102
+ unsafeAllTenants: query.payload.unsafeAllTenants ?? false,
103
103
  }),
104
104
  { access: { openToAll: true } },
105
105
  );
@@ -172,8 +172,8 @@ describe("ctx.queryProjection", () => {
172
172
  expect(rows.map((r) => r.label).sort()).toEqual(["X", "Y"]);
173
173
  });
174
174
 
175
- test("allTenants=true bypasses tenant filter on tenant-scoped projection", async () => {
176
- // Repurpose list-system by passing allTenants=true — but list-system is
175
+ test("unsafeAllTenants=true bypasses tenant filter on tenant-scoped projection", async () => {
176
+ // Repurpose list-system by passing unsafeAllTenants=true — but list-system is
177
177
  // already no-tenant-column. The semantic matters when a projection HAS
178
178
  // tenant_id but the handler wants a cross-tenant sweep (audit). We
179
179
  // exercise that contract via a direct queryProjection call here.
@@ -186,7 +186,7 @@ describe("ctx.queryProjection", () => {
186
186
  // surface small — assert against the two query handlers we have.)
187
187
  const sys = await stack.http.queryOk<Array<{ label: string }>>(
188
188
  "qp:query:widget:list-system",
189
- { allTenants: true },
189
+ { unsafeAllTenants: true },
190
190
  admin,
191
191
  );
192
192
  expect(sys).toHaveLength(2);