@blamejs/core 0.5.15 → 0.5.17
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 +2 -0
- package/lib/backup/index.js +2 -2
- package/lib/cache.js +5 -9
- package/lib/cli.js +4 -3
- package/lib/cluster.js +4 -4
- package/lib/csv.js +208 -162
- package/lib/db.js +5 -5
- package/lib/external-db.js +4 -3
- package/lib/forms.js +3 -6
- package/lib/http-client.js +2 -2
- package/lib/mail.js +2 -2
- package/lib/middleware/body-parser.js +2 -1
- package/lib/middleware/compression.js +3 -2
- package/lib/middleware/cors.js +1 -1
- package/lib/middleware/rate-limit.js +4 -4
- package/lib/mtls-engine-default.js +3 -2
- package/lib/nonce-store.js +4 -4
- package/lib/otel-export.js +267 -0
- package/lib/parsers/index.js +3 -7
- package/lib/queue.js +6 -7
- package/lib/request-helpers.js +35 -3
- package/lib/restore.js +3 -3
- package/lib/safe-async.js +122 -0
- package/lib/tracing.js +3 -3
- package/lib/webhook.js +3 -3
- package/lib/websocket.js +6 -4
- package/package.json +1 -1
- package/lib/parsers/safe-csv.js +0 -224
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.16** (2026-04-30) — b.otelExport: OTLP/HTTP exporter for b.observability
|
|
12
|
+
- **0.5.15** (2026-04-30) — b.archive: ZIP creation
|
|
11
13
|
- **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
|
|
12
14
|
- **0.5.13** (2026-04-30) — b.testing.request: supertest-style chainable HTTP test helper
|
|
13
15
|
- **0.5.12** (2026-04-30) — b.middleware.requestLog: HTTP access-log middleware
|
package/index.js
CHANGED
|
@@ -145,6 +145,7 @@ var pagination = require("./lib/pagination");
|
|
|
145
145
|
var metrics = require("./lib/metrics");
|
|
146
146
|
var tracing = require("./lib/tracing");
|
|
147
147
|
var observability = require("./lib/observability");
|
|
148
|
+
var otelExport = require("./lib/otel-export");
|
|
148
149
|
var protocolDispatcher = require("./lib/protocol-dispatcher");
|
|
149
150
|
var requestHelpers = require("./lib/request-helpers");
|
|
150
151
|
var appShutdown = require("./lib/app-shutdown");
|
|
@@ -251,6 +252,7 @@ module.exports = {
|
|
|
251
252
|
metrics: metrics,
|
|
252
253
|
tracing: tracing,
|
|
253
254
|
observability: observability,
|
|
255
|
+
otelExport: otelExport,
|
|
254
256
|
protocolDispatcher: protocolDispatcher,
|
|
255
257
|
requestHelpers: requestHelpers,
|
|
256
258
|
appShutdown: appShutdown,
|
package/lib/backup/index.js
CHANGED
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
62
|
var fs = require("fs");
|
|
63
|
-
var nodeCrypto = require("node:crypto");
|
|
64
63
|
var os = require("os");
|
|
65
64
|
var path = require("path");
|
|
65
|
+
var crypto = require("../crypto");
|
|
66
66
|
var atomicFile = require("../atomic-file");
|
|
67
67
|
var backupBundle = require("./bundle");
|
|
68
68
|
var lazyRequire = require("../lazy-require");
|
|
@@ -80,7 +80,7 @@ function _isValidBundleId(s) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function _generateBundleId() {
|
|
83
|
-
return atomicFile.pathTimestamp() + "-" +
|
|
83
|
+
return atomicFile.pathTimestamp() + "-" + crypto.generateToken(4);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function _dirSize(p) {
|
package/lib/cache.js
CHANGED
|
@@ -94,6 +94,7 @@ var clusterStorage = require("./cluster-storage");
|
|
|
94
94
|
var C = require("./constants");
|
|
95
95
|
var lazyRequire = require("./lazy-require");
|
|
96
96
|
var requestHelpers = require("./request-helpers");
|
|
97
|
+
var safeAsync = require("./safe-async");
|
|
97
98
|
var validateOpts = require("./validate-opts");
|
|
98
99
|
var { CacheError } = require("./framework-error");
|
|
99
100
|
|
|
@@ -434,12 +435,11 @@ function _memoryBackend(cfg) {
|
|
|
434
435
|
|
|
435
436
|
function _startSweep(intervalMs) {
|
|
436
437
|
if (sweepTimer) return;
|
|
437
|
-
sweepTimer =
|
|
438
|
-
if (typeof sweepTimer.unref === "function") sweepTimer.unref();
|
|
438
|
+
sweepTimer = safeAsync.repeating(_sweep, intervalMs, { name: "cache-sweep" });
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
async function close() {
|
|
442
|
-
if (sweepTimer) {
|
|
442
|
+
if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
|
|
443
443
|
entries.clear();
|
|
444
444
|
tagIndex.clear();
|
|
445
445
|
totalBytes = 0;
|
|
@@ -579,15 +579,11 @@ function _clusterBackend(cfg) {
|
|
|
579
579
|
}
|
|
580
580
|
|
|
581
581
|
function _startSweep(intervalMs) {
|
|
582
|
-
|
|
583
|
-
_sweep().catch(function () { /* sweeper best-effort; next pass picks it up */ });
|
|
584
|
-
}, intervalMs);
|
|
585
|
-
if (typeof t.unref === "function") t.unref();
|
|
586
|
-
cfg._sweepTimer = t;
|
|
582
|
+
cfg._sweepTimer = safeAsync.repeating(_sweep, intervalMs, { name: "cache-sweep-cluster" });
|
|
587
583
|
}
|
|
588
584
|
|
|
589
585
|
async function close() {
|
|
590
|
-
if (cfg._sweepTimer) {
|
|
586
|
+
if (cfg._sweepTimer) { cfg._sweepTimer.stop(); cfg._sweepTimer = null; }
|
|
591
587
|
}
|
|
592
588
|
|
|
593
589
|
return {
|
package/lib/cli.js
CHANGED
|
@@ -35,15 +35,16 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
var fs = require("node:fs");
|
|
38
|
-
var nodeCrypto = require("node:crypto");
|
|
39
38
|
var os = require("node:os");
|
|
40
39
|
var path = require("path");
|
|
41
40
|
var apiSnapshot = require("./api-snapshot");
|
|
42
41
|
var auditTools = require("./audit-tools");
|
|
43
42
|
var cliHelpers = require("./cli-helpers");
|
|
44
43
|
var constants = require("./constants");
|
|
44
|
+
var crypto = require("./crypto");
|
|
45
45
|
var dev = require("./dev");
|
|
46
46
|
var migrations = require("./migrations");
|
|
47
|
+
var requestHelpers = require("./request-helpers");
|
|
47
48
|
var restoreBundle = require("./restore-bundle");
|
|
48
49
|
var seeders = require("./seeders");
|
|
49
50
|
var vaultPassphraseOps = require("./vault/passphrase-ops");
|
|
@@ -746,7 +747,7 @@ async function _runApiKey(args, ctx) {
|
|
|
746
747
|
var scopes = args.flags.scopes;
|
|
747
748
|
if (!ownerId || ownerId === true) return report.error("--owner-id <id> is required", 2);
|
|
748
749
|
if (!scopes || scopes === true) return report.error("--scopes <comma-separated> is required", 2);
|
|
749
|
-
var scopeList =
|
|
750
|
+
var scopeList = requestHelpers.parseListHeader(scopes);
|
|
750
751
|
if (scopeList.length === 0) {
|
|
751
752
|
return report.error("--scopes must contain at least one non-empty scope", 2);
|
|
752
753
|
}
|
|
@@ -902,7 +903,7 @@ async function _runBackup(args, ctx) {
|
|
|
902
903
|
|
|
903
904
|
if (sub === "verify") {
|
|
904
905
|
var stagingDir = path.join(os.tmpdir(),
|
|
905
|
-
"blamejs-backup-verify-" +
|
|
906
|
+
"blamejs-backup-verify-" + crypto.generateToken(8));
|
|
906
907
|
try {
|
|
907
908
|
var r = await restoreBundle.extract({
|
|
908
909
|
bundleDir: bundleDir,
|
package/lib/cluster.js
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
*/
|
|
53
53
|
var C = require("./constants");
|
|
54
54
|
var { boot } = require("./log");
|
|
55
|
+
var safeAsync = require("./safe-async");
|
|
55
56
|
var safeUrl = require("./safe-url");
|
|
56
57
|
var { FrameworkError, ClusterError } = require("./framework-error");
|
|
57
58
|
|
|
@@ -227,8 +228,7 @@ async function init(opts) {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
// Start heartbeat
|
|
230
|
-
heartbeatTimer =
|
|
231
|
-
heartbeatTimer.unref();
|
|
231
|
+
heartbeatTimer = safeAsync.repeating(_heartbeat, heartbeatMs, { name: "cluster-heartbeat" });
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
// Cluster-mode equivalent of db.js's single-node audit.tip-sidecar
|
|
@@ -619,7 +619,7 @@ function onTransition(handler) {
|
|
|
619
619
|
async function shutdown() {
|
|
620
620
|
if (!initialized) return;
|
|
621
621
|
if (heartbeatTimer) {
|
|
622
|
-
|
|
622
|
+
heartbeatTimer.stop();
|
|
623
623
|
heartbeatTimer = null;
|
|
624
624
|
}
|
|
625
625
|
if (lease) {
|
|
@@ -649,7 +649,7 @@ async function shutdown() {
|
|
|
649
649
|
// ---- test helpers — not part of public contract ----
|
|
650
650
|
|
|
651
651
|
function _resetForTest() {
|
|
652
|
-
if (heartbeatTimer)
|
|
652
|
+
if (heartbeatTimer) heartbeatTimer.stop();
|
|
653
653
|
heartbeatTimer = null;
|
|
654
654
|
initialized = false;
|
|
655
655
|
terminated = false;
|
package/lib/csv.js
CHANGED
|
@@ -1,229 +1,275 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* csv — RFC 4180 parser + serializer.
|
|
3
|
+
* csv — RFC 4180 parser + serializer with operator-friendly defaults.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* var rows = b.csv.parse(text);
|
|
6
|
+
* // header row by default — returns array of objects keyed by header
|
|
7
|
+
* // [ { name: "alice", age: "30" }, ... ]
|
|
6
8
|
*
|
|
7
|
-
* b.csv.parse(text,
|
|
8
|
-
*
|
|
9
|
+
* var arrays = b.csv.parse(text, { header: false });
|
|
10
|
+
* // returns array of arrays
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
12
|
+
* var out = b.csv.stringify(rows);
|
|
13
|
+
* // RFC 4180 + Excel-formula-injection prevention by default
|
|
18
14
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
15
|
+
* Defaults:
|
|
16
|
+
* parse:
|
|
17
|
+
* header: true first row is column names
|
|
18
|
+
* delimiter: "," single byte; "\t" / ";" supported
|
|
19
|
+
* quote: '"'
|
|
20
|
+
* trim: false cell whitespace preserved
|
|
21
|
+
* maxBytes: 16 MiB
|
|
22
|
+
* maxRows: 1,000,000
|
|
23
|
+
* maxFieldBytes: 1 MiB
|
|
24
|
+
* onBadRow: "throw" "skip" tolerates short/long rows
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* stringify:
|
|
27
|
+
* header: true (or array of explicit columns)
|
|
28
|
+
* delimiter: ","
|
|
29
|
+
* quote: '"'
|
|
30
|
+
* eol: "\r\n" RFC 4180; "\n" via opt
|
|
31
|
+
* alwaysQuote: false only quote when needed
|
|
32
|
+
* preventFormulaInjection: true prefix '=' / '+' / '-' / '@' / TAB / CR
|
|
33
|
+
* cells with "'" so Excel doesn't
|
|
34
|
+
* execute them when the CSV is opened
|
|
35
|
+
* formulaPrefixChars: ["=","+","-","@","\t","\r"]
|
|
30
36
|
*
|
|
31
|
-
*
|
|
37
|
+
* SECURITY: stringify defends against CSV-injection (Excel/Sheets execute
|
|
38
|
+
* cells starting with "=" / "+" / "-" / "@" / TAB / CR as formulas).
|
|
39
|
+
* Operator-controlled cells stay safe; user-supplied cells get the leading
|
|
40
|
+
* single-quote so the formula engine sees a literal string.
|
|
41
|
+
*
|
|
42
|
+
* Throws CsvError (FrameworkError, permanent) on shape violations.
|
|
32
43
|
*/
|
|
33
44
|
var C = require("./constants");
|
|
34
45
|
var { defineClass } = require("./framework-error");
|
|
35
46
|
|
|
36
47
|
var CsvError = defineClass("CsvError", { alwaysPermanent: true });
|
|
37
48
|
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
var DEFAULTS_PARSE = {
|
|
50
|
+
header: true,
|
|
51
|
+
delimiter: ",",
|
|
52
|
+
quote: "\"",
|
|
53
|
+
trim: false,
|
|
54
|
+
maxBytes: C.BYTES.mib(16),
|
|
55
|
+
maxRows: 1000000,
|
|
56
|
+
maxFieldBytes: C.BYTES.mib(1),
|
|
57
|
+
onBadRow: "throw",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
var DEFAULTS_STRINGIFY = {
|
|
61
|
+
header: true,
|
|
62
|
+
delimiter: ",",
|
|
63
|
+
quote: "\"",
|
|
64
|
+
eol: "\r\n",
|
|
65
|
+
alwaysQuote: false,
|
|
66
|
+
preventFormulaInjection: true,
|
|
67
|
+
formulaPrefixChars: ["=", "+", "-", "@", "\t", "\r"],
|
|
68
|
+
columns: null,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function _validateDelim(name, value) {
|
|
72
|
+
if (typeof value !== "string" || value.length !== 1) {
|
|
73
|
+
throw new CsvError("csv/bad-delimiter",
|
|
74
|
+
name + " must be a single character, got " + JSON.stringify(value));
|
|
75
|
+
}
|
|
76
|
+
if (value === "\r" || value === "\n") {
|
|
77
|
+
throw new CsvError("csv/bad-delimiter",
|
|
78
|
+
name + " cannot be CR or LF");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
41
81
|
|
|
42
82
|
// ---- parse ----
|
|
43
83
|
|
|
44
|
-
function parse(
|
|
45
|
-
opts = opts || {};
|
|
46
|
-
|
|
84
|
+
function parse(input, opts) {
|
|
85
|
+
opts = Object.assign({}, DEFAULTS_PARSE, opts || {});
|
|
86
|
+
|
|
87
|
+
var s;
|
|
88
|
+
if (typeof input === "string") s = input;
|
|
89
|
+
else if (Buffer.isBuffer(input)) s = input.toString("utf8");
|
|
90
|
+
else if (input instanceof Uint8Array) s = Buffer.from(input).toString("utf8");
|
|
91
|
+
else {
|
|
47
92
|
throw new CsvError("csv/bad-input",
|
|
48
|
-
"parse: input must be
|
|
93
|
+
"parse: input must be string, Buffer, or Uint8Array, got " + typeof input);
|
|
49
94
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (Buffer.byteLength(s, "utf8") > maxBytes) {
|
|
95
|
+
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
|
|
96
|
+
if (Buffer.byteLength(s, "utf8") > opts.maxBytes) {
|
|
53
97
|
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));
|
|
98
|
+
"parse: input exceeds maxBytes (" + opts.maxBytes + ")");
|
|
60
99
|
}
|
|
61
|
-
|
|
100
|
+
|
|
101
|
+
_validateDelim("delimiter", opts.delimiter);
|
|
102
|
+
_validateDelim("quote", opts.quote);
|
|
103
|
+
if (opts.delimiter === opts.quote) {
|
|
62
104
|
throw new CsvError("csv/bad-delimiter",
|
|
63
|
-
"
|
|
105
|
+
"delimiter and quote must differ");
|
|
64
106
|
}
|
|
65
|
-
|
|
66
|
-
var onBadRow = opts.onBadRow || "throw";
|
|
67
|
-
if (onBadRow !== "throw" && onBadRow !== "skip") {
|
|
107
|
+
if (opts.onBadRow !== "throw" && opts.onBadRow !== "skip") {
|
|
68
108
|
throw new CsvError("csv/bad-opt",
|
|
69
|
-
"
|
|
109
|
+
"onBadRow must be 'throw' or 'skip'");
|
|
70
110
|
}
|
|
71
111
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
var len = s.length;
|
|
113
|
+
var pos = 0;
|
|
75
114
|
var rows = [];
|
|
76
|
-
var field = "";
|
|
77
115
|
var row = [];
|
|
78
|
-
var
|
|
79
|
-
var
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
116
|
+
var field = "";
|
|
117
|
+
var inQuote = false;
|
|
118
|
+
|
|
119
|
+
function pushField() {
|
|
120
|
+
if (Buffer.byteLength(field, "utf8") > opts.maxFieldBytes) {
|
|
121
|
+
throw new CsvError("csv/field-too-large",
|
|
122
|
+
"field exceeds maxFieldBytes at row " + (rows.length + 1));
|
|
123
|
+
}
|
|
124
|
+
row.push(opts.trim ? field.trim() : field);
|
|
125
|
+
field = "";
|
|
126
|
+
}
|
|
127
|
+
function pushRow() {
|
|
128
|
+
pushField();
|
|
129
|
+
rows.push(row);
|
|
130
|
+
if (rows.length > opts.maxRows) {
|
|
131
|
+
throw new CsvError("csv/too-many-rows",
|
|
132
|
+
"row count exceeds maxRows (" + opts.maxRows + ")");
|
|
133
|
+
}
|
|
134
|
+
row = [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
while (pos < len) {
|
|
138
|
+
var ch = s.charAt(pos);
|
|
139
|
+
if (inQuote) {
|
|
140
|
+
if (ch === opts.quote) {
|
|
141
|
+
if (pos + 1 < len && s.charAt(pos + 1) === opts.quote) {
|
|
142
|
+
field += opts.quote;
|
|
143
|
+
pos += 2;
|
|
88
144
|
continue;
|
|
89
145
|
}
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
inQuote = false;
|
|
147
|
+
pos += 1;
|
|
92
148
|
continue;
|
|
93
149
|
}
|
|
94
|
-
field +=
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
150
|
+
field += ch;
|
|
151
|
+
pos += 1;
|
|
152
|
+
} else {
|
|
153
|
+
if (ch === opts.delimiter) {
|
|
154
|
+
pushField();
|
|
155
|
+
pos += 1;
|
|
156
|
+
} else if (ch === "\r") {
|
|
157
|
+
pushRow();
|
|
158
|
+
pos += 1;
|
|
159
|
+
if (pos < len && s.charAt(pos) === "\n") pos += 1;
|
|
160
|
+
} else if (ch === "\n") {
|
|
161
|
+
pushRow();
|
|
162
|
+
pos += 1;
|
|
163
|
+
} else if (ch === opts.quote && field === "") {
|
|
164
|
+
inQuote = true;
|
|
165
|
+
pos += 1;
|
|
166
|
+
} else {
|
|
167
|
+
field += ch;
|
|
168
|
+
pos += 1;
|
|
107
169
|
}
|
|
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
170
|
}
|
|
136
|
-
field += c;
|
|
137
|
-
i++;
|
|
138
171
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
rows.push(row);
|
|
172
|
+
if (inQuote) {
|
|
173
|
+
throw new CsvError("csv/unterminated-quote",
|
|
174
|
+
"unterminated quoted field");
|
|
143
175
|
}
|
|
176
|
+
if (field.length > 0 || row.length > 0) pushRow();
|
|
144
177
|
|
|
145
|
-
if (!
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return { headers: [], rows: [] };
|
|
150
|
-
}
|
|
151
|
-
var headers = rows[0];
|
|
178
|
+
if (!opts.header) return rows;
|
|
179
|
+
if (rows.length === 0) return [];
|
|
180
|
+
|
|
181
|
+
var header = rows[0];
|
|
152
182
|
var out = [];
|
|
153
183
|
for (var r = 1; r < rows.length; r++) {
|
|
154
|
-
var
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
if (onBadRow === "skip") continue;
|
|
184
|
+
var rec = rows[r];
|
|
185
|
+
if (rec.length !== header.length) {
|
|
186
|
+
if (opts.onBadRow === "skip") continue;
|
|
158
187
|
throw new CsvError("csv/row-length-mismatch",
|
|
159
|
-
"
|
|
188
|
+
"row " + r + " has " + rec.length + " fields, header has " + header.length);
|
|
160
189
|
}
|
|
161
190
|
var obj = {};
|
|
162
|
-
for (var
|
|
191
|
+
for (var i = 0; i < header.length; i++) obj[header[i]] = rec[i];
|
|
163
192
|
out.push(obj);
|
|
164
193
|
}
|
|
165
|
-
return
|
|
194
|
+
return out;
|
|
166
195
|
}
|
|
167
196
|
|
|
168
197
|
// ---- stringify ----
|
|
169
198
|
|
|
170
199
|
function stringify(rows, opts) {
|
|
171
|
-
opts = opts || {};
|
|
200
|
+
opts = Object.assign({}, DEFAULTS_STRINGIFY, opts || {});
|
|
172
201
|
if (!Array.isArray(rows)) {
|
|
173
202
|
throw new CsvError("csv/bad-input",
|
|
174
|
-
"stringify: rows must be an array");
|
|
203
|
+
"stringify: rows must be an array, got " + typeof rows);
|
|
204
|
+
}
|
|
205
|
+
_validateDelim("delimiter", opts.delimiter);
|
|
206
|
+
_validateDelim("quote", opts.quote);
|
|
207
|
+
if (opts.delimiter === opts.quote) {
|
|
208
|
+
throw new CsvError("csv/bad-delimiter",
|
|
209
|
+
"delimiter and quote must differ");
|
|
210
|
+
}
|
|
211
|
+
if (opts.eol !== "\r\n" && opts.eol !== "\n") {
|
|
212
|
+
throw new CsvError("csv/bad-opt",
|
|
213
|
+
"eol must be '\\r\\n' or '\\n'");
|
|
175
214
|
}
|
|
176
|
-
var delimiter = opts.delimiter || DEFAULT_DELIMITER;
|
|
177
|
-
var eol = opts.eol || DEFAULT_EOL;
|
|
178
|
-
var hasHeader = opts.header !== false;
|
|
179
215
|
if (rows.length === 0) return "";
|
|
180
216
|
|
|
181
|
-
var
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
} else if (
|
|
188
|
-
|
|
217
|
+
var first = rows[0];
|
|
218
|
+
var asObjects;
|
|
219
|
+
var header;
|
|
220
|
+
if (Array.isArray(first)) {
|
|
221
|
+
asObjects = false;
|
|
222
|
+
header = Array.isArray(opts.columns) ? opts.columns : null;
|
|
223
|
+
} else if (first !== null && typeof first === "object") {
|
|
224
|
+
asObjects = true;
|
|
225
|
+
header = Array.isArray(opts.columns) ? opts.columns : Object.keys(first);
|
|
189
226
|
} else {
|
|
190
227
|
throw new CsvError("csv/bad-input",
|
|
191
|
-
"stringify: rows
|
|
228
|
+
"stringify: rows must be arrays or plain objects");
|
|
192
229
|
}
|
|
193
230
|
|
|
194
|
-
var
|
|
195
|
-
|
|
231
|
+
var prefixSet = Object.create(null);
|
|
232
|
+
for (var pi = 0; pi < opts.formulaPrefixChars.length; pi++) {
|
|
233
|
+
prefixSet[opts.formulaPrefixChars[pi]] = true;
|
|
234
|
+
}
|
|
196
235
|
|
|
197
|
-
|
|
198
|
-
var
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
236
|
+
function escapeCell(value) {
|
|
237
|
+
var str = value == null ? "" : String(value);
|
|
238
|
+
if (opts.preventFormulaInjection && str.length > 0 && prefixSet[str.charAt(0)]) {
|
|
239
|
+
str = "'" + str;
|
|
240
|
+
}
|
|
241
|
+
var needsQuote = opts.alwaysQuote ||
|
|
242
|
+
str.indexOf(opts.delimiter) !== -1 ||
|
|
243
|
+
str.indexOf(opts.quote) !== -1 ||
|
|
244
|
+
str.indexOf("\n") !== -1 ||
|
|
245
|
+
str.indexOf("\r") !== -1;
|
|
246
|
+
if (needsQuote) {
|
|
247
|
+
str = opts.quote + str.split(opts.quote).join(opts.quote + opts.quote) + opts.quote;
|
|
209
248
|
}
|
|
210
|
-
|
|
249
|
+
return str;
|
|
211
250
|
}
|
|
212
|
-
return lines.join(eol) + eol;
|
|
213
|
-
}
|
|
214
251
|
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
252
|
+
var out = [];
|
|
253
|
+
if (opts.header && header) {
|
|
254
|
+
out.push(header.map(escapeCell).join(opts.delimiter));
|
|
255
|
+
}
|
|
256
|
+
for (var i = 0; i < rows.length; i++) {
|
|
257
|
+
var rec = rows[i];
|
|
258
|
+
var cells;
|
|
259
|
+
if (asObjects) {
|
|
260
|
+
cells = header.map(function (k) { return escapeCell(rec[k]); });
|
|
261
|
+
} else {
|
|
262
|
+
cells = rec.map(escapeCell);
|
|
263
|
+
}
|
|
264
|
+
out.push(cells.join(opts.delimiter));
|
|
221
265
|
}
|
|
222
|
-
return
|
|
266
|
+
return out.join(opts.eol);
|
|
223
267
|
}
|
|
224
268
|
|
|
225
269
|
module.exports = {
|
|
226
|
-
parse:
|
|
227
|
-
stringify:
|
|
228
|
-
CsvError:
|
|
270
|
+
parse: parse,
|
|
271
|
+
stringify: stringify,
|
|
272
|
+
CsvError: CsvError,
|
|
273
|
+
DEFAULTS_PARSE: DEFAULTS_PARSE,
|
|
274
|
+
DEFAULTS_STRINGIFY: DEFAULTS_STRINGIFY,
|
|
229
275
|
};
|
package/lib/db.js
CHANGED
|
@@ -55,6 +55,7 @@ var cryptoField = require("./crypto-field");
|
|
|
55
55
|
var { Query } = require("./db-query");
|
|
56
56
|
var dbSchema = require("./db-schema");
|
|
57
57
|
var { boot } = require("./log");
|
|
58
|
+
var safeAsync = require("./safe-async");
|
|
58
59
|
var safeEnv = require("./parsers/safe-env");
|
|
59
60
|
var safeJson = require("./safe-json");
|
|
60
61
|
var vault = require("./vault");
|
|
@@ -797,12 +798,11 @@ async function init(opts) {
|
|
|
797
798
|
|
|
798
799
|
// Start periodic encrypt timer (encrypted mode only)
|
|
799
800
|
if (atRest === "encrypted") {
|
|
800
|
-
encTimer =
|
|
801
|
+
encTimer = safeAsync.repeating(function () {
|
|
801
802
|
try { encryptToDisk(); } catch (e) {
|
|
802
803
|
log.error("periodic encrypt failed: " + e.message);
|
|
803
804
|
}
|
|
804
|
-
}, C.TIME.minutes(5));
|
|
805
|
-
encTimer.unref();
|
|
805
|
+
}, C.TIME.minutes(5), { name: "db-periodic-encrypt" });
|
|
806
806
|
|
|
807
807
|
// Final encrypt on process exit. We don't try to unlink the plaintext
|
|
808
808
|
// here — the SQLite handle may still be open, and the OS reclaims tmpfs
|
|
@@ -914,7 +914,7 @@ function hashFor(table, field, value) {
|
|
|
914
914
|
function close() {
|
|
915
915
|
if (!initialized) return;
|
|
916
916
|
if (encTimer) {
|
|
917
|
-
|
|
917
|
+
encTimer.stop();
|
|
918
918
|
encTimer = null;
|
|
919
919
|
}
|
|
920
920
|
// Best-effort final checkpoint before shutdown so the audit.tip sidecar
|
|
@@ -1070,7 +1070,7 @@ async function _runNtpBootCheck(opts) {
|
|
|
1070
1070
|
|
|
1071
1071
|
// Test helpers — not part of public contract
|
|
1072
1072
|
function _resetForTest() {
|
|
1073
|
-
if (encTimer) {
|
|
1073
|
+
if (encTimer) { encTimer.stop(); encTimer = null; }
|
|
1074
1074
|
try { if (database) database.close(); } catch (_e) {}
|
|
1075
1075
|
database = null;
|
|
1076
1076
|
dbPath = null;
|