@blamejs/blamejs-shop 0.4.33 → 0.4.38
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/lib/asset-manifest.json +1 -1
- package/lib/checkout.js +8 -0
- package/lib/loyalty.js +8 -1
- package/lib/order.js +38 -10
- package/lib/vendor/MANIFEST.json +54 -38
- 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 +4 -0
- package/lib/vendor/blamejs/README.md +5 -2
- package/lib/vendor/blamejs/SECURITY.md +3 -1
- 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/cli.js +8 -1
- package/lib/vendor/blamejs/lib/config-drift.js +2 -1
- 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/self-update.js +1 -1
- package/lib/vendor/blamejs/lib/session.js +64 -0
- 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.8.json +48 -0
- package/lib/vendor/blamejs/release-notes/v0.15.9.json +58 -0
- package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +338 -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 +174 -3
- 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/session-extensions.test.js +57 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +7 -3
- package/package.json +1 -1
|
@@ -2601,10 +2601,22 @@ async function testNoDuplicateCodeBlocks() {
|
|
|
2601
2601
|
var w = new Worker(WORKER_PATH, {
|
|
2602
2602
|
workerData: Object.assign({ files: shardFiles }, SHINGLE_OPTS_FOR_WORKER),
|
|
2603
2603
|
});
|
|
2604
|
-
|
|
2605
|
-
|
|
2604
|
+
// Reap the worker on EVERY settle path (#123). Previously the error and
|
|
2605
|
+
// exit handlers rejected without terminate(), so an errored worker thread
|
|
2606
|
+
// stayed alive holding its event-loop handles — the parent could not exit
|
|
2607
|
+
// and the smoke run ran to the 25-min watchdog on memory-starved
|
|
2608
|
+
// macOS-arm64 runners. settle() terminates first, idempotently.
|
|
2609
|
+
var settled = false;
|
|
2610
|
+
function settle(fn, arg) {
|
|
2611
|
+
if (settled) return;
|
|
2612
|
+
settled = true;
|
|
2613
|
+
try { w.terminate(); } catch (_e) { /* already terminating */ }
|
|
2614
|
+
fn(arg);
|
|
2615
|
+
}
|
|
2616
|
+
w.once("message", function (msg) { settle(resolve, msg); });
|
|
2617
|
+
w.once("error", function (e) { settle(reject, e); });
|
|
2606
2618
|
w.once("exit", function (code) {
|
|
2607
|
-
if (code !== 0 && code !== null) reject
|
|
2619
|
+
if (code !== 0 && code !== null) settle(reject, new Error("shingle worker exited " + code));
|
|
2608
2620
|
});
|
|
2609
2621
|
});
|
|
2610
2622
|
}
|
|
@@ -2991,6 +3003,79 @@ async function testNoDuplicateCodeBlocks() {
|
|
|
2991
3003
|
],
|
|
2992
3004
|
reason: "Config-time validateOpts cascade family — `validateOpts(opts, KEYS, label)` followed by per-field requireNonEmptyString / optionalPositiveInt / optionalFunction checks at a factory or builder entry point. jar.js:build joined in v0.14.22 (the RFC 9101 request-object builder validates clientId / audience / key / expiresInMs before minting). Each member emits its own error class and code namespace over a different opts vocabulary; the shared helper (validateOpts) is the extraction, and consolidating the cascades past the call boundary would surface the wrong error code for operator typos.",
|
|
2993
3005
|
},
|
|
3006
|
+
{
|
|
3007
|
+
// The b.dsa (EU DSA) + b.pipl (China PIPL) compliance record-builders
|
|
3008
|
+
// are a fresh pair of validateOpts-cascade + Object.freeze-record +
|
|
3009
|
+
// audit.safeEmit functions. Each validates a DIFFERENT opts vocabulary
|
|
3010
|
+
// (notice / statement-of-reasons / transparency-report; cross-border
|
|
3011
|
+
// assessment / security-assessment certificate), builds a DIFFERENT
|
|
3012
|
+
// frozen record, and emits a DIFFERENT audit action — so their 50/60-tok
|
|
3013
|
+
// shingles coincide with the large existing compliance/builder family
|
|
3014
|
+
// (validateOpts cascade + audit-emit), bridging the config-validation
|
|
3015
|
+
// and audit-emit clusters. Shape-only: the shared primitives
|
|
3016
|
+
// (validateOpts, audit.safeEmit, Object.freeze) ARE the extraction;
|
|
3017
|
+
// collapsing the per-builder bodies would surface the wrong error code
|
|
3018
|
+
// and the wrong audit action for each regulation. Union of the clusters
|
|
3019
|
+
// the two new files participate in.
|
|
3020
|
+
mode: "family-subset",
|
|
3021
|
+
files: [
|
|
3022
|
+
"lib/ai-adverse-decision.js:wrap",
|
|
3023
|
+
"lib/ai-dp.js:budget",
|
|
3024
|
+
"lib/api-key.js:_validateIssueOpts",
|
|
3025
|
+
"lib/audit-daily-review.js:create",
|
|
3026
|
+
"lib/auth/jar.js:build",
|
|
3027
|
+
"lib/auth/oauth.js:buildClientAttestationPop",
|
|
3028
|
+
"lib/auth/oid4vci.js:create",
|
|
3029
|
+
"lib/auth/saml.js:create",
|
|
3030
|
+
"lib/auth/sd-jwt-vc-issuer.js:create",
|
|
3031
|
+
"lib/break-glass.js:_validatePolicySet",
|
|
3032
|
+
"lib/budr.js:declare",
|
|
3033
|
+
"lib/calendar.js:validate",
|
|
3034
|
+
"lib/cloud-events.js:wrap",
|
|
3035
|
+
"lib/compliance-eaa.js:create",
|
|
3036
|
+
"lib/compliance-sanctions-fetcher.js:create",
|
|
3037
|
+
"lib/cose.js:macVerify0",
|
|
3038
|
+
"lib/cose.js:verify",
|
|
3039
|
+
"lib/crypto-field.js:declarePerRowResidency",
|
|
3040
|
+
"lib/daemon.js:_validateStartOpts",
|
|
3041
|
+
"lib/daemon.js:_validateStopOpts",
|
|
3042
|
+
"lib/data-act.js:recordSwitchRequest",
|
|
3043
|
+
"lib/data-act.js:shareWithThirdParty",
|
|
3044
|
+
"lib/db.js:declareRequireDualControl",
|
|
3045
|
+
"lib/ddl-change-control.js:create",
|
|
3046
|
+
"lib/dsa.js:noticeAndAction",
|
|
3047
|
+
"lib/dsa.js:statementOfReasons",
|
|
3048
|
+
"lib/dsr.js:create",
|
|
3049
|
+
"lib/external-db-migrate.js:create",
|
|
3050
|
+
"lib/fda-21cfr11.js:posture",
|
|
3051
|
+
"lib/fdx.js:consentReceipt",
|
|
3052
|
+
"lib/fedcm.js:wellKnown",
|
|
3053
|
+
"lib/file-upload.js:_validateCreateOpts",
|
|
3054
|
+
"lib/http-client-cache.js:create",
|
|
3055
|
+
"lib/http-client.js:_validateDownloadOpts",
|
|
3056
|
+
"lib/mcp-tool-registry.js:verifyCall",
|
|
3057
|
+
"lib/middleware/assetlinks.js:create",
|
|
3058
|
+
"lib/middleware/protected-resource-metadata.js:create",
|
|
3059
|
+
"lib/middleware/span-http-server.js:create",
|
|
3060
|
+
"lib/network-heartbeat.js:start",
|
|
3061
|
+
"lib/network-tls.js:_emitAuditAdd",
|
|
3062
|
+
"lib/network-tls.js:_emitAuditRemove",
|
|
3063
|
+
"lib/observability-tracer.js:create",
|
|
3064
|
+
"lib/outbox.js:create",
|
|
3065
|
+
"lib/pipl-cn.js:sccFilingAssessment",
|
|
3066
|
+
"lib/pipl-cn.js:securityAssessmentCertificate",
|
|
3067
|
+
"lib/privacy.js:vendorReview",
|
|
3068
|
+
"lib/redact.js:installOutboundDlp",
|
|
3069
|
+
"lib/sec-cyber.js:eightKArtifact",
|
|
3070
|
+
"lib/self-update.js:_validatePollOpts",
|
|
3071
|
+
"lib/self-update.js:_validateVerifyOpts",
|
|
3072
|
+
"lib/static.js:_validateCreateOpts",
|
|
3073
|
+
"lib/tcpa-10dlc.js:recordConsent",
|
|
3074
|
+
"lib/vex.js:document",
|
|
3075
|
+
"lib/watcher.js:_validateOpts",
|
|
3076
|
+
],
|
|
3077
|
+
reason: "v0.15.8 — b.dsa (EU Digital Services Act) + b.pipl (China PIPL) cross-border record-builders join the config-time validateOpts-cascade + Object.freeze-record + audit.safeEmit family. Each validates a distinct opts vocabulary, builds a distinct frozen record, and emits a distinct audit action; the shared primitives (validateOpts / audit.safeEmit / Object.freeze) are the extraction. Shape-only — collapsing the per-builder bodies would emit the wrong error code and audit action per regulation. Union of the 50/60-tok clusters the two new files participate in.",
|
|
3078
|
+
},
|
|
2994
3079
|
{
|
|
2995
3080
|
mode: "family-subset",
|
|
2996
3081
|
files: [
|
|
@@ -6997,6 +7082,74 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
6997
7082
|
allowlist: [],
|
|
6998
7083
|
reason: "CWE-532 — span/metric/resource attributes are a first-class egress sink. #69 fixed lib/otel-export.js _attrsToOtlp but pinned its detector to that one function name, leaving the SPAN exporter's two sibling encoders (lib/observability-otlp-exporter.js _attrToOtlp JSON + _attrsToProto protobuf) shipping attribute values verbatim to the collector (#125). The shared root is 'an attribute-map encoder that serializes without scrubbing'; every such encoder must pass each value through observability.redactAttrs() (default composes b.redact.redact, fail-toward-dropping on a throwing redactor) before the wire payload. The negative lookahead exempts the per-value type-encoders (_valueToOtlp/_anyValueToProto) which run after redaction; the {0,4000} bound is a ReDoS backstop well above the real encoder bodies (<2000 chars).",
|
|
6999
7084
|
},
|
|
7085
|
+
// Same CWE-532 class, the OTHER OTLP egress family the span/metric detector
|
|
7086
|
+
// above could not see: the LOG sinks (lib/log-stream-otlp.js HTTP-JSON +
|
|
7087
|
+
// lib/log-stream-otlp-grpc.js gRPC). Their encoders are named per the OTLP
|
|
7088
|
+
// logs schema (_toLogRecord / _serializeBatch / _encodeLogRecord /
|
|
7089
|
+
// _encodeResourceLogs), not _attr*, so the function-name-anchored detector
|
|
7090
|
+
// never matched them — the #125 span fix left the log record-meta + resource
|
|
7091
|
+
// attributes shipping verbatim. Anchor on the buggy SHAPE instead: a raw
|
|
7092
|
+
// `record.meta` / `resourceAttrs` / `_resourceAttrs(cfg)` handed straight to an
|
|
7093
|
+
// _encode* call. The fix wraps the arg in observability().redactAttrs(...), so
|
|
7094
|
+
// the first token inside the paren becomes `observability` and the match dies.
|
|
7095
|
+
{
|
|
7096
|
+
id: "otlp-log-sink-encodes-attrs-without-redactor",
|
|
7097
|
+
primitive: "the OTLP LOG sinks (log-stream-otlp / log-stream-otlp-grpc) must run record.meta and resource attributes through observability.redactAttrs() before encoding — a log line's meta or a resource attribute holding a bearer token / password / API key ships to the collector verbatim otherwise (CWE-532), the same egress class as the span/metric exporters",
|
|
7098
|
+
scanScope: "lib",
|
|
7099
|
+
regex: /_encode(?:Attrs|Attributes|Resource)\(\s*(?:record\.meta\b|resourceAttrs\b|_resourceAttrs\(cfg\))/,
|
|
7100
|
+
allowlist: [],
|
|
7101
|
+
reason: "CWE-532 secret/PII egress. v0.15.4 (#125) baked observability.redactAttrs() into every SPAN + METRIC attribute encoder but the detector was anchored on the _attr* function names, so it was blind to the LOG sinks whose encoders carry the OTLP-logs schema names. lib/log-stream-otlp.js (_toLogRecord meta + _serializeBatch resource) and lib/log-stream-otlp-grpc.js (_encodeLogRecord meta + _encodeResourceLogs resource) handed record.meta and resourceAttrs straight to _encodeAttrs/_encodeAttributes/_encodeResource — a log record's meta or a resource attribute holding a credential reached the collector unscrubbed. Root: 'every OTLP egress encoder redacts'; the span detector saw spans/metrics, this one sees logs. Fires on the raw `_encode*(record.meta` / `_encode*(resourceAttrs` / `_encodeResource(_resourceAttrs(cfg)` shape; the fix wraps the arg in observability().redactAttrs(...) (the span/metric contract), making the first paren token `observability` so the match goes silent.",
|
|
7102
|
+
},
|
|
7103
|
+
// #146 — every FINAL temp->dest rename must route through
|
|
7104
|
+
// atomicFile.renameWithRetry, the bounded retry on a Windows-transient
|
|
7105
|
+
// destination lock (EPERM/EACCES/EBUSY from AV / the search indexer /
|
|
7106
|
+
// Dropbox / OneDrive briefly holding the target). A bare nodeFs.renameSync
|
|
7107
|
+
// surfaces a transient lock as a hard failure (httpClient.downloadStream
|
|
7108
|
+
// shipped exactly that) and re-hand-rolls a retry the framework already owns
|
|
7109
|
+
// as a primitive. The ONLY legitimate bare renameSync is inside the
|
|
7110
|
+
// primitive itself (atomic-file.js _renameWithRetry).
|
|
7111
|
+
{
|
|
7112
|
+
id: "bare-renamesync-not-via-renamewithretry",
|
|
7113
|
+
primitive: "route every final temp->dest rename through atomicFile.renameWithRetry (the Windows rename-lock retry) instead of a bare nodeFs.renameSync / fs.renameSync — a transient AV / indexer / cloud-sync lock on the destination must be retried, not surfaced as a hard failure",
|
|
7114
|
+
scanScope: "lib",
|
|
7115
|
+
regex: /\b(?:nodeFs|fs)\.renameSync\(/,
|
|
7116
|
+
allowlist: [
|
|
7117
|
+
// atomic-file.js IS the primitive — _renameWithRetry wraps the one real
|
|
7118
|
+
// nodeFs.renameSync with the bounded transient-lock retry loop.
|
|
7119
|
+
"lib/atomic-file.js",
|
|
7120
|
+
],
|
|
7121
|
+
reason: "#146 (user-surfaced). atomicFile.writeSync retries its final rename on a Windows-transient destination lock; httpClient.downloadStream's final rename was a bare nodeFs.renameSync, so a download into a cloud-synced / AV-scanned directory surfaced the transient lock as a hard failure. The retry was also hand-rolled and un-reusable. The fix exports atomic-file's _renameWithRetry as atomicFile.renameWithRetry and routes EVERY final temp->dest rename through it (downloadStream + vault/passphrase-ops + mtls-ca + log-stream-local + self-update + config-drift + archive-read + archive-tar-read + local-db-thin + restore-rollback). Fires on any bare nodeFs.renameSync/fs.renameSync; the only allowlisted use is atomic-file.js where the primitive's retry loop lives.",
|
|
7122
|
+
},
|
|
7123
|
+
// v0.15.9 — db.init() must construct its DatabaseSync with the SQLITE_LIMIT_*
|
|
7124
|
+
// sqlLength cap: a parse-time DoS floor that rejects a megaquery on the
|
|
7125
|
+
// raw-SQL surface. The ephemeral storage headroom probe (new DatabaseSync(p))
|
|
7126
|
+
// is a different construction and is not anchored. Fires if the main db handle
|
|
7127
|
+
// is built without the limits shape.
|
|
7128
|
+
{
|
|
7129
|
+
id: "db-databasesync-without-sqlite-limits",
|
|
7130
|
+
primitive: "construct the main db.init DatabaseSync(dbPath, ...) with the node:sqlite limits option (sqlLength) — a parse-time DoS floor complementary to streamLimit",
|
|
7131
|
+
scanScope: "lib",
|
|
7132
|
+
regex: /new DatabaseSync\(dbPath\b/,
|
|
7133
|
+
requires: /limits:\s*\{[\s\S]{0,200}?sqlLength/,
|
|
7134
|
+
skipCommentLines: true,
|
|
7135
|
+
allowlist: [],
|
|
7136
|
+
reason: "v0.15.9 Node-24.16 adoption. The framework parameterizes every builder value, so SQLITE_LIMIT_LENGTH (sqlLength) guards the raw-SQL surface (b.db.runSql) against an attacker-influenced megaquery the parser would otherwise chew (SQLite default is 1 GB). Fires when the main db handle `new DatabaseSync(dbPath` is constructed without the `limits: { ... sqlLength` shape; the headroom probe `new DatabaseSync(p)` is intentionally not matched (ephemeral, no attacker input). (SQLITE_LIMIT_ATTACHED is left at the SQLite default — the snapshot/backup path uses ATTACH.)",
|
|
7137
|
+
},
|
|
7138
|
+
// v0.15.9 — the RFC 9527 Clear-Site-Data header value must be built via the
|
|
7139
|
+
// shared middleware/clear-site-data headerValue() helper (which validates
|
|
7140
|
+
// each directive against the known set), not a hand-rolled quoted-token
|
|
7141
|
+
// string. A literal `setHeader("Clear-Site-Data", '"cookies", ...')` skips
|
|
7142
|
+
// the directive validation and re-hand-rolls the RFC 9527 quoting the
|
|
7143
|
+
// primitive owns (the b.session.logout extraction lesson).
|
|
7144
|
+
{
|
|
7145
|
+
id: "clear-site-data-header-hand-rolled",
|
|
7146
|
+
primitive: "build the Clear-Site-Data response header via clearSiteData.headerValue(types) (validated RFC 9527 quoting) — do not hand-roll the quoted-directive string literal in setHeader",
|
|
7147
|
+
scanScope: "lib",
|
|
7148
|
+
regex: /setHeader\(\s*["']Clear-Site-Data["']\s*,\s*["']/,
|
|
7149
|
+
skipCommentLines: true,
|
|
7150
|
+
allowlist: [],
|
|
7151
|
+
reason: "v0.15.9 (Clear-Site-Data logout wiring). The middleware/clear-site-data headerValue() helper is the single builder of the RFC 9527 §3 quoted-token list and validates every directive against KNOWN_TYPES; both emitters (the middleware's create() and b.session.logout) route through it. A literal `setHeader(\"Clear-Site-Data\", '\"cookies\", ...')` hand-rolls the quoting and skips the validation. Fires when the second setHeader arg is a string literal; the live emitters pass a var/call so they stay silent.",
|
|
7152
|
+
},
|
|
7000
7153
|
// #131 — the b.middleware.dpop factory must REQUIRE its replayStore at config
|
|
7001
7154
|
// time. The store is DPoP's jti-replay defense (RFC 9449 §11.1); reading it
|
|
7002
7155
|
// optionally and gating the check behind `if (replayStore)` silently mounts a
|
|
@@ -9374,6 +9527,24 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
9374
9527
|
// detector encoded. These 5 are the highest-priority subset (P1 +
|
|
9375
9528
|
// P2 by audit ranking). The remaining 8 are scoped for follow-up.
|
|
9376
9529
|
|
|
9530
|
+
{
|
|
9531
|
+
// #123 — a worker_threads Worker whose error/exit handler rejects WITHOUT
|
|
9532
|
+
// terminating leaves the thread alive holding its event-loop handles, so the
|
|
9533
|
+
// parent process can't exit and the smoke run hangs under the watchdog on
|
|
9534
|
+
// memory-starved CI runners (macOS-arm64). Every Promise-settle path
|
|
9535
|
+
// (message / error / exit) must reap the worker via w.terminate() first; the
|
|
9536
|
+
// fix funnels all three through a settle() guard that terminates before
|
|
9537
|
+
// resolve/reject. Structural resource-hygiene drift a behavioral test can't
|
|
9538
|
+
// assert — the harness closure isn't exported and the hang is a slow-runner
|
|
9539
|
+
// race — so the detector is the guard.
|
|
9540
|
+
id: "test-worker-reject-without-terminate",
|
|
9541
|
+
primitive: "a worker_threads Worker error/exit handler must terminate() the worker before rejecting (route message/error/exit through a settle() helper) — a bare reject leaks the thread's handles and hangs process exit on slow CI runners",
|
|
9542
|
+
scanScope: "test",
|
|
9543
|
+
regex: /\bw\.once\("error",\s*reject\)/,
|
|
9544
|
+
skipCommentLines: true,
|
|
9545
|
+
allowlist: [],
|
|
9546
|
+
reason: "#123 macOS codebase-patterns watchdog hang. _scanShardInWorker rejected on worker error/exit without w.terminate(), so an errored worker thread stayed alive holding open handles; the parent then could not exit and the smoke run ran to the 25-min watchdog on memory-starved macOS-arm64 runners (it hung this very release's CI). Every settle path must reap the worker via w.terminate() first; the fix funnels message/error/exit through a settle() guard that terminates before resolve/reject. Fires on the bare `w.once(\"error\", reject)` shape; silent once error/exit route through settle().",
|
|
9547
|
+
},
|
|
9377
9548
|
{
|
|
9378
9549
|
// `Promise + setTimeout` direct sleep in tests is forbidden;
|
|
9379
9550
|
// tests waiting on an asynchronous condition MUST use
|
|
@@ -146,6 +146,37 @@ async function testSnapshotPlainMode() {
|
|
|
146
146
|
await b.db.close();
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
async function testSqliteResourceLimits() {
|
|
150
|
+
// The node:sqlite `limits` option caps SQLITE_LIMIT_* at construction: a
|
|
151
|
+
// parse-time DoS floor. A statement over 1 MiB is rejected before SQLite's
|
|
152
|
+
// parser processes it, and ATTACH DATABASE is denied (the framework never
|
|
153
|
+
// uses it). RED on a tree without the limits option: the megaquery parses.
|
|
154
|
+
var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sqlim-"));
|
|
155
|
+
await _resetState();
|
|
156
|
+
await b.vault.init({ dataDir: tmpDir, mode: "plaintext" });
|
|
157
|
+
await b.db.init({
|
|
158
|
+
dataDir: tmpDir,
|
|
159
|
+
atRest: "plain",
|
|
160
|
+
schema: [{ name: "t", columns: { _id: "TEXT PRIMARY KEY" } }],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// A raw statement whose SQL TEXT exceeds 1 MiB is rejected at parse
|
|
164
|
+
// (sqlLength caps statement text, not bound-parameter values — the builder
|
|
165
|
+
// path always parameterizes, so this cap guards the raw-SQL surface).
|
|
166
|
+
var hugeSql = "SELECT '" + "x".repeat(1100000) + "' AS big";
|
|
167
|
+
var threw = null;
|
|
168
|
+
try { b.db.runSql(hugeSql); }
|
|
169
|
+
catch (e) { threw = e; }
|
|
170
|
+
check("sqlite limits: a >1 MiB raw statement is rejected at parse", threw !== null);
|
|
171
|
+
|
|
172
|
+
// A normal statement under the cap is unaffected.
|
|
173
|
+
b.db.from("t").insertOne({ _id: "normal" });
|
|
174
|
+
check("sqlite limits: a normal statement is unaffected",
|
|
175
|
+
b.db.from("t").where({ _id: "normal" }).first()._id === "normal");
|
|
176
|
+
|
|
177
|
+
await b.db.close();
|
|
178
|
+
}
|
|
179
|
+
|
|
149
180
|
async function run() {
|
|
150
181
|
await testFrameworkTablesOff();
|
|
151
182
|
await testAuditSigningOff();
|
|
@@ -153,6 +184,7 @@ async function run() {
|
|
|
153
184
|
await testDbKeyPathOverride();
|
|
154
185
|
await testSnapshot();
|
|
155
186
|
await testSnapshotPlainMode();
|
|
187
|
+
await testSqliteResourceLimits();
|
|
156
188
|
}
|
|
157
189
|
|
|
158
190
|
module.exports = { run: run };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// b.dsa — EU Digital Services Act (Reg 2022/2065) record-builders:
|
|
3
|
+
// Art. 16 noticeAndAction, Art. 17 statementOfReasons, Art. 15/24(3)
|
|
4
|
+
// transparencyReport. Pure builders (no DB); audit emission is captured
|
|
5
|
+
// by swapping b.audit.safeEmit for the duration of each assertion.
|
|
6
|
+
|
|
7
|
+
var helpers = require("../helpers");
|
|
8
|
+
var b = helpers.b;
|
|
9
|
+
var check = helpers.check;
|
|
10
|
+
|
|
11
|
+
// Capture the audit events a builder emits. b.dsa resolves its audit
|
|
12
|
+
// sink via require("./audit"), the same module object as b.audit, so
|
|
13
|
+
// swapping b.audit.safeEmit intercepts the emission. Always restore in a
|
|
14
|
+
// finally so a thrown assertion can't leak the stub.
|
|
15
|
+
function captureAudit(fn) {
|
|
16
|
+
var events = [];
|
|
17
|
+
var orig = b.audit.safeEmit;
|
|
18
|
+
b.audit.safeEmit = function (ev) { events.push(ev); };
|
|
19
|
+
try { fn(events); }
|
|
20
|
+
finally { b.audit.safeEmit = orig; }
|
|
21
|
+
return events;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function expectCode(label, fn, code) {
|
|
25
|
+
var threw = null;
|
|
26
|
+
try { fn(); } catch (e) { threw = e; }
|
|
27
|
+
check(label, threw && (threw.code || "") === code);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function expectThrows(label, fn) {
|
|
31
|
+
var threw = null;
|
|
32
|
+
try { fn(); } catch (e) { threw = e; }
|
|
33
|
+
check(label, threw instanceof Error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function run() {
|
|
37
|
+
// ---- surface ----
|
|
38
|
+
check("noticeAndAction is a function", typeof b.dsa.noticeAndAction === "function");
|
|
39
|
+
check("statementOfReasons is a function", typeof b.dsa.statementOfReasons === "function");
|
|
40
|
+
check("transparencyReport is a function", typeof b.dsa.transparencyReport === "function");
|
|
41
|
+
check("listTransparencyMetrics is a function", typeof b.dsa.listTransparencyMetrics === "function");
|
|
42
|
+
check("DsaError exposed on b.frameworkError", typeof b.frameworkError.DsaError === "function");
|
|
43
|
+
check("b.dsa.DsaError is the same constructor", b.dsa.DsaError === b.frameworkError.DsaError);
|
|
44
|
+
|
|
45
|
+
// ===== Art. 16 — noticeAndAction =====
|
|
46
|
+
var submittedAt = 1700000000000;
|
|
47
|
+
var nEvents = captureAudit(function () {
|
|
48
|
+
var n = b.dsa.noticeAndAction({
|
|
49
|
+
contentId: "post-9931",
|
|
50
|
+
noticeType: "illegal-content",
|
|
51
|
+
reason: "Depicts a sale prohibited under national law.",
|
|
52
|
+
submittedAt: submittedAt,
|
|
53
|
+
submitterType: "trusted-flagger",
|
|
54
|
+
});
|
|
55
|
+
check("notice: record frozen", Object.isFrozen(n));
|
|
56
|
+
check("notice: status recorded", n.status === "recorded");
|
|
57
|
+
check("notice: default noticeId derived", n.noticeId === "dsa-notice-" + submittedAt);
|
|
58
|
+
check("notice: actionDueBy = submittedAt+24h", n.actionDueBy === submittedAt + b.constants.TIME.hours(24));
|
|
59
|
+
check("notice: illegal-content requires SoR", n.statementOfReasonsRequired === true);
|
|
60
|
+
});
|
|
61
|
+
check("notice: emitted one audit event", nEvents.length === 1);
|
|
62
|
+
check("notice: audit action dsa.notice.recorded", nEvents[0] && nEvents[0].action === "dsa.notice.recorded");
|
|
63
|
+
check("notice: audit metadata carries contentId", nEvents[0] && nEvents[0].metadata.contentId === "post-9931");
|
|
64
|
+
|
|
65
|
+
var nTerms = b.dsa.noticeAndAction({
|
|
66
|
+
contentId: "post-1", noticeType: "terms-violation", reason: "Breaches guidelines.",
|
|
67
|
+
submittedAt: submittedAt, submitterType: "individual",
|
|
68
|
+
noticeId: "op-77", actionWindowMs: b.constants.TIME.hours(48),
|
|
69
|
+
});
|
|
70
|
+
check("notice: terms-violation no SoR", nTerms.statementOfReasonsRequired === false);
|
|
71
|
+
check("notice: explicit noticeId honored", nTerms.noticeId === "op-77");
|
|
72
|
+
check("notice: explicit actionWindow honored", nTerms.actionDueBy === submittedAt + b.constants.TIME.hours(48));
|
|
73
|
+
|
|
74
|
+
expectCode("notice: non-object opts throws",
|
|
75
|
+
function () { b.dsa.noticeAndAction("nope"); }, "dsa/bad-opts");
|
|
76
|
+
expectThrows("notice: unknown opt key throws",
|
|
77
|
+
function () { b.dsa.noticeAndAction({ contentId: "c", noticeType: "other", reason: "r", submittedAt: submittedAt, submitterType: "individual", bogus: 1 }); });
|
|
78
|
+
expectCode("notice: missing contentId throws",
|
|
79
|
+
function () { b.dsa.noticeAndAction({ noticeType: "other", reason: "r", submittedAt: submittedAt, submitterType: "individual" }); }, "dsa/bad-content-id");
|
|
80
|
+
expectCode("notice: unknown noticeType throws",
|
|
81
|
+
function () { b.dsa.noticeAndAction({ contentId: "c", noticeType: "wat", reason: "r", submittedAt: submittedAt, submitterType: "individual" }); }, "dsa/unknown-notice-type");
|
|
82
|
+
expectCode("notice: missing submittedAt throws",
|
|
83
|
+
function () { b.dsa.noticeAndAction({ contentId: "c", noticeType: "other", reason: "r", submitterType: "individual" }); }, "dsa/bad-submitted-at");
|
|
84
|
+
expectCode("notice: unknown submitterType throws",
|
|
85
|
+
function () { b.dsa.noticeAndAction({ contentId: "c", noticeType: "other", reason: "r", submittedAt: submittedAt, submitterType: "robot" }); }, "dsa/unknown-submitter-type");
|
|
86
|
+
|
|
87
|
+
// ===== Art. 17 — statementOfReasons =====
|
|
88
|
+
var sEvents = captureAudit(function () {
|
|
89
|
+
var s = b.dsa.statementOfReasons({
|
|
90
|
+
contentId: "post-9931", decision: "content-removed",
|
|
91
|
+
legalGround: "National law prohibiting the depicted sale.",
|
|
92
|
+
facts: "Listing offered a prohibited item for sale.", automated: false,
|
|
93
|
+
redressOptions: ["internal-complaint", "judicial-redress"], noticeId: "dsa-notice-" + submittedAt,
|
|
94
|
+
});
|
|
95
|
+
check("sor: record frozen", Object.isFrozen(s));
|
|
96
|
+
check("sor: default sorId derived", /^dsa-sor-\d+$/.test(s.sorId));
|
|
97
|
+
check("sor: groundType legal", s.groundType === "legal");
|
|
98
|
+
check("sor: contractualGround null", s.contractualGround === null);
|
|
99
|
+
check("sor: redressOptions frozen", Object.isFrozen(s.redressOptions) && s.redressOptions.length === 2);
|
|
100
|
+
check("sor: noticeId linked", s.noticeId === "dsa-notice-" + submittedAt);
|
|
101
|
+
});
|
|
102
|
+
check("sor: emitted one audit event", sEvents.length === 1);
|
|
103
|
+
check("sor: audit action dsa.sor.recorded", sEvents[0] && sEvents[0].action === "dsa.sor.recorded");
|
|
104
|
+
|
|
105
|
+
var sC = b.dsa.statementOfReasons({
|
|
106
|
+
contentId: "post-2", decision: "account-suspended",
|
|
107
|
+
contractualGround: "Terms section 4.2 — repeated spam.", facts: "Five spam posts in 24h.",
|
|
108
|
+
automated: true, redressOptions: ["internal-complaint"],
|
|
109
|
+
});
|
|
110
|
+
check("sor: groundType contractual", sC.groundType === "contractual");
|
|
111
|
+
check("sor: legalGround null", sC.legalGround === null);
|
|
112
|
+
check("sor: automated true", sC.automated === true);
|
|
113
|
+
|
|
114
|
+
expectCode("sor: unknown decision throws",
|
|
115
|
+
function () { b.dsa.statementOfReasons({ contentId: "c", decision: "nuke", legalGround: "x", facts: "f", automated: false, redressOptions: ["internal-complaint"] }); }, "dsa/unknown-decision");
|
|
116
|
+
expectCode("sor: non-boolean automated throws",
|
|
117
|
+
function () { b.dsa.statementOfReasons({ contentId: "c", decision: "no-action", legalGround: "x", facts: "f", automated: "no", redressOptions: ["internal-complaint"] }); }, "dsa/bad-automated");
|
|
118
|
+
expectCode("sor: neither ground throws",
|
|
119
|
+
function () { b.dsa.statementOfReasons({ contentId: "c", decision: "no-action", facts: "f", automated: false, redressOptions: ["internal-complaint"] }); }, "dsa/ground-required");
|
|
120
|
+
expectCode("sor: both grounds throws",
|
|
121
|
+
function () { b.dsa.statementOfReasons({ contentId: "c", decision: "no-action", legalGround: "x", contractualGround: "y", facts: "f", automated: false, redressOptions: ["internal-complaint"] }); }, "dsa/ground-required");
|
|
122
|
+
expectCode("sor: empty redressOptions throws",
|
|
123
|
+
function () { b.dsa.statementOfReasons({ contentId: "c", decision: "content-removed", legalGround: "x", facts: "f", automated: false, redressOptions: [] }); }, "dsa/redress-required");
|
|
124
|
+
expectCode("sor: unknown redress option throws",
|
|
125
|
+
function () { b.dsa.statementOfReasons({ contentId: "c", decision: "content-removed", legalGround: "x", facts: "f", automated: false, redressOptions: ["call-the-mayor"] }); }, "dsa/unknown-redress-option");
|
|
126
|
+
|
|
127
|
+
// ===== Art. 15 / 24(3) — transparencyReport =====
|
|
128
|
+
var metricNames = b.dsa.listTransparencyMetrics();
|
|
129
|
+
check("metrics: frozen list", Array.isArray(metricNames) && Object.isFrozen(metricNames));
|
|
130
|
+
check("metrics: includes noticesReceived", metricNames.indexOf("noticesReceived") !== -1);
|
|
131
|
+
|
|
132
|
+
var period = { from: Date.UTC(2025, 0, 1), to: Date.UTC(2025, 11, 31) };
|
|
133
|
+
var rEvents = captureAudit(function () {
|
|
134
|
+
var r = b.dsa.transparencyReport({
|
|
135
|
+
period: period,
|
|
136
|
+
metrics: { noticesReceived: 1200, actionsTaken: 940, automatedDecisions: 610, appeals: 75 },
|
|
137
|
+
service: "example-platform",
|
|
138
|
+
});
|
|
139
|
+
check("report: record frozen", Object.isFrozen(r));
|
|
140
|
+
check("report: metrics frozen", Object.isFrozen(r.metrics));
|
|
141
|
+
check("report: period frozen", Object.isFrozen(r.period));
|
|
142
|
+
check("report: supplied metric carried", r.metrics.noticesReceived === 1200);
|
|
143
|
+
check("report: omitted metric defaults 0", r.metrics.statementsOfReasons === 0);
|
|
144
|
+
check("report: all metric fields present", metricNames.every(function (m) { return typeof r.metrics[m] === "number"; }));
|
|
145
|
+
check("report: default reportId derived", r.reportId === "dsa-transparency-" + period.to);
|
|
146
|
+
check("report: nextReportDueBy = to+365d", r.nextReportDueBy === period.to + b.constants.TIME.days(365));
|
|
147
|
+
});
|
|
148
|
+
check("report: emitted one audit event", rEvents.length === 1);
|
|
149
|
+
check("report: audit action transparency event", rEvents[0] && rEvents[0].action === "dsa.transparency_report.generated");
|
|
150
|
+
check("report: audit metadata actionsTaken", rEvents[0] && rEvents[0].metadata.actionsTaken === 940);
|
|
151
|
+
|
|
152
|
+
expectCode("report: missing period throws",
|
|
153
|
+
function () { b.dsa.transparencyReport({ metrics: {} }); }, "dsa/bad-period");
|
|
154
|
+
expectCode("report: from >= to throws",
|
|
155
|
+
function () { b.dsa.transparencyReport({ period: { from: period.to, to: period.from } }); }, "dsa/bad-period-order");
|
|
156
|
+
expectCode("report: unknown metric key throws",
|
|
157
|
+
function () { b.dsa.transparencyReport({ period: period, metrics: { bogusMetric: 1 } }); }, "dsa/unknown-metric");
|
|
158
|
+
expectCode("report: negative metric throws",
|
|
159
|
+
function () { b.dsa.transparencyReport({ period: period, metrics: { appeals: -3 } }); }, "dsa/bad-metric-value");
|
|
160
|
+
expectCode("report: non-integer metric throws",
|
|
161
|
+
function () { b.dsa.transparencyReport({ period: period, metrics: { appeals: 2.5 } }); }, "dsa/bad-metric-value");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { run: run };
|
|
165
|
+
|
|
166
|
+
if (require.main === module) {
|
|
167
|
+
try { run(); console.log("[dsa] OK"); }
|
|
168
|
+
catch (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
169
|
+
}
|
|
@@ -15,7 +15,9 @@ var helpers = require("../helpers");
|
|
|
15
15
|
var b = helpers.b;
|
|
16
16
|
var check = helpers.check;
|
|
17
17
|
|
|
18
|
-
var otlp
|
|
18
|
+
var otlp = require("../../lib/observability-otlp-exporter");
|
|
19
|
+
var otlpLog = require("../../lib/log-stream-otlp");
|
|
20
|
+
var otlpLogGrpc = require("../../lib/log-stream-otlp-grpc");
|
|
19
21
|
|
|
20
22
|
var SECRET_TOKEN = "Bearer eyJSECRETtokenABCdef.ghi.jkl";
|
|
21
23
|
var SECRET_PW = "hunter2-PLAINTEXT-PASSWORD";
|
|
@@ -89,6 +91,43 @@ async function run() {
|
|
|
89
91
|
check("protobuf: api key redacted on the OTLP wire", protoWire.indexOf(SECRET_API) === -1);
|
|
90
92
|
check("protobuf: non-sensitive control attribute survives", protoWire.indexOf(CONTROL_VAL) !== -1);
|
|
91
93
|
|
|
94
|
+
// ---- OTLP LOG sinks are EGRESS too: record-meta + resource attrs must redact ----
|
|
95
|
+
// The span/metric exporters redact; the log sinks (HTTP-JSON + gRPC) shipped
|
|
96
|
+
// the same attribute maps to the collector UNREDACTED — a log line carrying a
|
|
97
|
+
// bearer token / password in its meta or a credential in a resource attribute
|
|
98
|
+
// reached the wire verbatim (CWE-532). Drive the serialization seam of both.
|
|
99
|
+
var logRec = otlpLog._toLogRecord({
|
|
100
|
+
ts: 1700000000000, level: "error", message: "boom",
|
|
101
|
+
meta: { "http.method": CONTROL_VAL, authorization: SECRET_TOKEN, password: SECRET_PW },
|
|
102
|
+
});
|
|
103
|
+
var logRecWire = JSON.stringify(logRec);
|
|
104
|
+
check("otlp-log json: record-meta bearer token redacted", logRecWire.indexOf(SECRET_TOKEN) === -1);
|
|
105
|
+
check("otlp-log json: record-meta password redacted", logRecWire.indexOf(SECRET_PW) === -1);
|
|
106
|
+
check("otlp-log json: non-sensitive record-meta survives", logRecWire.indexOf(CONTROL_VAL) !== -1);
|
|
107
|
+
|
|
108
|
+
var logBatchWire = otlpLog._serializeBatch(
|
|
109
|
+
[{ ts: 1700000000000, level: "info", message: "m", meta: { api_key: SECRET_API } }],
|
|
110
|
+
{ serviceName: "svc", resourceAttributes: { authorization: SECRET_TOKEN, region: CONTROL_VAL } },
|
|
111
|
+
"1.0.0"
|
|
112
|
+
).toString("utf8");
|
|
113
|
+
check("otlp-log json: resource-attr bearer token redacted", logBatchWire.indexOf(SECRET_TOKEN) === -1);
|
|
114
|
+
check("otlp-log json: record-meta api key redacted", logBatchWire.indexOf(SECRET_API) === -1);
|
|
115
|
+
check("otlp-log json: non-sensitive resource attr survives", logBatchWire.indexOf(CONTROL_VAL) !== -1);
|
|
116
|
+
|
|
117
|
+
var grpcRec = otlpLogGrpc._encodeLogRecord({
|
|
118
|
+
ts: 1700000000000, level: "error", message: "boom",
|
|
119
|
+
meta: { authorization: SECRET_TOKEN, password: SECRET_PW },
|
|
120
|
+
}).toString("latin1");
|
|
121
|
+
check("otlp-log grpc: record-meta bearer token redacted", grpcRec.indexOf(SECRET_TOKEN) === -1);
|
|
122
|
+
check("otlp-log grpc: record-meta password redacted", grpcRec.indexOf(SECRET_PW) === -1);
|
|
123
|
+
|
|
124
|
+
var grpcReq = otlpLogGrpc._encodeExportRequest(
|
|
125
|
+
[{ ts: 1700000000000, level: "info", message: "m", meta: { api_key: SECRET_API } }],
|
|
126
|
+
{ serviceName: "svc", resourceAttributes: { authorization: SECRET_TOKEN } }
|
|
127
|
+
).toString("latin1");
|
|
128
|
+
check("otlp-log grpc: resource-attr bearer token redacted", grpcReq.indexOf(SECRET_TOKEN) === -1);
|
|
129
|
+
check("otlp-log grpc: record-meta api key redacted", grpcReq.indexOf(SECRET_API) === -1);
|
|
130
|
+
|
|
92
131
|
b.observability.setRedactor(null); // restore default for other tests
|
|
93
132
|
process.stdout.write("OK — otlp attribute redaction tests\n");
|
|
94
133
|
}
|