@cosmicdrift/kumiko-framework 0.2.1 → 0.2.3

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 (42) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +4 -3
  3. package/src/auth/__tests__/roles.test.ts +24 -0
  4. package/src/auth/index.ts +7 -0
  5. package/src/auth/roles.ts +42 -0
  6. package/src/compliance/__tests__/duration-spec.test.ts +72 -0
  7. package/src/compliance/__tests__/profiles.test.ts +308 -0
  8. package/src/compliance/__tests__/sub-processors.test.ts +139 -0
  9. package/src/compliance/duration-spec.ts +44 -0
  10. package/src/compliance/index.ts +31 -0
  11. package/src/compliance/override-schema.ts +136 -0
  12. package/src/compliance/profiles.ts +427 -0
  13. package/src/compliance/sub-processors.ts +152 -0
  14. package/src/db/__tests__/big-int-field.test.ts +131 -0
  15. package/src/db/table-builder.ts +18 -1
  16. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
  17. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
  18. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
  19. package/src/engine/boot-validator.ts +276 -0
  20. package/src/engine/define-feature.ts +39 -0
  21. package/src/engine/extension-names.ts +105 -0
  22. package/src/engine/extensions/user-data.ts +106 -0
  23. package/src/engine/factories.ts +15 -5
  24. package/src/engine/feature-ast/extractors.ts +40 -0
  25. package/src/engine/feature-ast/parse.ts +6 -0
  26. package/src/engine/feature-ast/patterns.ts +22 -0
  27. package/src/engine/feature-ast/render.ts +14 -0
  28. package/src/engine/index.ts +21 -0
  29. package/src/engine/pattern-library/__tests__/library.test.ts +5 -0
  30. package/src/engine/pattern-library/library.ts +36 -0
  31. package/src/engine/schema-builder.ts +8 -0
  32. package/src/engine/types/feature.ts +51 -0
  33. package/src/engine/types/fields.ts +134 -10
  34. package/src/engine/types/index.ts +3 -0
  35. package/src/files/__tests__/read-stream.test.ts +105 -0
  36. package/src/files/__tests__/write-stream.test.ts +233 -0
  37. package/src/files/__tests__/zip-stream.test.ts +357 -0
  38. package/src/files/in-memory-provider.ts +38 -0
  39. package/src/files/index.ts +3 -0
  40. package/src/files/local-provider.ts +58 -1
  41. package/src/files/types.ts +34 -6
  42. package/src/files/zip-stream.ts +251 -0
@@ -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
+ }