@blamejs/blamejs-shop 0.4.32 → 0.4.37
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 +10 -0
- package/README.md +1 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +72 -52
- package/lib/vendor/blamejs/.github/workflows/ci.yml +12 -12
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +37 -5
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/MIGRATING.md +12 -0
- package/lib/vendor/blamejs/README.md +5 -2
- package/lib/vendor/blamejs/SECURITY.md +4 -2
- package/lib/vendor/blamejs/api-snapshot.json +137 -2
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/archive-read.js +2 -1
- package/lib/vendor/blamejs/lib/archive-tar-read.js +2 -1
- package/lib/vendor/blamejs/lib/atomic-file.js +5 -0
- package/lib/vendor/blamejs/lib/audit.js +2 -0
- package/lib/vendor/blamejs/lib/auth/elevation-grant.js +6 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +13 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +5 -2
- package/lib/vendor/blamejs/lib/cli.js +8 -1
- package/lib/vendor/blamejs/lib/compliance.js +4 -0
- package/lib/vendor/blamejs/lib/config-drift.js +2 -1
- package/lib/vendor/blamejs/lib/credential-hash.js +9 -0
- package/lib/vendor/blamejs/lib/db.js +15 -2
- package/lib/vendor/blamejs/lib/dsa.js +482 -0
- package/lib/vendor/blamejs/lib/framework-error.js +14 -0
- package/lib/vendor/blamejs/lib/http-client.js +5 -2
- package/lib/vendor/blamejs/lib/local-db-thin.js +3 -2
- package/lib/vendor/blamejs/lib/log-stream-local.js +1 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +9 -2
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +16 -7
- package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +36 -11
- package/lib/vendor/blamejs/lib/mtls-ca.js +2 -2
- package/lib/vendor/blamejs/lib/observability.js +3 -2
- package/lib/vendor/blamejs/lib/pipl-cn.js +377 -0
- package/lib/vendor/blamejs/lib/restore-rollback.js +5 -5
- package/lib/vendor/blamejs/lib/retention.js +16 -2
- package/lib/vendor/blamejs/lib/scheduler.js +12 -0
- package/lib/vendor/blamejs/lib/self-update.js +1 -1
- package/lib/vendor/blamejs/lib/session.js +64 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +25 -7
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -3
- package/lib/vendor/blamejs/lib/watcher.js +8 -0
- package/lib/vendor/blamejs/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.15.7.json +43 -0
- package/lib/vendor/blamejs/release-notes/v0.15.8.json +48 -0
- package/lib/vendor/blamejs/release-notes/v0.15.9.json +58 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +16 -0
- package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +338 -0
- package/lib/vendor/blamejs/test/00-primitives.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-rename-retry.test.js +70 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-init-extensions.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsa.test.js +169 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +40 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +172 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-floor.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +64 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-watchdog-stale-settle.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +57 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +7 -3
- package/package.json +2 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// b.pipl.sccFilingAssessment + b.pipl.securityAssessmentCertificate —
|
|
3
|
+
// China PIPL Art. 38/40/55 cross-border transfer record-builders (pure;
|
|
4
|
+
// no DB). Drives the real b.pipl.* consumer path, asserts frozen records +
|
|
5
|
+
// audit emission via a captured injected sink, and the config-time throws.
|
|
6
|
+
|
|
7
|
+
var helpers = require("../helpers");
|
|
8
|
+
var b = helpers.b;
|
|
9
|
+
var check = helpers.check;
|
|
10
|
+
|
|
11
|
+
// A b.audit-shaped capture sink — the builder prefers an injected
|
|
12
|
+
// opts.audit object over the global b.audit (drop-silent without a DB
|
|
13
|
+
// handler), so this is how the test asserts emission.
|
|
14
|
+
function _captureAudit() {
|
|
15
|
+
var events = [];
|
|
16
|
+
return { events: events, safeEmit: function (ev) { events.push(ev); } };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function expectCode(label, fn, code) {
|
|
20
|
+
var threw = null;
|
|
21
|
+
try { fn(); } catch (e) { threw = e; }
|
|
22
|
+
check(label, threw && (threw.code || "") === code);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function expectThrows(label, fn) {
|
|
26
|
+
var threw = null;
|
|
27
|
+
try { fn(); } catch (e) { threw = e; }
|
|
28
|
+
check(label, threw instanceof Error);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function run() {
|
|
32
|
+
check("sccFilingAssessment is a function", typeof b.pipl.sccFilingAssessment === "function");
|
|
33
|
+
check("securityAssessmentCertificate is a function", typeof b.pipl.securityAssessmentCertificate === "function");
|
|
34
|
+
check("PiplError exposed on b.frameworkError", typeof b.frameworkError.PiplError === "function");
|
|
35
|
+
check("b.pipl.PiplError is the same constructor", b.pipl.PiplError === b.frameworkError.PiplError);
|
|
36
|
+
check("LEGAL_BASES is the Art. 38(1) triad",
|
|
37
|
+
b.pipl.LEGAL_BASES.length === 3 &&
|
|
38
|
+
b.pipl.LEGAL_BASES.indexOf("standard-contract") !== -1 &&
|
|
39
|
+
b.pipl.LEGAL_BASES.indexOf("security-assessment") !== -1 &&
|
|
40
|
+
b.pipl.LEGAL_BASES.indexOf("certification") !== -1);
|
|
41
|
+
check("pipl-cn is a cross-border-regulated posture",
|
|
42
|
+
b.compliance.isCrossBorderRegulated("pipl-cn") === true);
|
|
43
|
+
|
|
44
|
+
var recordedAt = 1700000000000;
|
|
45
|
+
|
|
46
|
+
// ---- sccFilingAssessment: below-threshold standard contract ----
|
|
47
|
+
var sink1 = _captureAudit();
|
|
48
|
+
var scc = b.pipl.sccFilingAssessment({
|
|
49
|
+
assessmentId: "xfer-1", transferType: "processor", recipientJurisdiction: "US",
|
|
50
|
+
dataCategories: ["contact", "billing"], legalBasis: "standard-contract",
|
|
51
|
+
volume: 5000, sensitivePI: false, recordedAt: recordedAt, audit: sink1,
|
|
52
|
+
});
|
|
53
|
+
check("scc: honors standard-contract below thresholds", scc.mechanismRequired === "standard-contract");
|
|
54
|
+
check("scc: securityAssessmentRequired false", scc.securityAssessmentRequired === false);
|
|
55
|
+
check("scc: record frozen", Object.isFrozen(scc));
|
|
56
|
+
check("scc: dataCategories frozen", Object.isFrozen(scc.dataCategories));
|
|
57
|
+
check("scc: nextReviewDueBy = recordedAt + 1 year",
|
|
58
|
+
scc.nextReviewDueBy === recordedAt + b.constants.TIME.days(365));
|
|
59
|
+
check("scc: emitted pipl.transfer.assessed",
|
|
60
|
+
sink1.events.length === 1 && sink1.events[0].action === "pipl.transfer.assessed");
|
|
61
|
+
check("scc: audit carries mechanismRequired",
|
|
62
|
+
sink1.events[0].metadata.mechanismRequired === "standard-contract");
|
|
63
|
+
|
|
64
|
+
// ---- CIIO forces security assessment over declared basis ----
|
|
65
|
+
var ciio = b.pipl.sccFilingAssessment({
|
|
66
|
+
assessmentId: "xfer-2", transferType: "controller-to-controller", recipientJurisdiction: "EU",
|
|
67
|
+
dataCategories: ["health"], legalBasis: "standard-contract",
|
|
68
|
+
volume: 100, sensitivePI: true, ciio: true, recordedAt: recordedAt,
|
|
69
|
+
});
|
|
70
|
+
check("scc: CIIO forces security-assessment", ciio.mechanismRequired === "security-assessment");
|
|
71
|
+
check("scc: CIIO sets securityAssessmentRequired", ciio.securityAssessmentRequired === true);
|
|
72
|
+
check("scc: CIIO trigger named", ciio.securityAssessmentTriggers.indexOf("ciio") !== -1);
|
|
73
|
+
check("scc: mandated assessment carries 3-year clock",
|
|
74
|
+
ciio.nextReviewDueBy === recordedAt + b.constants.TIME.days(365 * 3));
|
|
75
|
+
|
|
76
|
+
// ---- >1M non-sensitive PI forces it ----
|
|
77
|
+
var bigVol = b.pipl.sccFilingAssessment({
|
|
78
|
+
assessmentId: "xfer-3", transferType: "processor", recipientJurisdiction: "SG",
|
|
79
|
+
dataCategories: ["contact"], legalBasis: "certification",
|
|
80
|
+
volume: 1000001, sensitivePI: false, recordedAt: recordedAt,
|
|
81
|
+
});
|
|
82
|
+
check("scc: >1M non-sensitive volume forces security-assessment",
|
|
83
|
+
bigVol.mechanismRequired === "security-assessment" &&
|
|
84
|
+
bigVol.securityAssessmentTriggers.indexOf("non-sensitive-pi-volume") !== -1);
|
|
85
|
+
|
|
86
|
+
// ---- 100k-1M non-sensitive band is SCC, NOT security-assessment (the
|
|
87
|
+
// 100k cumulative threshold is the standard-contract tier per the CAC
|
|
88
|
+
// 2024 Provisions; a 200k non-sensitive transfer must not over-classify) ----
|
|
89
|
+
var midBand = b.pipl.sccFilingAssessment({
|
|
90
|
+
assessmentId: "xfer-3b", transferType: "processor", recipientJurisdiction: "US",
|
|
91
|
+
dataCategories: ["contact"], legalBasis: "standard-contract",
|
|
92
|
+
volume: 200000, sensitivePI: false, recordedAt: recordedAt,
|
|
93
|
+
});
|
|
94
|
+
check("scc: 200k non-sensitive stays standard-contract (no over-classify)",
|
|
95
|
+
midBand.mechanismRequired === "standard-contract" &&
|
|
96
|
+
midBand.securityAssessmentRequired === false);
|
|
97
|
+
|
|
98
|
+
// ---- THIS transfer's volume counts toward the cumulative sensitive
|
|
99
|
+
// threshold: a first transfer of 10,001 sensitive subjects forces it
|
|
100
|
+
// even with cumulativeSensitivePI omitted (defaults 0) ----
|
|
101
|
+
var firstSens = b.pipl.sccFilingAssessment({
|
|
102
|
+
assessmentId: "xfer-3c", transferType: "processor", recipientJurisdiction: "US",
|
|
103
|
+
dataCategories: ["biometric"], legalBasis: "standard-contract",
|
|
104
|
+
volume: 10001, sensitivePI: true, recordedAt: recordedAt,
|
|
105
|
+
});
|
|
106
|
+
check("scc: first 10,001 sensitive-PI transfer forces it (own volume counts)",
|
|
107
|
+
firstSens.securityAssessmentRequired === true &&
|
|
108
|
+
firstSens.securityAssessmentTriggers.indexOf("sensitive-pi-volume") !== -1);
|
|
109
|
+
|
|
110
|
+
// ---- cumulative sensitive-PI threshold (this transfer + prior cumulative) ----
|
|
111
|
+
var cumSens = b.pipl.sccFilingAssessment({
|
|
112
|
+
assessmentId: "xfer-4", transferType: "processor", recipientJurisdiction: "US",
|
|
113
|
+
dataCategories: ["biometric"], legalBasis: "standard-contract",
|
|
114
|
+
volume: 200, sensitivePI: true, cumulativeSensitivePI: 10001, recordedAt: recordedAt,
|
|
115
|
+
});
|
|
116
|
+
check("scc: >10k cumulative sensitive-PI forces it",
|
|
117
|
+
cumSens.securityAssessmentRequired === true &&
|
|
118
|
+
cumSens.securityAssessmentTriggers.indexOf("sensitive-pi-volume") !== -1);
|
|
119
|
+
|
|
120
|
+
// ---- securityAssessmentCertificate: happy path ----
|
|
121
|
+
var sink3 = _captureAudit();
|
|
122
|
+
var cert = b.pipl.securityAssessmentCertificate({
|
|
123
|
+
certId: "sa-1", assessmentScope: "CRM outbound replication",
|
|
124
|
+
dataExporter: "Acme (Shanghai) Co., Ltd.", overseasRecipient: "Acme Inc.",
|
|
125
|
+
riskRating: "medium", safeguards: ["XChaCha20 at rest", "standard contractual clauses"],
|
|
126
|
+
filingRef: "CAC-2026-0042", recordedAt: recordedAt, audit: sink3,
|
|
127
|
+
});
|
|
128
|
+
check("cert: record frozen", Object.isFrozen(cert));
|
|
129
|
+
check("cert: safeguards frozen", Object.isFrozen(cert.safeguards));
|
|
130
|
+
check("cert: validUntil = recordedAt + 3 years",
|
|
131
|
+
cert.validUntil === recordedAt + b.constants.TIME.days(365 * 3));
|
|
132
|
+
check("cert: filingRef carried", cert.filingRef === "CAC-2026-0042");
|
|
133
|
+
check("cert: emitted pipl.security_assessment.recorded",
|
|
134
|
+
sink3.events.length === 1 && sink3.events[0].action === "pipl.security_assessment.recorded");
|
|
135
|
+
check("cert: filingRef omitted defaults null",
|
|
136
|
+
b.pipl.securityAssessmentCertificate({
|
|
137
|
+
certId: "sa-2", assessmentScope: "s", dataExporter: "e", overseasRecipient: "r",
|
|
138
|
+
riskRating: "low", safeguards: ["x"], recordedAt: recordedAt,
|
|
139
|
+
}).filingRef === null);
|
|
140
|
+
|
|
141
|
+
// ---- Config-time throws ----
|
|
142
|
+
expectCode("scc: non-object opts throws",
|
|
143
|
+
function () { b.pipl.sccFilingAssessment("nope"); }, "pipl/bad-opts");
|
|
144
|
+
expectThrows("scc: unknown opt key throws",
|
|
145
|
+
function () { b.pipl.sccFilingAssessment({ assessmentId: "x", bogusKey: 1 }); });
|
|
146
|
+
expectCode("scc: missing assessmentId throws",
|
|
147
|
+
function () { b.pipl.sccFilingAssessment({ transferType: "p", recipientJurisdiction: "US", dataCategories: ["c"], legalBasis: "standard-contract", volume: 1, sensitivePI: false, recordedAt: recordedAt }); }, "pipl/bad-assessment-id");
|
|
148
|
+
expectCode("scc: empty dataCategories throws",
|
|
149
|
+
function () { b.pipl.sccFilingAssessment({ assessmentId: "x", transferType: "p", recipientJurisdiction: "US", dataCategories: [], legalBasis: "standard-contract", volume: 1, sensitivePI: false, recordedAt: recordedAt }); }, "pipl/bad-data-categories");
|
|
150
|
+
expectCode("scc: bad legalBasis throws",
|
|
151
|
+
function () { b.pipl.sccFilingAssessment({ assessmentId: "x", transferType: "p", recipientJurisdiction: "US", dataCategories: ["c"], legalBasis: "bogus", volume: 1, sensitivePI: false, recordedAt: recordedAt }); }, "pipl/bad-legal-basis");
|
|
152
|
+
expectCode("scc: non-boolean sensitivePI throws",
|
|
153
|
+
function () { b.pipl.sccFilingAssessment({ assessmentId: "x", transferType: "p", recipientJurisdiction: "US", dataCategories: ["c"], legalBasis: "standard-contract", volume: 1, sensitivePI: "yes", recordedAt: recordedAt }); }, "pipl/bad-sensitive-pi");
|
|
154
|
+
expectCode("scc: missing recordedAt throws",
|
|
155
|
+
function () { b.pipl.sccFilingAssessment({ assessmentId: "x", transferType: "p", recipientJurisdiction: "US", dataCategories: ["c"], legalBasis: "standard-contract", volume: 1, sensitivePI: false }); }, "pipl/bad-recorded-at");
|
|
156
|
+
|
|
157
|
+
expectCode("cert: missing certId throws",
|
|
158
|
+
function () { b.pipl.securityAssessmentCertificate({ assessmentScope: "s", dataExporter: "e", overseasRecipient: "r", riskRating: "low", safeguards: ["x"], recordedAt: recordedAt }); }, "pipl/bad-cert-id");
|
|
159
|
+
expectCode("cert: bad riskRating throws",
|
|
160
|
+
function () { b.pipl.securityAssessmentCertificate({ certId: "c", assessmentScope: "s", dataExporter: "e", overseasRecipient: "r", riskRating: "critical", safeguards: ["x"], recordedAt: recordedAt }); }, "pipl/bad-risk-rating");
|
|
161
|
+
expectCode("cert: empty safeguards throws",
|
|
162
|
+
function () { b.pipl.securityAssessmentCertificate({ certId: "c", assessmentScope: "s", dataExporter: "e", overseasRecipient: "r", riskRating: "low", safeguards: [], recordedAt: recordedAt }); }, "pipl/bad-safeguards");
|
|
163
|
+
expectCode("cert: bad audit sink shape throws",
|
|
164
|
+
function () { b.pipl.securityAssessmentCertificate({ certId: "c", assessmentScope: "s", dataExporter: "e", overseasRecipient: "r", riskRating: "low", safeguards: ["x"], recordedAt: recordedAt, audit: { nope: 1 } }); }, "pipl/bad-audit");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { run: run };
|
|
168
|
+
|
|
169
|
+
if (require.main === module) {
|
|
170
|
+
try { run(); console.log("[pipl-cn] OK"); }
|
|
171
|
+
catch (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
172
|
+
}
|
|
@@ -51,12 +51,71 @@ function testUnknownPostureThrows() {
|
|
|
51
51
|
threw && /unknown-posture/.test(threw.code || threw.message || ""));
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function testOptionalPostureInheritance() {
|
|
55
|
+
// #121 — applyPosture(posture) records an active posture that
|
|
56
|
+
// complianceFloor() callers without an explicit posture inherit (the
|
|
57
|
+
// advertised cascade behavior). complianceFloor hard-required a string and
|
|
58
|
+
// never read STATE.activePosture, so the inheritance was unimplemented dead
|
|
59
|
+
// state; applyPosture(null) now also clears it (was a no-op).
|
|
60
|
+
var r = b.retention;
|
|
61
|
+
var prior = r.activePosture();
|
|
62
|
+
try {
|
|
63
|
+
r.applyPosture(null);
|
|
64
|
+
check("#121 applyPosture(null) clears the active posture",
|
|
65
|
+
r.activePosture() === null);
|
|
66
|
+
var threwNoActive = null;
|
|
67
|
+
try { r.complianceFloor(b.constants.TIME.days(30)); } catch (e) { threwNoActive = e; }
|
|
68
|
+
check("#121 no active posture + omitted posture → throws clearly",
|
|
69
|
+
threwNoActive !== null);
|
|
70
|
+
|
|
71
|
+
r.applyPosture("hipaa");
|
|
72
|
+
check("#121 activePosture reflects the set value", r.activePosture() === "hipaa");
|
|
73
|
+
check("#121 complianceFloor(ttl) inherits the active posture (single numeric arg)",
|
|
74
|
+
r.complianceFloor(b.constants.TIME.days(30)) === b.constants.TIME.days(365 * 6));
|
|
75
|
+
check("#121 complianceFloor(undefined, ttl) inherits the active posture",
|
|
76
|
+
r.complianceFloor(undefined, b.constants.TIME.days(30)) === b.constants.TIME.days(365 * 6));
|
|
77
|
+
check("#121 a candidate longer than the inherited floor still wins",
|
|
78
|
+
r.complianceFloor(b.constants.TIME.days(365 * 10)) === b.constants.TIME.days(365 * 10));
|
|
79
|
+
check("#121 an explicit posture still overrides the active one",
|
|
80
|
+
r.complianceFloor("pci-dss", 0) === b.constants.TIME.days(365));
|
|
81
|
+
} finally {
|
|
82
|
+
if (typeof prior === "string") r.applyPosture(prior); else r.applyPosture(null);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function testComplianceClearCascadesToRetention() {
|
|
87
|
+
// b.compliance.set cascades the posture into retention (via applyPosture), so
|
|
88
|
+
// b.compliance.clear must cascade the clear too — otherwise complianceFloor
|
|
89
|
+
// keeps inheriting the stale posture after the global posture was cleared.
|
|
90
|
+
if (!b.compliance || typeof b.compliance.set !== "function") return;
|
|
91
|
+
var r = b.retention;
|
|
92
|
+
try {
|
|
93
|
+
if (b.compliance.current()) b.compliance.clear();
|
|
94
|
+
r.applyPosture(null);
|
|
95
|
+
b.compliance.set("hipaa");
|
|
96
|
+
check("compliance.set cascades the posture into retention",
|
|
97
|
+
r.activePosture() === "hipaa");
|
|
98
|
+
b.compliance.clear();
|
|
99
|
+
check("compliance.clear cascades the clear into retention (no stale inheritance)",
|
|
100
|
+
r.activePosture() === null);
|
|
101
|
+
var threw = null;
|
|
102
|
+
try { r.complianceFloor(b.constants.TIME.days(30)); } catch (e) { threw = e; }
|
|
103
|
+
check("after clear, complianceFloor with no explicit posture throws (not the stale floor)",
|
|
104
|
+
threw !== null);
|
|
105
|
+
} finally {
|
|
106
|
+
try { if (b.compliance.current()) b.compliance.clear(); } catch (_e) { /* best-effort restore */ }
|
|
107
|
+
try { r.applyPosture(null); } catch (_e) { /* best-effort restore */ }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
54
111
|
async function run() {
|
|
55
112
|
testSurface();
|
|
56
113
|
testKnownPostures();
|
|
57
114
|
testCandidateGreaterThanFloor();
|
|
58
115
|
testCandidateShorterThanFloor();
|
|
59
116
|
testUnknownPostureThrows();
|
|
117
|
+
testOptionalPostureInheritance();
|
|
118
|
+
testComplianceClearCascadesToRetention();
|
|
60
119
|
}
|
|
61
120
|
|
|
62
121
|
module.exports = { run: run };
|
|
@@ -51,23 +51,75 @@ function testIpv4LoopbackEquivalenceClass() {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function testIpv6MappedEquivalenceClass() {
|
|
54
|
-
// IPv4-mapped IPv6
|
|
55
|
-
//
|
|
56
|
-
|
|
54
|
+
// An IPv4-mapped IPv6 address (::ffff:a.b.c.d, the ::ffff:0:0/96 block) IS
|
|
55
|
+
// the IPv4 address a.b.c.d for routing / access-control: a dual-stack peer
|
|
56
|
+
// arriving on ::ffff:1.2.3.4 reaches the same host as 1.2.3.4, and the SSRF
|
|
57
|
+
// classifier already re-classifies it by the embedded v4. So the canonical
|
|
58
|
+
// form must FOLD it to the IPv4 dotted form — otherwise a dual-stack peer
|
|
59
|
+
// never unifies with an operator's IPv4 allowlist entry (the exact bypass).
|
|
60
|
+
var mappedForms = [
|
|
57
61
|
"http://[::ffff:127.0.0.1]/",
|
|
58
62
|
"http://[::ffff:7f00:1]/",
|
|
59
63
|
"http://[0:0:0:0:0:ffff:7f00:1]/",
|
|
60
64
|
"http://[0:0:0:0:0:FFFF:7F00:1]/", // mixed-case hex
|
|
61
65
|
];
|
|
62
|
-
var
|
|
63
|
-
check("
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
forms[i] !== forms[0]);
|
|
66
|
+
var bareV4 = b.safeUrl.canonicalize("http://127.0.0.1/");
|
|
67
|
+
check("plain IPv4 canonical is the dotted form", bareV4 === "http://127.0.0.1/");
|
|
68
|
+
for (var i = 0; i < mappedForms.length; i += 1) {
|
|
69
|
+
check("ipv4-mapped '" + mappedForms[i] + "' folds to the bare IPv4 form",
|
|
70
|
+
b.safeUrl.canonicalize(mappedForms[i]) === bareV4);
|
|
71
|
+
check("raw '" + mappedForms[i] + "' !== 'http://127.0.0.1/' (old-world unequal)",
|
|
72
|
+
mappedForms[i] !== "http://127.0.0.1/");
|
|
70
73
|
}
|
|
74
|
+
// canonicalizeHost folds the host-only form too (used for host allowlists).
|
|
75
|
+
check("canonicalizeHost folds ::ffff:1.2.3.4 to 1.2.3.4",
|
|
76
|
+
b.ssrfGuard.canonicalizeHost("::ffff:1.2.3.4") === "1.2.3.4");
|
|
77
|
+
check("canonicalizeHost folds the all-hex mapped spelling too",
|
|
78
|
+
b.ssrfGuard.canonicalizeHost("::ffff:102:304") === "1.2.3.4");
|
|
79
|
+
// A non-mapped IPv6 (no ::ffff:0:0/96 prefix) stays IPv6 — only the
|
|
80
|
+
// v4-mapped block is an IPv4 alias; an embedded-v4 in a documentation
|
|
81
|
+
// prefix is a distinct address.
|
|
82
|
+
check("a non-mapped IPv6 stays IPv6 (::1)",
|
|
83
|
+
b.ssrfGuard.canonicalizeHost("::1") === "::1");
|
|
84
|
+
check("2001:db8::1.2.3.4 (v4 suffix, NOT v4-mapped) stays IPv6",
|
|
85
|
+
b.ssrfGuard.canonicalizeHost("2001:db8::1.2.3.4").indexOf(".") === -1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function testEmbeddedV4AndTrailingDotUnification() {
|
|
89
|
+
// The canonical form must never flip an SSRF classify() verdict from blocked
|
|
90
|
+
// to allowed. Only the IPv4-mapped block (::ffff:0:0/96) folds, because
|
|
91
|
+
// classify(::ffff:x) === classify(x) — its branch returns classify(mappedV4)
|
|
92
|
+
// with NO reserved fallback. NAT64 (64:ff9b::/96) and 6to4 (2002::/16) are
|
|
93
|
+
// NOT folded: classify treats a NAT64 literal as `classify(v4) || "reserved"`,
|
|
94
|
+
// so classify("64:ff9b::8.8.8.8") is "reserved" while classify("8.8.8.8") is
|
|
95
|
+
// null — folding would turn a blocked NAT64 address into an allowed public
|
|
96
|
+
// IPv4 verdict. The invariant below pins that: canonicalizing then classifying
|
|
97
|
+
// must agree with classifying the original.
|
|
98
|
+
var classify = b.ssrfGuard.classify;
|
|
99
|
+
function classifyAgrees(host) {
|
|
100
|
+
return classify(b.ssrfGuard.canonicalizeHost(host)) === classify(host);
|
|
101
|
+
}
|
|
102
|
+
check("NAT64 stays IPv6 (a public NAT64 literal must not become an allowed IPv4)",
|
|
103
|
+
b.ssrfGuard.canonicalizeHost("64:ff9b::8.8.8.8").indexOf(".") === -1);
|
|
104
|
+
check("canonicalize agrees with classify on a public NAT64 literal",
|
|
105
|
+
classifyAgrees("64:ff9b::8.8.8.8"));
|
|
106
|
+
check("canonicalize agrees with classify on a NAT64 loopback literal",
|
|
107
|
+
classifyAgrees("64:ff9b::127.0.0.1"));
|
|
108
|
+
check("canonicalize agrees with classify on a public IPv4-mapped literal",
|
|
109
|
+
classifyAgrees("::ffff:8.8.8.8"));
|
|
110
|
+
// 6to4 (2002::/16) is a /48 PREFIX, not a 1:1 alias — it must stay IPv6
|
|
111
|
+
// (folding it would collapse a whole subnet onto one IPv4).
|
|
112
|
+
check("6to4 2002:7f00:1:: stays IPv6 (not a 1:1 v4 alias)",
|
|
113
|
+
b.ssrfGuard.canonicalizeHost("2002:7f00:1::").indexOf(".") === -1);
|
|
114
|
+
|
|
115
|
+
// Trailing dots are not significant for host identity — every count must
|
|
116
|
+
// collapse to the bare name so host / host. / host.. all unify.
|
|
117
|
+
check("single trailing dot strips to the bare name",
|
|
118
|
+
b.ssrfGuard.canonicalizeHost("example.com.") === "example.com");
|
|
119
|
+
check("multiple trailing dots all strip to the bare name",
|
|
120
|
+
b.ssrfGuard.canonicalizeHost("example.com..") === "example.com");
|
|
121
|
+
check("canonicalize unifies a trailing-dot URL host with the bare host",
|
|
122
|
+
b.safeUrl.canonicalize("http://example.com./p") === b.safeUrl.canonicalize("http://example.com/p"));
|
|
71
123
|
}
|
|
72
124
|
|
|
73
125
|
function testIpv6ZeroCompressionEquivalenceClass() {
|
|
@@ -284,6 +336,7 @@ function testUserinfoDroppedFromCanonicalForm() {
|
|
|
284
336
|
|
|
285
337
|
async function run() {
|
|
286
338
|
testUserinfoDroppedFromCanonicalForm();
|
|
339
|
+
testEmbeddedV4AndTrailingDotUnification();
|
|
287
340
|
testIpv4LoopbackEquivalenceClass();
|
|
288
341
|
testIpv6MappedEquivalenceClass();
|
|
289
342
|
testIpv6ZeroCompressionEquivalenceClass();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// #130: the scheduler watchdog force-clears task.running after maxJobMs and
|
|
3
|
+
// lets the next tick re-fire. The ORIGINAL (slow) run's promise then settles
|
|
4
|
+
// late and, before the fix, unconditionally wrote back task.running/lastFinish/
|
|
5
|
+
// lastError AND emitted system.scheduler.task.success|failure — clobbering the
|
|
6
|
+
// state the watchdog (and the new run) had moved on from, and double-counting.
|
|
7
|
+
// The fix tags each run with a generation the watchdog + each fire bump; a
|
|
8
|
+
// settle whose tag is stale is ignored.
|
|
9
|
+
//
|
|
10
|
+
// Driven through the public scheduler with a run() whose promise the test
|
|
11
|
+
// controls. RED on the buggy tree: resolving the watchdog-abandoned run emits
|
|
12
|
+
// a stale success. GREEN: it emits nothing; only the current run settles.
|
|
13
|
+
|
|
14
|
+
var helpers = require("../helpers");
|
|
15
|
+
var b = helpers.b;
|
|
16
|
+
var check = helpers.check;
|
|
17
|
+
var auditMod = require("../../lib/audit");
|
|
18
|
+
|
|
19
|
+
var TASK = "wd-stale-settle-test";
|
|
20
|
+
|
|
21
|
+
async function run() {
|
|
22
|
+
var realEmit = auditMod.safeEmit;
|
|
23
|
+
var successes = 0;
|
|
24
|
+
auditMod.safeEmit = function (ev) {
|
|
25
|
+
if (ev && ev.action === "system.scheduler.task.success" &&
|
|
26
|
+
ev.metadata && ev.metadata.name === TASK) {
|
|
27
|
+
successes += 1;
|
|
28
|
+
}
|
|
29
|
+
return realEmit.call(auditMod, ev);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
var resolvers = []; // one resolve fn per fire — the test settles them by hand
|
|
33
|
+
var sched = b.scheduler.create({ maxJobMs: 1, audit: true }); // 1ms watchdog → any stuck run is reaped next tick
|
|
34
|
+
try {
|
|
35
|
+
sched.schedule({
|
|
36
|
+
name: TASK,
|
|
37
|
+
every: 1000, // builder floor is 1000ms; fire 1 ≈ 1s, watchdog re-fire ≈ 2s
|
|
38
|
+
run: function () { return new Promise(function (resolve) { resolvers.push(resolve); }); },
|
|
39
|
+
});
|
|
40
|
+
sched.start();
|
|
41
|
+
|
|
42
|
+
// Wait until the watchdog has reaped fire 1 (still pending) and re-fired,
|
|
43
|
+
// so two runs exist: resolvers[0] (abandoned) + resolvers[1] (current).
|
|
44
|
+
await helpers.waitUntil(function () { return resolvers.length >= 2; },
|
|
45
|
+
{ timeoutMs: 9000, label: "#130: watchdog re-fired the task after a stuck run" });
|
|
46
|
+
|
|
47
|
+
var before = successes;
|
|
48
|
+
// Settle the FIRST run — the one the watchdog abandoned. Its late resolve
|
|
49
|
+
// must NOT emit a success (its generation is stale).
|
|
50
|
+
resolvers[0]();
|
|
51
|
+
await helpers.passiveObserve(400, "#130: stale-settle window for the abandoned run");
|
|
52
|
+
check("#130 a watchdog-abandoned run's late resolve emits NO stale success",
|
|
53
|
+
successes === before);
|
|
54
|
+
|
|
55
|
+
// The current run still settles normally → exactly one success.
|
|
56
|
+
resolvers[1]();
|
|
57
|
+
await helpers.waitUntil(function () { return successes === before + 1; },
|
|
58
|
+
{ timeoutMs: 5000, label: "#130: the current run records its success" });
|
|
59
|
+
check("#130 the current (post-watchdog) run still records its success",
|
|
60
|
+
successes === before + 1);
|
|
61
|
+
} finally {
|
|
62
|
+
sched.stop();
|
|
63
|
+
auditMod.safeEmit = realEmit;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { run: run };
|
|
68
|
+
if (require.main === module) {
|
|
69
|
+
run().then(function () { process.exit(0); })
|
|
70
|
+
.catch(function (err) { process.stderr.write(String(err && err.stack || err) + "\n"); process.exit(1); });
|
|
71
|
+
}
|
|
@@ -317,7 +317,64 @@ async function testRotateRekeysFingerprint() {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
async function testLogoutEmitsClearSiteData() {
|
|
321
|
+
var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ses-logout-"));
|
|
322
|
+
try {
|
|
323
|
+
await setupTestDb(tmpDir);
|
|
324
|
+
var s = await b.session.create({ userId: "u-logout" });
|
|
325
|
+
check("session created", typeof s.token === "string");
|
|
326
|
+
|
|
327
|
+
var headers = {};
|
|
328
|
+
var res = {
|
|
329
|
+
setHeader: function (k, v) { headers[k] = v; },
|
|
330
|
+
};
|
|
331
|
+
var destroyed = await b.session.logout(res, s.token);
|
|
332
|
+
|
|
333
|
+
check("logout returns true (session destroyed)", destroyed === true);
|
|
334
|
+
check("logout emits Clear-Site-Data header",
|
|
335
|
+
typeof headers["Clear-Site-Data"] === "string" &&
|
|
336
|
+
headers["Clear-Site-Data"].indexOf('"cookies"') !== -1 &&
|
|
337
|
+
headers["Clear-Site-Data"].indexOf('"storage"') !== -1);
|
|
338
|
+
check("logout expires the session cookie",
|
|
339
|
+
typeof headers["Set-Cookie"] === "string" &&
|
|
340
|
+
/(^|;)\s*Max-Age=0/.test(headers["Set-Cookie"]) &&
|
|
341
|
+
headers["Set-Cookie"].indexOf("sid=;") === 0);
|
|
342
|
+
check("logout cookie is Secure + HttpOnly",
|
|
343
|
+
/HttpOnly/.test(headers["Set-Cookie"]) && /Secure/.test(headers["Set-Cookie"]));
|
|
344
|
+
|
|
345
|
+
// The session is gone cluster-wide.
|
|
346
|
+
var after = await b.session.verify(s.token);
|
|
347
|
+
check("logout destroyed the session (verify returns null)", after === null);
|
|
348
|
+
|
|
349
|
+
// Custom cookie name + an unknown Clear-Site-Data directive throws.
|
|
350
|
+
var s2 = await b.session.create({ userId: "u-logout-2" });
|
|
351
|
+
var h2 = {}; var res2 = { setHeader: function (k, v) { h2[k] = v; } };
|
|
352
|
+
await b.session.logout(res2, s2.token, { cookieName: "__Host-sid" });
|
|
353
|
+
check("logout honors custom cookieName", h2["Set-Cookie"].indexOf("__Host-sid=;") === 0);
|
|
354
|
+
|
|
355
|
+
// An unknown directive throws BEFORE any side effect — the session is NOT
|
|
356
|
+
// destroyed and no client-wipe headers are queued (validate-before-revoke).
|
|
357
|
+
var s3 = await b.session.create({ userId: "u-logout-3" });
|
|
358
|
+
var h3 = {}; var res3 = { setHeader: function (k, v) { h3[k] = v; } };
|
|
359
|
+
var threw = null;
|
|
360
|
+
try { await b.session.logout(res3, s3.token, { types: ["bogus"] }); }
|
|
361
|
+
catch (e) { threw = e; }
|
|
362
|
+
check("logout rejects an unknown Clear-Site-Data directive", threw !== null);
|
|
363
|
+
check("logout did NOT queue headers on the bad-directive throw",
|
|
364
|
+
h3["Clear-Site-Data"] === undefined && h3["Set-Cookie"] === undefined);
|
|
365
|
+
check("logout did NOT destroy the session on the bad-directive throw",
|
|
366
|
+
(await b.session.verify(s3.token)) !== null);
|
|
367
|
+
|
|
368
|
+
var badRes = null;
|
|
369
|
+
try { await b.session.logout({}, "x"); } catch (e) { badRes = e; }
|
|
370
|
+
check("logout rejects a res without setHeader", badRes && badRes.code === "session/bad-res");
|
|
371
|
+
} finally {
|
|
372
|
+
await teardownTestDb(tmpDir);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
320
376
|
async function run() {
|
|
377
|
+
await testLogoutEmitsClearSiteData();
|
|
321
378
|
await testSealedCookieDefault();
|
|
322
379
|
await testSealedCookieRotateAndDestroy();
|
|
323
380
|
await testClientIpPrefixV4();
|
|
@@ -81,8 +81,11 @@ async function run() {
|
|
|
81
81
|
audit: false,
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// .root is canonicalized (realpathSync.native) so a Windows 8.3 short-name
|
|
85
|
+
// or a macOS /var -> /private/var symlink resolves — fs.watch event paths
|
|
86
|
+
// then prefix-match the watched root.
|
|
87
|
+
check("watcher.create: returns stop + canonical root",
|
|
88
|
+
typeof w.stop === "function" && w.root === fs.realpathSync.native(path.resolve(tmpDir)));
|
|
86
89
|
|
|
87
90
|
// Drop the legacy "prime the watcher with a 200ms sleep" step —
|
|
88
91
|
// helpers.waitForWatcher (15s default budget) absorbs fs.watch's
|
|
@@ -151,7 +154,8 @@ async function run() {
|
|
|
151
154
|
var shape = changes.find(function (e) { return e.relativePath === "shape.txt"; });
|
|
152
155
|
check("watcher.create: onChange has type/relativePath/fullPath/size/mtime",
|
|
153
156
|
shape && shape.type === "file" && shape.size === 4 &&
|
|
154
|
-
|
|
157
|
+
// fullPath is rooted at the canonical (realpath'd) watcher root.
|
|
158
|
+
shape.fullPath === path.join(w.root, "shape.txt") &&
|
|
155
159
|
shape.mtime instanceof Date);
|
|
156
160
|
|
|
157
161
|
// Symlinks must be skipped on the post-event lstat path. Skip on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/blamejs-shop",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.37",
|
|
4
4
|
"description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"release": "node scripts/release.js"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=24.
|
|
15
|
+
"node": ">=24.16.0"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"lib/",
|