@blamejs/core 0.5.14 → 0.5.15

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 CHANGED
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.5.x
10
10
 
11
+ - **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
11
12
  - **0.5.13** (2026-04-30) — b.testing.request: supertest-style chainable HTTP test helper
12
13
  - **0.5.12** (2026-04-30) — b.middleware.requestLog: HTTP access-log middleware
13
14
  - **0.5.11** (2026-04-30) — b.config: schema-validated environment configuration
package/index.js CHANGED
@@ -105,6 +105,7 @@ var staticServe = require("./lib/static");
105
105
  var forms = require("./lib/forms");
106
106
  var app = require("./lib/app");
107
107
  var jobs = require("./lib/jobs");
108
+ var archive = require("./lib/archive");
108
109
  var breakGlass = require("./lib/break-glass");
109
110
  var config = require("./lib/config");
110
111
  var csv = require("./lib/csv");
@@ -211,6 +212,7 @@ module.exports = {
211
212
  forms: forms,
212
213
  createApp: app.createApp,
213
214
  jobs: jobs,
215
+ archive: archive,
214
216
  breakGlass: breakGlass,
215
217
  config: config,
216
218
  csv: csv,
package/lib/archive.js ADDED
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ /**
3
+ * archive — ZIP creation. Operator-data-export shape ("download my
4
+ * data as a zip"), log archives, plain-zip exports for users.
5
+ *
6
+ * var archive = b.archive.zip();
7
+ * archive.addFile("readme.txt", "Hello\n");
8
+ * archive.addFile("data/users.csv", csvBytes, { method: "deflate" });
9
+ * archive.addFile("avatars/me.png", pngBuf, { method: "store" }); // already-compressed
10
+ * var zipBytes = archive.toBuffer();
11
+ *
12
+ * // OR write directly to disk:
13
+ * archive.writeTo("/tmp/export.zip");
14
+ *
15
+ * Format support:
16
+ * - Stored (no compression — for already-compressed inputs like
17
+ * PNG / JPEG / mp4)
18
+ * - Deflate via node:zlib's deflateRawSync (default for everything else)
19
+ * - File names with / are honored — directory entries are implicit;
20
+ * extractors create the directory structure on demand
21
+ * - UTF-8 file names (sets the EFS bit per APPNOTE 6.3.4)
22
+ * - Modification time defaults to "now"; operators override per file
23
+ *
24
+ * v1 scope cuts (deferred):
25
+ * - ZIP64 (>4 GiB archives, >65535 files) — operators with that
26
+ * scale bring their own
27
+ * - Encryption — `b.crypto.encryptPacked` produces a sealed bundle
28
+ * for the operator's encryption-at-rest needs; ZIP-native
29
+ * password encryption is broken-by-design
30
+ * - Streaming write (toStream) — toBuffer() is enough for the
31
+ * "download my data" shape; operators streaming gigabytes
32
+ * have a different toolset
33
+ * - Reading / extraction — write-only for now
34
+ */
35
+ var zlib = require("node:zlib");
36
+ var fs = require("node:fs");
37
+ var nodeCrypto = require("node:crypto");
38
+ var { defineClass } = require("./framework-error");
39
+
40
+ var ArchiveError = defineClass("ArchiveError", { alwaysPermanent: true });
41
+
42
+ // ZIP signatures
43
+ var SIG_LFH = 0x04034b50; // local file header
44
+ var SIG_CFH = 0x02014b50; // central directory file header
45
+ var SIG_EOCD = 0x06054b50; // end of central directory
46
+
47
+ // Compression methods
48
+ var METHOD_STORE = 0;
49
+ var METHOD_DEFLATE = 8;
50
+
51
+ // CRC-32 — IEEE 802.3 polynomial. node:crypto has no native CRC32, so
52
+ // we vendor the standard table-driven implementation.
53
+ var CRC32_TABLE = (function () {
54
+ var t = new Uint32Array(256);
55
+ for (var i = 0; i < 256; i++) {
56
+ var c = i;
57
+ for (var j = 0; j < 8; j++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
58
+ t[i] = c >>> 0;
59
+ }
60
+ return t;
61
+ })();
62
+
63
+ function _crc32(buf) {
64
+ var crc = 0xffffffff;
65
+ for (var i = 0; i < buf.length; i++) {
66
+ crc = CRC32_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
67
+ }
68
+ return (crc ^ 0xffffffff) >>> 0;
69
+ }
70
+
71
+ // MS-DOS date/time encoding — APPNOTE 4.4.6
72
+ function _msdosDateTime(date) {
73
+ var d = date instanceof Date ? date : new Date(date);
74
+ if (isNaN(d.getTime())) d = new Date();
75
+ var dosTime = ((d.getHours() & 0x1f) << 11) |
76
+ ((d.getMinutes() & 0x3f) << 5) |
77
+ ((Math.floor(d.getSeconds() / 2)) & 0x1f);
78
+ var dosDate = (((d.getFullYear() - 1980) & 0x7f) << 9) |
79
+ (((d.getMonth() + 1) & 0xf) << 5) |
80
+ (d.getDate() & 0x1f);
81
+ return { time: dosTime, date: dosDate };
82
+ }
83
+
84
+ function zip() {
85
+ var entries = [];
86
+
87
+ function addFile(name, content, opts) {
88
+ if (typeof name !== "string" || name.length === 0) {
89
+ throw new ArchiveError("archive/bad-name", "addFile: name must be a non-empty string");
90
+ }
91
+ if (name.indexOf("\0") !== -1) {
92
+ throw new ArchiveError("archive/bad-name", "addFile: name contains null byte");
93
+ }
94
+ // No path traversal — relative paths only, no leading slash, no ".." segments.
95
+ var normalized = name.replace(/\\/g, "/").replace(/^\/+/, "");
96
+ var segs = normalized.split("/");
97
+ for (var si = 0; si < segs.length; si++) {
98
+ if (segs[si] === "..") {
99
+ throw new ArchiveError("archive/bad-name", "addFile: name contains '..' segment");
100
+ }
101
+ }
102
+ var bodyBuf;
103
+ if (Buffer.isBuffer(content)) bodyBuf = content;
104
+ else if (typeof content === "string") bodyBuf = Buffer.from(content, "utf8");
105
+ else throw new ArchiveError("archive/bad-content",
106
+ "addFile: content must be a Buffer or string, got " + typeof content);
107
+
108
+ opts = opts || {};
109
+ var method = opts.method === "store" ? METHOD_STORE : METHOD_DEFLATE;
110
+ var mtime = opts.mtime instanceof Date ? opts.mtime : new Date();
111
+
112
+ var crc = _crc32(bodyBuf);
113
+ var stored = bodyBuf;
114
+ if (method === METHOD_DEFLATE) {
115
+ stored = zlib.deflateRawSync(bodyBuf);
116
+ // If deflate didn't shrink it (small/already-compressed inputs),
117
+ // fall back to STORE to save the operator a few bytes.
118
+ if (stored.length >= bodyBuf.length) {
119
+ stored = bodyBuf;
120
+ method = METHOD_STORE;
121
+ }
122
+ }
123
+
124
+ entries.push({
125
+ name: normalized,
126
+ method: method,
127
+ mtime: mtime,
128
+ crc: crc,
129
+ stored: stored,
130
+ uncompressedSize: bodyBuf.length,
131
+ });
132
+ }
133
+
134
+ function _buildLocalFileHeader(entry) {
135
+ var nameBuf = Buffer.from(entry.name, "utf8");
136
+ var dt = _msdosDateTime(entry.mtime);
137
+ var hdr = Buffer.alloc(30);
138
+ hdr.writeUInt32LE(SIG_LFH, 0);
139
+ hdr.writeUInt16LE(20, 4); // version needed
140
+ hdr.writeUInt16LE(0x0800, 6); // flags: bit 11 = UTF-8 name
141
+ hdr.writeUInt16LE(entry.method, 8);
142
+ hdr.writeUInt16LE(dt.time, 10);
143
+ hdr.writeUInt16LE(dt.date, 12);
144
+ hdr.writeUInt32LE(entry.crc, 14);
145
+ hdr.writeUInt32LE(entry.stored.length, 18);
146
+ hdr.writeUInt32LE(entry.uncompressedSize, 22);
147
+ hdr.writeUInt16LE(nameBuf.length, 26);
148
+ hdr.writeUInt16LE(0, 28); // extra field length
149
+ return Buffer.concat([hdr, nameBuf]);
150
+ }
151
+
152
+ function _buildCentralDirectoryEntry(entry, lfhOffset) {
153
+ var nameBuf = Buffer.from(entry.name, "utf8");
154
+ var dt = _msdosDateTime(entry.mtime);
155
+ var hdr = Buffer.alloc(46);
156
+ hdr.writeUInt32LE(SIG_CFH, 0);
157
+ hdr.writeUInt16LE(0x033f, 4); // version made by (UNIX | 6.3)
158
+ hdr.writeUInt16LE(20, 6); // version needed
159
+ hdr.writeUInt16LE(0x0800, 8); // flags: bit 11 = UTF-8
160
+ hdr.writeUInt16LE(entry.method, 10);
161
+ hdr.writeUInt16LE(dt.time, 12);
162
+ hdr.writeUInt16LE(dt.date, 14);
163
+ hdr.writeUInt32LE(entry.crc, 16);
164
+ hdr.writeUInt32LE(entry.stored.length, 20);
165
+ hdr.writeUInt32LE(entry.uncompressedSize, 24);
166
+ hdr.writeUInt16LE(nameBuf.length, 28);
167
+ hdr.writeUInt16LE(0, 30); // extra field length
168
+ hdr.writeUInt16LE(0, 32); // file comment length
169
+ hdr.writeUInt16LE(0, 34); // disk number start
170
+ hdr.writeUInt16LE(0, 36); // internal file attributes
171
+ hdr.writeUInt32LE(0, 38); // external file attributes
172
+ hdr.writeUInt32LE(lfhOffset, 42);
173
+ return Buffer.concat([hdr, nameBuf]);
174
+ }
175
+
176
+ function toBuffer() {
177
+ if (entries.length > 65535) {
178
+ throw new ArchiveError("archive/too-many-entries",
179
+ "ZIP archive cannot contain more than 65535 entries (ZIP64 unsupported in v1)");
180
+ }
181
+ var pieces = [];
182
+ var offsets = [];
183
+ var totalLocalBytes = 0;
184
+ for (var i = 0; i < entries.length; i++) {
185
+ offsets.push(totalLocalBytes);
186
+ var lfh = _buildLocalFileHeader(entries[i]);
187
+ pieces.push(lfh);
188
+ pieces.push(entries[i].stored);
189
+ totalLocalBytes += lfh.length + entries[i].stored.length;
190
+ }
191
+ var cdStart = totalLocalBytes;
192
+ var cdSize = 0;
193
+ for (var j = 0; j < entries.length; j++) {
194
+ var cdh = _buildCentralDirectoryEntry(entries[j], offsets[j]);
195
+ pieces.push(cdh);
196
+ cdSize += cdh.length;
197
+ }
198
+ // End of Central Directory
199
+ var eocd = Buffer.alloc(22);
200
+ eocd.writeUInt32LE(SIG_EOCD, 0);
201
+ eocd.writeUInt16LE(0, 4); // disk number
202
+ eocd.writeUInt16LE(0, 6); // disk where CD starts
203
+ eocd.writeUInt16LE(entries.length, 8); // entries on this disk
204
+ eocd.writeUInt16LE(entries.length, 10); // total entries
205
+ eocd.writeUInt32LE(cdSize, 12); // size of central directory
206
+ eocd.writeUInt32LE(cdStart, 16); // offset of central directory
207
+ eocd.writeUInt16LE(0, 20); // comment length
208
+ pieces.push(eocd);
209
+ return Buffer.concat(pieces);
210
+ }
211
+
212
+ function writeTo(filepath) {
213
+ var buf = toBuffer();
214
+ fs.writeFileSync(filepath, buf);
215
+ return buf.length;
216
+ }
217
+
218
+ function digest() {
219
+ // SHA-256 of the produced archive bytes — useful for operator-side
220
+ // integrity logging on exported bundles. Not vendor-locked to any
221
+ // particular hash; SHA-256 is universally recognized for content
222
+ // addressing.
223
+ return nodeCrypto.createHash("sha256").update(toBuffer()).digest("hex");
224
+ }
225
+
226
+ return {
227
+ addFile: addFile,
228
+ toBuffer: toBuffer,
229
+ writeTo: writeTo,
230
+ digest: digest,
231
+ get entryCount() { return entries.length; },
232
+ };
233
+ }
234
+
235
+ module.exports = {
236
+ zip: zip,
237
+ ArchiveError: ArchiveError,
238
+ // Test-only export — operators don't call this; it's here for unit-testing
239
+ // the CRC implementation against known vectors.
240
+ _crc32ForTest: _crc32,
241
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.5.14",
3
+ "version": "0.5.15",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",