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