@blamejs/core 0.5.7 → 0.5.9
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 +2 -0
- package/index.js +4 -0
- package/lib/csv.js +229 -0
- package/lib/uuid.js +105 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.5.x
|
|
10
10
|
|
|
11
|
+
- **0.5.8** (2026-04-30) — b.uuid: RFC 4122 v4 + RFC 9562 v7
|
|
12
|
+
- **0.5.7** (2026-04-30) — defensive validation + queue closure capture + audit context
|
|
11
13
|
- **0.5.6** (2026-04-30) — break-glass: trustProxy honored, cache require hoisted
|
|
12
14
|
- **0.5.5** (2026-04-30) — strict default CSP + IPv6 special-range expansion
|
|
13
15
|
- **0.5.4** (2026-04-30) — close SSRF DNS-rebinding window with pinned outbound DNS
|
package/index.js
CHANGED
|
@@ -106,6 +106,8 @@ var forms = require("./lib/forms");
|
|
|
106
106
|
var app = require("./lib/app");
|
|
107
107
|
var jobs = require("./lib/jobs");
|
|
108
108
|
var breakGlass = require("./lib/break-glass");
|
|
109
|
+
var csv = require("./lib/csv");
|
|
110
|
+
var uuid = require("./lib/uuid");
|
|
109
111
|
var mail = require("./lib/mail");
|
|
110
112
|
var mailBounce = require("./lib/mail-bounce");
|
|
111
113
|
var websocketChannels = require("./lib/websocket-channels");
|
|
@@ -208,6 +210,8 @@ module.exports = {
|
|
|
208
210
|
createApp: app.createApp,
|
|
209
211
|
jobs: jobs,
|
|
210
212
|
breakGlass: breakGlass,
|
|
213
|
+
csv: csv,
|
|
214
|
+
uuid: uuid,
|
|
211
215
|
mail: mail,
|
|
212
216
|
mailBounce: mailBounce,
|
|
213
217
|
websocketChannels: websocketChannels,
|
package/lib/csv.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* csv — RFC 4180 parser + serializer.
|
|
4
|
+
*
|
|
5
|
+
* Public API:
|
|
6
|
+
*
|
|
7
|
+
* b.csv.parse(text, opts?) → { headers, rows } (or { rows } if no header)
|
|
8
|
+
* b.csv.stringify(rows, opts?) → string
|
|
9
|
+
*
|
|
10
|
+
* Opts (parse):
|
|
11
|
+
* delimiter: "," default
|
|
12
|
+
* header: true default — first row becomes column names; rows
|
|
13
|
+
* are objects keyed by header
|
|
14
|
+
* maxBytes: 8 MiB default — refuses parse-bombs the same way
|
|
15
|
+
* safeJson does
|
|
16
|
+
* onBadRow: "throw" default | "skip" — what to do when a row has
|
|
17
|
+
* a different column count than the header
|
|
18
|
+
*
|
|
19
|
+
* Opts (stringify):
|
|
20
|
+
* delimiter: "," default
|
|
21
|
+
* header: true default — emit a header row from object keys
|
|
22
|
+
* columns: array of explicit column order (default: first row's keys)
|
|
23
|
+
* eol: "\r\n" default — RFC 4180 mandates CRLF; some parsers
|
|
24
|
+
* want LF. Operators choose.
|
|
25
|
+
*
|
|
26
|
+
* Format support:
|
|
27
|
+
* - Quoted fields with embedded commas, quotes ("" → "), newlines.
|
|
28
|
+
* - Optional BOM at file start (consumed silently if present).
|
|
29
|
+
* - Trailing newline tolerated (RFC 4180 says SHOULD; we tolerate).
|
|
30
|
+
*
|
|
31
|
+
* Throws CsvError (FrameworkError) on shape violations.
|
|
32
|
+
*/
|
|
33
|
+
var C = require("./constants");
|
|
34
|
+
var { defineClass } = require("./framework-error");
|
|
35
|
+
|
|
36
|
+
var CsvError = defineClass("CsvError", { alwaysPermanent: true });
|
|
37
|
+
|
|
38
|
+
var DEFAULT_MAX_BYTES = C.BYTES.mib(8);
|
|
39
|
+
var DEFAULT_DELIMITER = ",";
|
|
40
|
+
var DEFAULT_EOL = "\r\n";
|
|
41
|
+
|
|
42
|
+
// ---- parse ----
|
|
43
|
+
|
|
44
|
+
function parse(text, opts) {
|
|
45
|
+
opts = opts || {};
|
|
46
|
+
if (typeof text !== "string" && !Buffer.isBuffer(text)) {
|
|
47
|
+
throw new CsvError("csv/bad-input",
|
|
48
|
+
"parse: input must be a string or Buffer, got " + typeof text);
|
|
49
|
+
}
|
|
50
|
+
var s = Buffer.isBuffer(text) ? text.toString("utf8") : text;
|
|
51
|
+
var maxBytes = opts.maxBytes != null ? opts.maxBytes : DEFAULT_MAX_BYTES;
|
|
52
|
+
if (Buffer.byteLength(s, "utf8") > maxBytes) {
|
|
53
|
+
throw new CsvError("csv/too-large",
|
|
54
|
+
"parse: input exceeds maxBytes (" + maxBytes + ")");
|
|
55
|
+
}
|
|
56
|
+
var delimiter = opts.delimiter || DEFAULT_DELIMITER;
|
|
57
|
+
if (typeof delimiter !== "string" || delimiter.length !== 1) {
|
|
58
|
+
throw new CsvError("csv/bad-delimiter",
|
|
59
|
+
"parse: delimiter must be a single character, got " + JSON.stringify(delimiter));
|
|
60
|
+
}
|
|
61
|
+
if (delimiter === "\"" || delimiter === "\r" || delimiter === "\n") {
|
|
62
|
+
throw new CsvError("csv/bad-delimiter",
|
|
63
|
+
"parse: delimiter cannot be quote / CR / LF");
|
|
64
|
+
}
|
|
65
|
+
var hasHeader = opts.header !== false;
|
|
66
|
+
var onBadRow = opts.onBadRow || "throw";
|
|
67
|
+
if (onBadRow !== "throw" && onBadRow !== "skip") {
|
|
68
|
+
throw new CsvError("csv/bad-opt",
|
|
69
|
+
"parse: onBadRow must be 'throw' or 'skip'");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Strip BOM if present.
|
|
73
|
+
if (s.charCodeAt(0) === 0xfeff) s = s.slice(1);
|
|
74
|
+
|
|
75
|
+
var rows = [];
|
|
76
|
+
var field = "";
|
|
77
|
+
var row = [];
|
|
78
|
+
var inQuotes = false;
|
|
79
|
+
var i = 0;
|
|
80
|
+
var len = s.length;
|
|
81
|
+
while (i < len) {
|
|
82
|
+
var c = s.charAt(i);
|
|
83
|
+
if (inQuotes) {
|
|
84
|
+
if (c === "\"") {
|
|
85
|
+
if (i + 1 < len && s.charAt(i + 1) === "\"") {
|
|
86
|
+
field += "\""; // escaped quote
|
|
87
|
+
i += 2;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
inQuotes = false;
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
field += c;
|
|
95
|
+
i++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (c === "\"") {
|
|
99
|
+
// Quote at the start of a field (or anywhere — treat as start of
|
|
100
|
+
// quoted run). RFC 4180 says quoted fields begin with a quote;
|
|
101
|
+
// fields that contain a quote not at the start are technically
|
|
102
|
+
// malformed but we tolerate by appending literally.
|
|
103
|
+
if (field.length === 0) {
|
|
104
|
+
inQuotes = true;
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
field += "\"";
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (c === delimiter) {
|
|
113
|
+
row.push(field);
|
|
114
|
+
field = "";
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (c === "\r") {
|
|
119
|
+
// CRLF or bare CR — treat both as row terminator.
|
|
120
|
+
row.push(field);
|
|
121
|
+
rows.push(row);
|
|
122
|
+
field = "";
|
|
123
|
+
row = [];
|
|
124
|
+
if (i + 1 < len && s.charAt(i + 1) === "\n") i += 2;
|
|
125
|
+
else i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (c === "\n") {
|
|
129
|
+
row.push(field);
|
|
130
|
+
rows.push(row);
|
|
131
|
+
field = "";
|
|
132
|
+
row = [];
|
|
133
|
+
i++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
field += c;
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
// Flush any trailing field/row (no trailing newline case)
|
|
140
|
+
if (field.length > 0 || row.length > 0) {
|
|
141
|
+
row.push(field);
|
|
142
|
+
rows.push(row);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!hasHeader) {
|
|
146
|
+
return { rows: rows };
|
|
147
|
+
}
|
|
148
|
+
if (rows.length === 0) {
|
|
149
|
+
return { headers: [], rows: [] };
|
|
150
|
+
}
|
|
151
|
+
var headers = rows[0];
|
|
152
|
+
var out = [];
|
|
153
|
+
for (var r = 1; r < rows.length; r++) {
|
|
154
|
+
var current = rows[r];
|
|
155
|
+
if (current.length === 1 && current[0] === "") continue; // skip blank lines
|
|
156
|
+
if (current.length !== headers.length) {
|
|
157
|
+
if (onBadRow === "skip") continue;
|
|
158
|
+
throw new CsvError("csv/row-length-mismatch",
|
|
159
|
+
"parse: row " + r + " has " + current.length + " columns, expected " + headers.length);
|
|
160
|
+
}
|
|
161
|
+
var obj = {};
|
|
162
|
+
for (var k = 0; k < headers.length; k++) obj[headers[k]] = current[k];
|
|
163
|
+
out.push(obj);
|
|
164
|
+
}
|
|
165
|
+
return { headers: headers, rows: out };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- stringify ----
|
|
169
|
+
|
|
170
|
+
function stringify(rows, opts) {
|
|
171
|
+
opts = opts || {};
|
|
172
|
+
if (!Array.isArray(rows)) {
|
|
173
|
+
throw new CsvError("csv/bad-input",
|
|
174
|
+
"stringify: rows must be an array");
|
|
175
|
+
}
|
|
176
|
+
var delimiter = opts.delimiter || DEFAULT_DELIMITER;
|
|
177
|
+
var eol = opts.eol || DEFAULT_EOL;
|
|
178
|
+
var hasHeader = opts.header !== false;
|
|
179
|
+
if (rows.length === 0) return "";
|
|
180
|
+
|
|
181
|
+
var columns;
|
|
182
|
+
if (Array.isArray(opts.columns)) {
|
|
183
|
+
columns = opts.columns.slice();
|
|
184
|
+
} else if (Array.isArray(rows[0])) {
|
|
185
|
+
// Array-of-arrays input — no headers to derive; emit as-is.
|
|
186
|
+
columns = null;
|
|
187
|
+
} else if (rows[0] && typeof rows[0] === "object") {
|
|
188
|
+
columns = Object.keys(rows[0]);
|
|
189
|
+
} else {
|
|
190
|
+
throw new CsvError("csv/bad-input",
|
|
191
|
+
"stringify: rows[0] must be an object or array");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var lines = [];
|
|
195
|
+
if (hasHeader && columns) lines.push(columns.map(_quoteCell).join(delimiter));
|
|
196
|
+
|
|
197
|
+
for (var r = 0; r < rows.length; r++) {
|
|
198
|
+
var row = rows[r];
|
|
199
|
+
var cells;
|
|
200
|
+
if (Array.isArray(row)) {
|
|
201
|
+
cells = row.map(_quoteCell);
|
|
202
|
+
} else if (row && typeof row === "object") {
|
|
203
|
+
cells = (columns || Object.keys(row)).map(function (col) {
|
|
204
|
+
return _quoteCell(row[col]);
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
throw new CsvError("csv/bad-input",
|
|
208
|
+
"stringify: rows[" + r + "] must be an object or array");
|
|
209
|
+
}
|
|
210
|
+
lines.push(cells.join(delimiter));
|
|
211
|
+
}
|
|
212
|
+
return lines.join(eol) + eol;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function _quoteCell(value) {
|
|
216
|
+
if (value === null || value === undefined) return "";
|
|
217
|
+
var s = typeof value === "string" ? value : String(value);
|
|
218
|
+
if (s.indexOf("\"") !== -1 || s.indexOf(",") !== -1 ||
|
|
219
|
+
s.indexOf("\r") !== -1 || s.indexOf("\n") !== -1) {
|
|
220
|
+
return "\"" + s.replace(/"/g, "\"\"") + "\"";
|
|
221
|
+
}
|
|
222
|
+
return s;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
parse: parse,
|
|
227
|
+
stringify: stringify,
|
|
228
|
+
CsvError: CsvError,
|
|
229
|
+
};
|
package/lib/uuid.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* uuid — RFC 4122 v4 (random) + RFC 9562 v7 (time-ordered).
|
|
4
|
+
*
|
|
5
|
+
* Two flavors:
|
|
6
|
+
*
|
|
7
|
+
* b.uuid.v4() — fully random 128-bit UUID. Standard, portable,
|
|
8
|
+
* the default choice when ordering doesn't matter.
|
|
9
|
+
*
|
|
10
|
+
* b.uuid.v7() — Unix-millisecond timestamp prefix + 74 random
|
|
11
|
+
* bits. Time-ordered (sorts by creation time even
|
|
12
|
+
* lexicographically), ideal as a database PK
|
|
13
|
+
* because B-tree inserts stay near the right edge
|
|
14
|
+
* — no random scattering across the index.
|
|
15
|
+
*
|
|
16
|
+
* b.uuid.parse(str) — { ok, version, bytes }. Validates the canonical
|
|
17
|
+
* 8-4-4-4-12 hex form and the version + variant
|
|
18
|
+
* bits. Returns ok:false (no throw) on bad input.
|
|
19
|
+
*
|
|
20
|
+
* b.uuid.isValid(str) — boolean shorthand. No version/variant check
|
|
21
|
+
* beyond shape — operators who care use parse().
|
|
22
|
+
*
|
|
23
|
+
* All entropy comes from `b.crypto.generateBytes`, which routes through
|
|
24
|
+
* `node:crypto.randomBytes` — same source as `crypto.randomUUID()`.
|
|
25
|
+
*
|
|
26
|
+
* Why ship v7 ourselves? Native `crypto.randomUUID()` only emits v4.
|
|
27
|
+
* v7 is the modern recommendation for any UUID landing in a sortable
|
|
28
|
+
* column (jobs queue, audit chain extensions, anything where insertion
|
|
29
|
+
* order matters for index locality).
|
|
30
|
+
*/
|
|
31
|
+
var { generateBytes } = require("./crypto");
|
|
32
|
+
|
|
33
|
+
// Canonical UUID layout: 8-4-4-4-12 hex digits, version nibble at byte
|
|
34
|
+
// 6 high-nibble, variant bits at byte 8 high two bits (must be 10).
|
|
35
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
36
|
+
// Loose form for `isValid` shape-only check — accepts any version 1-8 and
|
|
37
|
+
// any variant top-2-bit value. Operators wanting strict version+variant
|
|
38
|
+
// gating use `parse()`.
|
|
39
|
+
var UUID_LOOSE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
40
|
+
|
|
41
|
+
function _bytesToString(bytes) {
|
|
42
|
+
var hex = bytes.toString("hex");
|
|
43
|
+
return hex.slice(0, 8) + "-" +
|
|
44
|
+
hex.slice(8, 12) + "-" +
|
|
45
|
+
hex.slice(12, 16) + "-" +
|
|
46
|
+
hex.slice(16, 20) + "-" +
|
|
47
|
+
hex.slice(20, 32);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function v4() {
|
|
51
|
+
var b = generateBytes(16);
|
|
52
|
+
// version = 4 (0100): high nibble of byte 6
|
|
53
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
54
|
+
// variant = RFC 4122 (10xx): top two bits of byte 8
|
|
55
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
56
|
+
return _bytesToString(b);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function v7(opts) {
|
|
60
|
+
// RFC 9562 §5.7 layout:
|
|
61
|
+
// bytes 0-5 : 48-bit big-endian Unix timestamp in milliseconds
|
|
62
|
+
// bytes 6-7 : version nibble (7) + 12 bits random_a
|
|
63
|
+
// bytes 8-15 : variant bits + 62 bits random_b
|
|
64
|
+
var ms = (opts && typeof opts.now === "number") ? opts.now : Date.now();
|
|
65
|
+
var b = generateBytes(16);
|
|
66
|
+
// 48-bit ms timestamp (big-endian) into bytes 0-5
|
|
67
|
+
// ms can exceed 2^32 (we're in 2026, ms is ~1.78e12), so use Math + bit ops carefully
|
|
68
|
+
var msHi = Math.floor(ms / 0x100000000); // top 16 bits live in low 16 of msHi
|
|
69
|
+
var msLo = ms >>> 0; // bottom 32 bits unsigned
|
|
70
|
+
b[0] = (msHi >> 8) & 0xff;
|
|
71
|
+
b[1] = msHi & 0xff;
|
|
72
|
+
b[2] = (msLo >>> 24) & 0xff;
|
|
73
|
+
b[3] = (msLo >>> 16) & 0xff;
|
|
74
|
+
b[4] = (msLo >>> 8) & 0xff;
|
|
75
|
+
b[5] = msLo & 0xff;
|
|
76
|
+
// version = 7 (0111) in high nibble of byte 6, random_a in low nibble + byte 7
|
|
77
|
+
b[6] = (b[6] & 0x0f) | 0x70;
|
|
78
|
+
// variant = RFC 4122 (10xx) in top two bits of byte 8
|
|
79
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
80
|
+
return _bytesToString(b);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parse(str) {
|
|
84
|
+
if (typeof str !== "string") return { ok: false, reason: "not-a-string" };
|
|
85
|
+
if (!UUID_RE.test(str)) return { ok: false, reason: "malformed" };
|
|
86
|
+
var hex = str.replace(/-/g, "");
|
|
87
|
+
var bytes = Buffer.from(hex, "hex");
|
|
88
|
+
// Version is the high nibble of byte 6.
|
|
89
|
+
var version = (bytes[6] >> 4) & 0x0f;
|
|
90
|
+
// Variant: top two bits of byte 8 must be 10 for RFC 4122 / 9562.
|
|
91
|
+
var variant = (bytes[8] >> 6) & 0x03;
|
|
92
|
+
if (variant !== 0b10) return { ok: false, reason: "bad-variant" };
|
|
93
|
+
return { ok: true, version: version, bytes: bytes };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isValid(str) {
|
|
97
|
+
return typeof str === "string" && UUID_LOOSE_RE.test(str);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
v4: v4,
|
|
102
|
+
v7: v7,
|
|
103
|
+
parse: parse,
|
|
104
|
+
isValid: isValid,
|
|
105
|
+
};
|