@blamejs/blamejs-shop 0.4.49 → 0.4.50
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/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +58 -46
- package/lib/vendor/blamejs/.github/workflows/ci.yml +134 -1
- package/lib/vendor/blamejs/.gitignore +5 -1
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +3 -1
- package/lib/vendor/blamejs/api-snapshot.json +10 -2
- package/lib/vendor/blamejs/lib/bundler.js +2 -7
- package/lib/vendor/blamejs/lib/config-drift.js +17 -3
- package/lib/vendor/blamejs/lib/crypto-field.js +30 -0
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +20 -1
- package/lib/vendor/blamejs/lib/db-schema.js +29 -0
- package/lib/vendor/blamejs/lib/db.js +7 -0
- package/lib/vendor/blamejs/lib/guard-csv.js +13 -4
- package/lib/vendor/blamejs/lib/local-db-thin.js +23 -1
- package/lib/vendor/blamejs/lib/mail-bimi.js +16 -3
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -5
- package/lib/vendor/blamejs/lib/mail.js +16 -9
- package/lib/vendor/blamejs/lib/mcp.js +28 -6
- package/lib/vendor/blamejs/lib/middleware/bot-disclose.js +7 -5
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -4
- package/lib/vendor/blamejs/lib/numeric-bounds.js +32 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +12 -1
- package/lib/vendor/blamejs/lib/object-store/http-put.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/index.js +4 -0
- package/lib/vendor/blamejs/lib/object-store/local.js +11 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +86 -5
- package/lib/vendor/blamejs/lib/parsers/safe-env.js +6 -3
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +6 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +69 -1
- package/lib/vendor/blamejs/lib/safe-decompress.js +3 -12
- package/lib/vendor/blamejs/lib/seeders.js +33 -39
- package/lib/vendor/blamejs/lib/storage.js +71 -7
- package/lib/vendor/blamejs/lib/vault/rotate.js +4 -13
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.10.json +53 -0
- package/lib/vendor/blamejs/release-notes/v0.15.11.json +52 -0
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +90 -16
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +150 -39
- package/lib/vendor/blamejs/test/layer-0-primitives/config-drift.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-aad-downgrade.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-transaction.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/declare-row-policy.test.js +43 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/local-db-thin.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/numeric-bounds.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-versioned-delete.test.js +97 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-5-integration/bundler-output.test.js +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dbSchema.runInTransaction / runInTransactionAsync — the BEGIN / COMMIT /
|
|
4
|
+
* ROLLBACK wrappers that seeders, migrations, and vault-rotate route through
|
|
5
|
+
* instead of hand-rolling the transaction skeleton. A fake handle records the
|
|
6
|
+
* exact statement sequence so the commit-on-success / rollback-on-failure
|
|
7
|
+
* contract — and, for the async form, that COMMIT waits for the awaited body —
|
|
8
|
+
* is asserted directly, without standing up a real SQLite database.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var helpers = require("../helpers");
|
|
12
|
+
var check = helpers.check;
|
|
13
|
+
var dbSchema = require("../../lib/db-schema");
|
|
14
|
+
|
|
15
|
+
// A handle exposing exec() — the shape runSqlOnHandle drives. It records every
|
|
16
|
+
// statement; an optional failOn set makes a named statement (e.g. ROLLBACK)
|
|
17
|
+
// throw so the onRollbackFail path is exercisable.
|
|
18
|
+
function _fakeDb(failOn) {
|
|
19
|
+
var calls = [];
|
|
20
|
+
return {
|
|
21
|
+
_calls: calls,
|
|
22
|
+
exec: function (sql) {
|
|
23
|
+
calls.push(sql);
|
|
24
|
+
if (failOn && failOn[sql]) throw new Error("exec failed: " + sql);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function testSyncCommitAndRollback() {
|
|
30
|
+
var okDb = _fakeDb();
|
|
31
|
+
var ret = dbSchema.runInTransaction(okDb, function () {
|
|
32
|
+
okDb.exec("WORK");
|
|
33
|
+
return 42;
|
|
34
|
+
});
|
|
35
|
+
check("runInTransaction returns the fn result", ret === 42);
|
|
36
|
+
check("runInTransaction sequence is BEGIN, WORK, COMMIT",
|
|
37
|
+
okDb._calls.join(",") === "BEGIN,WORK,COMMIT");
|
|
38
|
+
|
|
39
|
+
var failDb = _fakeDb();
|
|
40
|
+
var threw = null;
|
|
41
|
+
try {
|
|
42
|
+
dbSchema.runInTransaction(failDb, function () { throw new Error("boom"); });
|
|
43
|
+
} catch (e) { threw = e; }
|
|
44
|
+
check("runInTransaction re-throws the body error", threw && threw.message === "boom");
|
|
45
|
+
check("runInTransaction rolls back (BEGIN, ROLLBACK — no COMMIT)",
|
|
46
|
+
failDb._calls.join(",") === "BEGIN,ROLLBACK");
|
|
47
|
+
|
|
48
|
+
// lockMode appends to BEGIN.
|
|
49
|
+
var lockDb = _fakeDb();
|
|
50
|
+
dbSchema.runInTransaction(lockDb, function () {}, { lockMode: "IMMEDIATE" });
|
|
51
|
+
check("runInTransaction honours opts.lockMode", lockDb._calls[0] === "BEGIN IMMEDIATE");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function testAsyncCommitWaitsForBody() {
|
|
55
|
+
var okDb = _fakeDb();
|
|
56
|
+
var ret = await dbSchema.runInTransactionAsync(okDb, async function () {
|
|
57
|
+
// Yield to the event loop, THEN record work — proves COMMIT waits for the
|
|
58
|
+
// awaited body rather than firing synchronously after fn() returns a promise.
|
|
59
|
+
await Promise.resolve();
|
|
60
|
+
okDb.exec("ASYNC-WORK");
|
|
61
|
+
return "done";
|
|
62
|
+
});
|
|
63
|
+
check("runInTransactionAsync returns the awaited result", ret === "done");
|
|
64
|
+
check("runInTransactionAsync commits AFTER the awaited body (BEGIN, ASYNC-WORK, COMMIT)",
|
|
65
|
+
okDb._calls.join(",") === "BEGIN,ASYNC-WORK,COMMIT");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function testAsyncRollbackOnReject() {
|
|
69
|
+
var failDb = _fakeDb();
|
|
70
|
+
var threw = null;
|
|
71
|
+
try {
|
|
72
|
+
await dbSchema.runInTransactionAsync(failDb, async function () {
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
throw new Error("async-boom");
|
|
75
|
+
});
|
|
76
|
+
} catch (e) { threw = e; }
|
|
77
|
+
check("runInTransactionAsync re-throws the rejected body error",
|
|
78
|
+
threw && threw.message === "async-boom");
|
|
79
|
+
check("runInTransactionAsync rolls back on async reject (BEGIN, ROLLBACK)",
|
|
80
|
+
failDb._calls.join(",") === "BEGIN,ROLLBACK");
|
|
81
|
+
|
|
82
|
+
// onRollbackFail fires when ROLLBACK itself throws, and the ORIGINAL error
|
|
83
|
+
// still surfaces (the rollback failure must not mask the body failure).
|
|
84
|
+
var rbFailDb = _fakeDb({ ROLLBACK: true });
|
|
85
|
+
var sawRollbackFail = false;
|
|
86
|
+
var origErr = null;
|
|
87
|
+
try {
|
|
88
|
+
await dbSchema.runInTransactionAsync(rbFailDb, async function () {
|
|
89
|
+
throw new Error("primary");
|
|
90
|
+
}, { onRollbackFail: function () { sawRollbackFail = true; } });
|
|
91
|
+
} catch (e) { origErr = e; }
|
|
92
|
+
check("onRollbackFail invoked when ROLLBACK throws", sawRollbackFail === true);
|
|
93
|
+
check("original body error still surfaces despite rollback failure",
|
|
94
|
+
origErr && origErr.message === "primary");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function run() {
|
|
98
|
+
testSyncCommitAndRollback();
|
|
99
|
+
await testAsyncCommitWaitsForBody();
|
|
100
|
+
await testAsyncRollbackOnReject();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { run: run };
|
|
104
|
+
|
|
105
|
+
if (require.main === module) {
|
|
106
|
+
run().then(
|
|
107
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
|
|
108
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -21,8 +21,15 @@ function _fakeXdb(spec) {
|
|
|
21
21
|
captured.push({ sql: sql, params: (params || []).slice() });
|
|
22
22
|
if (/FROM pg_class/.test(sql)) {
|
|
23
23
|
if (spec.tableExists === false) return { rows: [], rowCount: 0 };
|
|
24
|
+
// A native pg driver hands back a JS boolean for relrowsecurity.
|
|
25
|
+
// `relrowsecurityRaw` lets a test inject exactly what a proxy /
|
|
26
|
+
// ORM / non-native driver returns instead (the string "t"/"f",
|
|
27
|
+
// "true"/"false", or 1/0) without the !! coercion masking it.
|
|
28
|
+
var rel = Object.prototype.hasOwnProperty.call(spec, "relrowsecurityRaw")
|
|
29
|
+
? spec.relrowsecurityRaw
|
|
30
|
+
: !!spec.rlsAlreadyOn;
|
|
24
31
|
return {
|
|
25
|
-
rows: [{ relrowsecurity:
|
|
32
|
+
rows: [{ relrowsecurity: rel }],
|
|
26
33
|
rowCount: 1,
|
|
27
34
|
};
|
|
28
35
|
}
|
|
@@ -157,6 +164,41 @@ async function run() {
|
|
|
157
164
|
check("no withCheck: clause omitted",
|
|
158
165
|
!sqls2.some(function (s) { return /WITH CHECK/.test(s); }));
|
|
159
166
|
|
|
167
|
+
// ---- non-native-boolean driver: the string "f" means RLS-DISABLED ----
|
|
168
|
+
// A proxy / ORM / non-native pg driver returns relrowsecurity as the
|
|
169
|
+
// string "f" (false) rather than a JS boolean. "f" is TRUTHY, so a bare
|
|
170
|
+
// `!relrowsecurity` would read it as "already enabled" and SILENTLY SKIP
|
|
171
|
+
// the ENABLE ROW LEVEL SECURITY — leaving every row in the table
|
|
172
|
+
// unprotected while the migration reports success. ENABLE must still be
|
|
173
|
+
// emitted for "f"/"false"/0/"no"; it must be skipped only for a value
|
|
174
|
+
// that unambiguously means true.
|
|
175
|
+
async function _runMig(relRaw) {
|
|
176
|
+
var x = _fakeXdb({ relrowsecurityRaw: relRaw });
|
|
177
|
+
var m = b.db.declareRowPolicy({
|
|
178
|
+
schema: "public", table: "sessions", name: "rls_coerce",
|
|
179
|
+
using: "user_id = current_setting('app.user_id')::uuid", command: "ALL",
|
|
180
|
+
});
|
|
181
|
+
await m.up(x, ctx);
|
|
182
|
+
return x.captured.map(function (c) { return c.sql; });
|
|
183
|
+
}
|
|
184
|
+
function _hasEnable(sqls) {
|
|
185
|
+
return sqls.some(function (s) { return /ENABLE ROW LEVEL SECURITY/.test(s); });
|
|
186
|
+
}
|
|
187
|
+
check('non-native driver: ENABLE emitted when relrowsecurity is the string "f"',
|
|
188
|
+
_hasEnable(await _runMig("f")));
|
|
189
|
+
check('non-native driver: ENABLE emitted when relrowsecurity is "false"',
|
|
190
|
+
_hasEnable(await _runMig("false")));
|
|
191
|
+
check("non-native driver: ENABLE emitted when relrowsecurity is the number 0",
|
|
192
|
+
_hasEnable(await _runMig(0)));
|
|
193
|
+
check('non-native driver: ENABLE emitted when relrowsecurity is "no"',
|
|
194
|
+
_hasEnable(await _runMig("no")));
|
|
195
|
+
check('enabled-state respected: ENABLE skipped when relrowsecurity is the string "t"',
|
|
196
|
+
!_hasEnable(await _runMig("t")));
|
|
197
|
+
check('enabled-state respected: ENABLE skipped when relrowsecurity is "true"',
|
|
198
|
+
!_hasEnable(await _runMig("true")));
|
|
199
|
+
check("enabled-state respected: ENABLE skipped when relrowsecurity is the number 1",
|
|
200
|
+
!_hasEnable(await _runMig(1)));
|
|
201
|
+
|
|
160
202
|
// ---- table not found → throws ----
|
|
161
203
|
var xdb3 = _fakeXdb({ tableExists: false });
|
|
162
204
|
var threw3 = null;
|
|
@@ -71,8 +71,36 @@ async function run() {
|
|
|
71
71
|
check("localDb.thin: PRAGMA journal_mode=WAL",
|
|
72
72
|
modeRow && String(modeRow.journal_mode).toLowerCase() === "wal");
|
|
73
73
|
|
|
74
|
+
// ---- #320: SQLITE_LIMIT_LENGTH parity (sqlLength cap) ----
|
|
75
|
+
// The thin path now opens with the same 1 MiB sqlLength cap as b.db / the
|
|
76
|
+
// CLI: a >1 MiB raw statement is rejected at parse time. The builder/run
|
|
77
|
+
// path parameterizes values, so this guards prepare()/exec() of raw SQL.
|
|
78
|
+
var hugeSql = "SELECT '" + "x".repeat(1024 * 1024 + 64) + "'";
|
|
79
|
+
threw = null;
|
|
80
|
+
try { handle.prepare(hugeSql); } catch (e) { threw = e; }
|
|
81
|
+
check("localDb.thin: raw statement over the 1 MiB sqlLength cap is rejected at parse",
|
|
82
|
+
threw !== null);
|
|
83
|
+
// A normal statement is unaffected.
|
|
84
|
+
check("localDb.thin: a small statement still prepares", !!handle.prepare("SELECT 2 AS v"));
|
|
85
|
+
|
|
74
86
|
handle.close();
|
|
75
87
|
|
|
88
|
+
// bad limits shape throws at validation.
|
|
89
|
+
threw = null;
|
|
90
|
+
try {
|
|
91
|
+
b.localDb.thin({ file: path.join(tmpDir, "lim.db"),
|
|
92
|
+
schemaSql: "CREATE TABLE t(x)", audit: false, limits: "nope" });
|
|
93
|
+
} catch (e) { threw = e; }
|
|
94
|
+
check("localDb.thin: non-object limits throws", threw && threw.code === "localdb-thin/bad-limits");
|
|
95
|
+
|
|
96
|
+
// opts.limits raises the cap — a statement that the default rejects now
|
|
97
|
+
// prepares under a 2 MiB sqlLength.
|
|
98
|
+
var raised = b.localDb.thin({ file: path.join(tmpDir, "raised.db"),
|
|
99
|
+
schemaSql: "CREATE TABLE t(x)", audit: false, limits: { sqlLength: 2 * 1024 * 1024 } });
|
|
100
|
+
check("localDb.thin: opts.limits raises sqlLength (over-default statement prepares)",
|
|
101
|
+
!!raised.prepare(hugeSql));
|
|
102
|
+
raised.close();
|
|
103
|
+
|
|
76
104
|
// After close, prepare/run/query must throw.
|
|
77
105
|
threw = null;
|
|
78
106
|
try { handle.run("SELECT 1"); } catch (e) { threw = e; }
|
|
@@ -70,6 +70,31 @@ async function run() {
|
|
|
70
70
|
}, { posture: "sanitize" });
|
|
71
71
|
check("mcp.toolResult.sanitize: sanitize-mode redacts <script>", s.content[0].text.indexOf("[REDACTED]") !== -1);
|
|
72
72
|
|
|
73
|
+
// sanitize mode must redact EVERY dangerous token, not just the leftmost.
|
|
74
|
+
// On `data:text/html,<script>...` a non-global .replace would strip the
|
|
75
|
+
// data: scheme and leave the executable <script> — sanitize returning
|
|
76
|
+
// runnable HTML. The result must carry no <script / <iframe / javascript:
|
|
77
|
+
// / vbscript: / data:text/html anywhere.
|
|
78
|
+
var dangerous = [
|
|
79
|
+
"data:text/html,<script>alert(1)</script>",
|
|
80
|
+
"javascript:alert(1) then <iframe src=x></iframe>",
|
|
81
|
+
"<embed src=x> and vbscript:msgbox(1)",
|
|
82
|
+
];
|
|
83
|
+
var leakRe = /<script\b|<iframe\b|<object\b|<embed\b|javascript:|vbscript:|data:\s*text\/html/i;
|
|
84
|
+
for (var di = 0; di < dangerous.length; di++) {
|
|
85
|
+
var sanOut = b.mcp.toolResult.sanitize({
|
|
86
|
+
content: [{ type: "text", text: dangerous[di] }],
|
|
87
|
+
}, { posture: "sanitize" });
|
|
88
|
+
check("mcp.toolResult.sanitize: no dangerous token survives — " + dangerous[di].slice(0, 24),
|
|
89
|
+
!leakRe.test(sanOut.content[0].text));
|
|
90
|
+
}
|
|
91
|
+
// A benign non-HTML data: URL is NOT over-redacted.
|
|
92
|
+
var benign = b.mcp.toolResult.sanitize({
|
|
93
|
+
content: [{ type: "text", text: "logo data:image/png;base64,AAAA" }],
|
|
94
|
+
}, { posture: "sanitize" });
|
|
95
|
+
check("mcp.toolResult.sanitize: benign data:image/png left intact",
|
|
96
|
+
benign.content[0].text.indexOf("data:image/png") !== -1);
|
|
97
|
+
|
|
73
98
|
var capScope = b.mcp.capability.create(["fs:read", "fs:write"]);
|
|
74
99
|
check("mcp.capability: scopes captured", capScope.scopes.length === 2);
|
|
75
100
|
check("mcp.capability: satisfiedBy succeeds with full grant", capScope.satisfiedBy(["fs:read", "fs:write", "extra"]));
|
|
@@ -126,8 +126,37 @@ function testConsumersAcceptLegitimate() {
|
|
|
126
126
|
check("legitimate maxBytes / maxUrlLength inputs pass through every consumer", true);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
function _ErrK(code, message) { this.code = code; this.message = message; }
|
|
130
|
+
|
|
131
|
+
function testRequirePositiveFiniteInt() {
|
|
132
|
+
// The REQUIRED sibling of requirePositiveFiniteIntIfPresent: unlike the
|
|
133
|
+
// IfPresent form (which skips when undefined), the required form throws on
|
|
134
|
+
// a missing value — so a required numeric opt can't slip through absent.
|
|
135
|
+
check("requirePositiveFiniteInt accepts a valid value",
|
|
136
|
+
nb.requirePositiveFiniteInt(5, "x", _ErrK, "n/bad") === 5);
|
|
137
|
+
_expect("requirePositiveFiniteInt THROWS on undefined (required, not IfPresent)",
|
|
138
|
+
function () { nb.requirePositiveFiniteInt(undefined, "x", _ErrK, "n/bad"); }, "n/bad");
|
|
139
|
+
_expect("requirePositiveFiniteInt rejects Infinity",
|
|
140
|
+
function () { nb.requirePositiveFiniteInt(Infinity, "x", _ErrK, "n/bad"); }, "n/bad");
|
|
141
|
+
_expect("requirePositiveFiniteInt rejects 0 (not positive)",
|
|
142
|
+
function () { nb.requirePositiveFiniteInt(0, "x", _ErrK, "n/bad"); }, "n/bad");
|
|
143
|
+
// Optional { min, max } range — the bundler/mail-scan/safe-decompress shape.
|
|
144
|
+
check("requirePositiveFiniteInt accepts in-range",
|
|
145
|
+
nb.requirePositiveFiniteInt(50, "x", _ErrK, "n/bad", { min: 1, max: 100 }) === 50);
|
|
146
|
+
_expect("requirePositiveFiniteInt rejects above max",
|
|
147
|
+
function () { nb.requirePositiveFiniteInt(101, "x", _ErrK, "n/bad", { max: 100 }); }, "n/bad");
|
|
148
|
+
_expect("requirePositiveFiniteInt rejects below min",
|
|
149
|
+
function () { nb.requirePositiveFiniteInt(2, "x", _ErrK, "n/bad", { min: 5 }); }, "n/bad");
|
|
150
|
+
var msg = null;
|
|
151
|
+
try { nb.requirePositiveFiniteInt(70000, "port", _ErrK, "n/bad", { max: 65535 }); }
|
|
152
|
+
catch (e) { msg = e.message; }
|
|
153
|
+
check("range error names the bound + the offending shape",
|
|
154
|
+
/<= 65535/.test(msg || "") && /number 70000/.test(msg || ""));
|
|
155
|
+
}
|
|
156
|
+
|
|
129
157
|
async function run() {
|
|
130
158
|
testHelperPredicate();
|
|
159
|
+
testRequirePositiveFiniteInt();
|
|
131
160
|
testConsumersRejectInfinity();
|
|
132
161
|
testConsumersAcceptLegitimate();
|
|
133
162
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* object-store versioned-delete surface — the cross-backend contract for the
|
|
4
|
+
* S3 Object-Lock erasure workflow (#88), exercised WITHOUT a live S3. The
|
|
5
|
+
* full enforcement proof (a COMPLIANCE-retained version's delete is refused)
|
|
6
|
+
* lives in test/integration/object-store-worm-lock.test.js against MinIO;
|
|
7
|
+
* this layer-0 test pins the parts that hold on any host:
|
|
8
|
+
*
|
|
9
|
+
* - sigv4 exposes listVersions + a versionId-aware delete;
|
|
10
|
+
* - non-S3 backends (local) have NO version surface and REFUSE a versioned
|
|
11
|
+
* delete loudly (VERSIONID_UNSUPPORTED) rather than silently dropping the
|
|
12
|
+
* current object — a silent drop on an erasure path is the footgun;
|
|
13
|
+
* - the facade feature-detects listVersions (null on backends without it).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
var helpers = require("../helpers");
|
|
17
|
+
var check = helpers.check;
|
|
18
|
+
var b = helpers.b;
|
|
19
|
+
var os = require("../../lib/object-store");
|
|
20
|
+
var nodeOs = require("os");
|
|
21
|
+
var nodePath = require("path");
|
|
22
|
+
var nodeFs = require("fs");
|
|
23
|
+
|
|
24
|
+
function _sigv4() {
|
|
25
|
+
return os.buildBackend({
|
|
26
|
+
name: "t", protocol: "sigv4", endpoint: "https://s3.local", region: "us-east-1",
|
|
27
|
+
bucket: "b", accessKeyId: "AK", secretAccessKey: "SK", allowInternal: true,
|
|
28
|
+
forcePathStyle: true, classifications: ["operational"], residencyTag: "unrestricted",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function _local() {
|
|
32
|
+
return os.buildBackend({
|
|
33
|
+
name: "l", protocol: "local",
|
|
34
|
+
rootDir: nodePath.join(nodeOs.tmpdir(), "bjv-" + process.pid + "-" + Math.floor(Math.random() * 1e6)),
|
|
35
|
+
classifications: ["operational"], residencyTag: "unrestricted",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function run() {
|
|
40
|
+
var sig = _sigv4();
|
|
41
|
+
check("sigv4 facade exposes listVersions()", typeof sig.listVersions === "function");
|
|
42
|
+
check("sigv4 raw deleteKey is versionId-aware (arity 2)", sig.raw.delete.length === 2);
|
|
43
|
+
check("sigv4 raw exposes listVersions()", typeof sig.raw.listVersions === "function");
|
|
44
|
+
|
|
45
|
+
var loc = _local();
|
|
46
|
+
check("local facade reports no listVersions (feature-detect → null)", loc.listVersions === null);
|
|
47
|
+
|
|
48
|
+
// http-put is the fifth backend (a bare PUT/DELETE target, no version
|
|
49
|
+
// surface) — it must refuse a versioned delete loudly too, not forward a
|
|
50
|
+
// dropped versionId to a plain DELETE (the gap Codex P2 caught on PR #319).
|
|
51
|
+
var httpPut = os.buildBackend({
|
|
52
|
+
name: "h", protocol: "http-put", baseUrl: "https://up.invalid/bucket",
|
|
53
|
+
classifications: ["operational"], residencyTag: "unrestricted",
|
|
54
|
+
});
|
|
55
|
+
check("http-put facade reports no listVersions (feature-detect → null)", httpPut.listVersions === null);
|
|
56
|
+
var hpThrew = null;
|
|
57
|
+
try { await httpPut.delete("some-key", { versionId: "v1" }); }
|
|
58
|
+
catch (e) { hpThrew = e; }
|
|
59
|
+
check("http-put versioned delete REFUSES loudly (VERSIONID_UNSUPPORTED)",
|
|
60
|
+
hpThrew && hpThrew.code === "VERSIONID_UNSUPPORTED");
|
|
61
|
+
|
|
62
|
+
// A versioned delete on a backend with no version surface must THROW, not
|
|
63
|
+
// silently unlink the single on-disk file and report a version erased.
|
|
64
|
+
var threw = null;
|
|
65
|
+
try { await loc.delete("some-key", { versionId: "v1" }); }
|
|
66
|
+
catch (e) { threw = e; }
|
|
67
|
+
check("local versioned delete REFUSES loudly (VERSIONID_UNSUPPORTED)",
|
|
68
|
+
threw && threw.code === "VERSIONID_UNSUPPORTED");
|
|
69
|
+
|
|
70
|
+
// An UNVERSIONED local delete still works normally (returns false for a
|
|
71
|
+
// missing key) — the guard only fires when versionId is actually passed.
|
|
72
|
+
var missing = await loc.delete("definitely-absent-" + process.pid);
|
|
73
|
+
check("local unversioned delete still works (false for a missing key)", missing === false);
|
|
74
|
+
|
|
75
|
+
// b.storage.listVersions — the routed public facade. Against a local
|
|
76
|
+
// backend (no version surface) it refuses loudly with VERSIONS_UNSUPPORTED,
|
|
77
|
+
// the same contract as the backend level, so an erasure workflow can never
|
|
78
|
+
// mistake a single-version backend for a fully-enumerated one.
|
|
79
|
+
var sdir = nodePath.join(nodeOs.tmpdir(), "bjv-storage-" + process.pid + "-" + Math.floor(Math.random() * 1e6));
|
|
80
|
+
nodeFs.mkdirSync(sdir, { recursive: true });
|
|
81
|
+
b.storage.init({ backend: "local", uploadDir: sdir });
|
|
82
|
+
var lvThrew = null;
|
|
83
|
+
try { await b.storage.listVersions("any/prefix/"); }
|
|
84
|
+
catch (e) { lvThrew = e; }
|
|
85
|
+
check("b.storage.listVersions refuses on a no-version backend (VERSIONS_UNSUPPORTED)",
|
|
86
|
+
lvThrew && lvThrew.code === "VERSIONS_UNSUPPORTED");
|
|
87
|
+
b.storage._resetForTest();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { run: run };
|
|
91
|
+
|
|
92
|
+
if (require.main === module) {
|
|
93
|
+
run().then(
|
|
94
|
+
function () { console.log("OK — " + helpers.getChecks() + " checks passed"); },
|
|
95
|
+
function (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.safeBuffer linear-scan helpers that replace O(n^2) regexes (ReDoS class).
|
|
4
|
+
*
|
|
5
|
+
* stripTrailingHspace and indexAfterOpenTag each replace a regex whose
|
|
6
|
+
* backtracking is quadratic in V8 on adversarial input:
|
|
7
|
+
* - stripTrailingHspace: /[ \t]+$/ (greedy-then-$)
|
|
8
|
+
* - indexAfterOpenTag: /<tag[^>]*>/ (greedy-bracket-then->)
|
|
9
|
+
*
|
|
10
|
+
* Each test asserts (1) byte-for-byte output parity with the regex it
|
|
11
|
+
* replaces on every edge case, and (2) that a 400K-char adversarial input
|
|
12
|
+
* — which drives the regex to multiple seconds — completes near-instantly.
|
|
13
|
+
* The perf bound is deliberately loose (500ms) so it discriminates the
|
|
14
|
+
* O(n) fix from the O(n^2) regression (~8s) without flaking on a contended
|
|
15
|
+
* runner.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
var helpers = require("../helpers");
|
|
19
|
+
var b = helpers.b;
|
|
20
|
+
var check = helpers.check;
|
|
21
|
+
|
|
22
|
+
var TRAILING_HSPACE_RE = /[ \t]+$/;
|
|
23
|
+
|
|
24
|
+
function _elapsedMs(fn) {
|
|
25
|
+
var s = process.hrtime.bigint();
|
|
26
|
+
fn();
|
|
27
|
+
return Number(process.hrtime.bigint() - s) / 1e6;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function run() {
|
|
31
|
+
var sb = b.safeBuffer;
|
|
32
|
+
|
|
33
|
+
// ---- stripTrailingHspace: byte-identical to /[ \t]+$/ replace ----
|
|
34
|
+
var cases = [
|
|
35
|
+
"hello ", "a b\t\t", "hello \n", "hello \n ", "", " ", "\t\t\t",
|
|
36
|
+
"no-trailing", "trailing tab\t", "mixed \t \t ", " leading kept",
|
|
37
|
+
"internal spaces kept x", "unicode café ", "line1\nline2 ",
|
|
38
|
+
];
|
|
39
|
+
var allParity = true;
|
|
40
|
+
for (var i = 0; i < cases.length; i++) {
|
|
41
|
+
var got = sb.stripTrailingHspace(cases[i]);
|
|
42
|
+
var want = cases[i].replace(TRAILING_HSPACE_RE, "");
|
|
43
|
+
if (got !== want) { allParity = false; break; }
|
|
44
|
+
}
|
|
45
|
+
check("stripTrailingHspace: byte-identical to /[ \\t]+$/ on every edge case", allParity);
|
|
46
|
+
check("stripTrailingHspace: trailing \\n preserved (JS $ without /m)",
|
|
47
|
+
sb.stripTrailingHspace("hello \n") === "hello \n");
|
|
48
|
+
check("stripTrailingHspace: non-string passthrough", sb.stripTrailingHspace(42) === 42);
|
|
49
|
+
|
|
50
|
+
var bigWs = "content" + " ".repeat(400000);
|
|
51
|
+
var msStrip = _elapsedMs(function () { sb.stripTrailingHspace(bigWs); });
|
|
52
|
+
check("stripTrailingHspace: linear on 400K trailing spaces (< 500ms; regex was ~85s)", msStrip < 500);
|
|
53
|
+
check("stripTrailingHspace: 400K-space result correct", sb.stripTrailingHspace(bigWs) === "content");
|
|
54
|
+
|
|
55
|
+
// ---- indexAfterOpenTag: parity with /<body[^>]*>/i for real HTML ----
|
|
56
|
+
// Direct fully-qualified call (also satisfies the public-primitive
|
|
57
|
+
// coverage gate, which scans for the b.safeBuffer.* token form).
|
|
58
|
+
check("b.safeBuffer.indexAfterOpenTag: bare <body> → past the '>'",
|
|
59
|
+
b.safeBuffer.indexAfterOpenTag("<body>x", "body") === 6);
|
|
60
|
+
|
|
61
|
+
function oldIdx(html, tag) {
|
|
62
|
+
var m = html.match(new RegExp("<" + tag + "[^>]*>", "i"));
|
|
63
|
+
return m ? m.index + m[0].length : -1;
|
|
64
|
+
}
|
|
65
|
+
var htmls = [
|
|
66
|
+
"<!doctype html><html><head></head><body data-x=\"1\">CONTENT</body>",
|
|
67
|
+
"<body>bare", "<BODY id=a>x", "<html><body class=x>hi", "no body here",
|
|
68
|
+
"<body\nmultiline\nattrs>z",
|
|
69
|
+
];
|
|
70
|
+
var idxParity = true;
|
|
71
|
+
for (var j = 0; j < htmls.length; j++) {
|
|
72
|
+
// For inputs without a degenerate <bodyfoo>, the helper agrees with the regex.
|
|
73
|
+
if (sb.indexAfterOpenTag(htmls[j], "body") !== oldIdx(htmls[j], "body")) { idxParity = false; break; }
|
|
74
|
+
}
|
|
75
|
+
check("indexAfterOpenTag: agrees with /<body[^>]*>/i on real HTML", idxParity);
|
|
76
|
+
check("indexAfterOpenTag: stricter than regex — <bodyfoo> is NOT a <body>",
|
|
77
|
+
sb.indexAfterOpenTag("<bodyfoo>hi", "body") === -1);
|
|
78
|
+
check("indexAfterOpenTag: absent tag → -1", sb.indexAfterOpenTag("<p>x</p>", "body") === -1);
|
|
79
|
+
check("indexAfterOpenTag: unterminated <body → -1", sb.indexAfterOpenTag("<body no close", "body") === -1);
|
|
80
|
+
check("indexAfterOpenTag: non-string → -1", sb.indexAfterOpenTag(42, "body") === -1);
|
|
81
|
+
|
|
82
|
+
var bigBody = "<body".repeat(80000); // 400K chars, 80K starts, no closing >
|
|
83
|
+
var msIdx = _elapsedMs(function () { sb.indexAfterOpenTag(bigBody, "body"); });
|
|
84
|
+
check("indexAfterOpenTag: linear on 80K <body starts (< 500ms; regex was ~8.6s)", msIdx < 500);
|
|
85
|
+
check("indexAfterOpenTag: degenerate input → -1 (no terminated tag)",
|
|
86
|
+
sb.indexAfterOpenTag(bigBody, "body") === -1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { run: run };
|
|
90
|
+
|
|
91
|
+
if (require.main === module) {
|
|
92
|
+
try { run(); console.log("OK — " + helpers.getChecks() + " checks passed"); }
|
|
93
|
+
catch (e) { console.error("FAIL:", e.stack || e); process.exit(1); }
|
|
94
|
+
}
|
|
@@ -143,6 +143,57 @@ function _esbuildBinaryMatchesPlatform() {
|
|
|
143
143
|
catch (_e) { return false; }
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// ---- esbuild binary supply-chain pin ----
|
|
147
|
+
//
|
|
148
|
+
// esbuild ships its native compiler as a per-platform binary fetched at
|
|
149
|
+
// install time (@esbuild/<platform>-<arch>). The bundle test below runs that
|
|
150
|
+
// binary; this pin verifies the binary on disk is the EXACT one we reviewed,
|
|
151
|
+
// so a tampered / drifted binary is caught before it gets to bundle the
|
|
152
|
+
// framework. The hashes were captured by diffing the published 0.28.0 -> 0.28.1
|
|
153
|
+
// tarballs and sha256-ing the binaries (the dependency-review discipline: diff
|
|
154
|
+
// + verify the binary, never rubber-stamp a version bump).
|
|
155
|
+
//
|
|
156
|
+
// The map is keyed by (version -> platform-arch). The repo intentionally ships
|
|
157
|
+
// no committed lockfile (zero runtime deps), so a CI dep cache can hold an
|
|
158
|
+
// esbuild patch other than the one in package.json; we only have hashes for the
|
|
159
|
+
// versions we have actually reviewed. So: verify the binary hash when the
|
|
160
|
+
// installed (version, platform) pair has a reviewed hash, and note loudly +
|
|
161
|
+
// skip otherwise — never fail on a version we never diffed, and never claim a
|
|
162
|
+
// match we can't make. On every esbuild bump, re-review the diff and add the
|
|
163
|
+
// new version's hashes here.
|
|
164
|
+
var ESBUILD_BINARY_SHA256 = {
|
|
165
|
+
"0.28.1": {
|
|
166
|
+
"linux-x64": "0c6588b092a2c291a72bab90659f3c9e0e25e0fe59c9ac12b4dae4d945e5548c", // CI floor (ubuntu), authoritative
|
|
167
|
+
"win32-x64": "ec02ee9b14ab332416fedd10614dfb80eed5304d94f67745067c011934a8c3c3", // maintainer host (win32)
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function testEsbuildBinaryHashPinned() {
|
|
172
|
+
var ver = require("esbuild/package.json").version;
|
|
173
|
+
var platKey = process.platform + "-" + process.arch;
|
|
174
|
+
var verMap = ESBUILD_BINARY_SHA256[ver];
|
|
175
|
+
var pinned = verMap && verMap[platKey];
|
|
176
|
+
if (!pinned) {
|
|
177
|
+
// Either an esbuild patch we have not reviewed (no hash for this version),
|
|
178
|
+
// or a platform we did not capture (e.g. darwin-arm64). Note the gap loudly
|
|
179
|
+
// so it is visible, but do not fail — we can only verify what we reviewed.
|
|
180
|
+
check("esbuild binary pin: no reviewed hash for " + ver + " / " + platKey +
|
|
181
|
+
" — verification skipped (re-review + re-pin on bump)", true);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!_esbuildBinaryMatchesPlatform()) {
|
|
185
|
+
check("esbuild binary pin: skipped — node_modules binary mismatched for " +
|
|
186
|
+
platKey + " (host vs container deps)", true);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
var binPkg = path.dirname(require.resolve("@esbuild/" + platKey + "/package.json"));
|
|
190
|
+
var binName = process.platform === "win32" ? "esbuild.exe" : path.join("bin", "esbuild");
|
|
191
|
+
var binPath = path.join(binPkg, binName);
|
|
192
|
+
var actual = nodeCrypto.createHash("sha256").update(fs.readFileSync(binPath)).digest("hex");
|
|
193
|
+
check("esbuild " + ver + " native binary on disk matches the reviewed SHA-256 pin (" + platKey + ")",
|
|
194
|
+
actual === pinned);
|
|
195
|
+
}
|
|
196
|
+
|
|
146
197
|
function testEsbuildBundlePreservesVendorData() {
|
|
147
198
|
if (!_esbuildBinaryMatchesPlatform()) {
|
|
148
199
|
check("esbuild bundle: skipped — node_modules esbuild binary mismatched for " +
|
|
@@ -427,6 +478,7 @@ function testSeaBundlePreservesVendorData() {
|
|
|
427
478
|
|
|
428
479
|
async function run() {
|
|
429
480
|
try {
|
|
481
|
+
testEsbuildBinaryHashPinned();
|
|
430
482
|
testEsbuildBundlePreservesVendorData();
|
|
431
483
|
testEsbuildMinifiedBundlePreservesVendorData();
|
|
432
484
|
testBundleHasNoMissingModuleRuntimePath();
|
package/package.json
CHANGED