@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/checkout.js +8 -0
  4. package/lib/loyalty.js +8 -1
  5. package/lib/order.js +38 -10
  6. package/lib/vendor/MANIFEST.json +54 -38
  7. package/lib/vendor/blamejs/.github/workflows/ci.yml +12 -12
  8. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +37 -5
  9. package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
  10. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  11. package/lib/vendor/blamejs/README.md +5 -2
  12. package/lib/vendor/blamejs/SECURITY.md +3 -1
  13. package/lib/vendor/blamejs/api-snapshot.json +137 -2
  14. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
  15. package/lib/vendor/blamejs/index.js +4 -0
  16. package/lib/vendor/blamejs/lib/archive-read.js +2 -1
  17. package/lib/vendor/blamejs/lib/archive-tar-read.js +2 -1
  18. package/lib/vendor/blamejs/lib/atomic-file.js +5 -0
  19. package/lib/vendor/blamejs/lib/audit.js +2 -0
  20. package/lib/vendor/blamejs/lib/cli.js +8 -1
  21. package/lib/vendor/blamejs/lib/config-drift.js +2 -1
  22. package/lib/vendor/blamejs/lib/db.js +15 -2
  23. package/lib/vendor/blamejs/lib/dsa.js +482 -0
  24. package/lib/vendor/blamejs/lib/framework-error.js +14 -0
  25. package/lib/vendor/blamejs/lib/http-client.js +5 -2
  26. package/lib/vendor/blamejs/lib/local-db-thin.js +3 -2
  27. package/lib/vendor/blamejs/lib/log-stream-local.js +1 -1
  28. package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +9 -2
  29. package/lib/vendor/blamejs/lib/log-stream-otlp.js +16 -7
  30. package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +36 -11
  31. package/lib/vendor/blamejs/lib/mtls-ca.js +2 -2
  32. package/lib/vendor/blamejs/lib/observability.js +3 -2
  33. package/lib/vendor/blamejs/lib/pipl-cn.js +377 -0
  34. package/lib/vendor/blamejs/lib/restore-rollback.js +5 -5
  35. package/lib/vendor/blamejs/lib/self-update.js +1 -1
  36. package/lib/vendor/blamejs/lib/session.js +64 -0
  37. package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -3
  38. package/lib/vendor/blamejs/lib/watcher.js +8 -0
  39. package/lib/vendor/blamejs/package.json +2 -2
  40. package/lib/vendor/blamejs/release-notes/v0.15.8.json +48 -0
  41. package/lib/vendor/blamejs/release-notes/v0.15.9.json +58 -0
  42. package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +338 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-rename-retry.test.js +70 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +174 -3
  45. package/lib/vendor/blamejs/test/layer-0-primitives/db-init-extensions.test.js +32 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/dsa.test.js +169 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +40 -1
  48. package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +172 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +57 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +7 -3
  51. package/package.json +1 -1
@@ -169,7 +169,7 @@ async function seal(opts) {
169
169
  }
170
170
 
171
171
  // Step 3: atomic rename sealed.tmp → sealed
172
- nodeFs.renameSync(p.sealedTmp, p.sealed);
172
+ atomicFile.renameWithRetry(p.sealedTmp, p.sealed);
173
173
  atomicFile.fsyncDir(opts.dataDir);
174
174
 
175
175
  // Step 4: delete plaintext (unless keepPlaintext)
@@ -220,7 +220,7 @@ async function unseal(opts) {
220
220
  }
221
221
 
222
222
  // Step 3: atomic rename plaintext.tmp → plaintext
223
- nodeFs.renameSync(p.plaintextTmp, p.plaintext);
223
+ atomicFile.renameWithRetry(p.plaintextTmp, p.plaintext);
224
224
  atomicFile.fsyncDir(opts.dataDir);
225
225
 
226
226
  // Step 4: delete sealed file
@@ -293,7 +293,7 @@ async function rotate(opts) {
293
293
  }
294
294
 
295
295
  // Step 3: atomic rename — swap in the new sealed file
296
- nodeFs.renameSync(p.sealedTmp, p.sealed);
296
+ atomicFile.renameWithRetry(p.sealedTmp, p.sealed);
297
297
  atomicFile.fsyncDir(opts.dataDir);
298
298
 
299
299
  return { sealedPath: p.sealed };
@@ -316,6 +316,14 @@ function create(opts) {
316
316
  _validateOpts(opts);
317
317
 
318
318
  var root = nodePath.resolve(opts.root);
319
+ // Canonicalize to the real long path. On Windows a path with an 8.3
320
+ // short-name component (os.tmpdir() commonly resolves to C:\Users\RUNNER~1\…)
321
+ // makes the native recursive backend (ReadDirectoryChangesW) deliver
322
+ // long-name event paths that no longer prefix-match the watched root, which
323
+ // trips a libuv fs-event assertion and aborts the process on some Node builds.
324
+ // realpathSync.native expands short names and resolves symlinks; guarded so a
325
+ // non-existent root still falls through to the watcher's own not-found error.
326
+ try { root = nodeFs.realpathSync.native(root); } catch (_e) { /* keep resolved path */ }
319
327
  var debounceMs = (opts.debounceMs !== undefined) ? opts.debounceMs : DEFAULT_DEBOUNCE_MS;
320
328
  var maxPending = (opts.maxPending !== undefined) ? opts.maxPending : DEFAULT_MAX_PENDING;
321
329
  var requestedMode = opts.mode || "fs";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.7",
3
+ "version": "0.15.9",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -54,7 +54,7 @@
54
54
  "owns-its-stack"
55
55
  ],
56
56
  "engines": {
57
- "node": ">=24.14.1"
57
+ "node": ">=24.16.0"
58
58
  },
59
59
  "files": [
60
60
  "index.js",
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.15.8",
4
+ "date": "2026-06-13",
5
+ "headline": "Redacts OTLP log-sink attributes to close a secret/PII egress the span fix missed, adds EU DSA and China PIPL cross-border compliance record-builders, ships an SSDF producer self-attestation with every release, and makes the published tarball reproducible",
6
+ "summary": "This release closes a telemetry egress hole, adds two compliance record-builder namespaces, and hardens the release supply chain. The OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's meta attributes and the resource attributes to the collector unredacted — a log line carrying a bearer token, password, or API key reached the wire verbatim (CWE-532). The 0.15.4 fix wired the telemetry redactor into the span and metric exporters but the log sinks were missed; both now run record and resource attributes through the same redactor before serialization. New b.dsa builds the EU Digital Services Act (Reg 2022/2065) records an intermediary or platform must keep — Art. 16 notice-and-action, Art. 17 statement of reasons, and the Art. 15 / 24(3) transparency report. New b.pipl builds the China PIPL cross-border transfer records — an Art. 38/40 assessment that determines whether a CAC security assessment is mandatory (CIIO, important data, or the volume / sensitive-PI thresholds), and an Art. 40 security-assessment certificate. On the supply-chain side, every release now ships ssdf-attestation.json, a machine-readable NIST SP 800-218 / OMB M-22-18 producer self-attestation mapping each secure-development practice to its implementing control, and the published tarball is now packed with SOURCE_DATE_EPOCH so an operator can rebuild it byte-for-byte from the release commit.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "b.dsa — EU Digital Services Act compliance record-builders",
13
+ "body": "`b.dsa` builds the dated, frozen records the EU Digital Services Act (Regulation (EU) 2022/2065) requires an online intermediary or platform to keep: `b.dsa.noticeAndAction` (Art. 16) records a notice against a piece of content and computes the action-due window; `b.dsa.statementOfReasons` (Art. 17) records a moderation decision with its legal or contractual ground (exactly one is required), the facts, whether it was automated, and the redress routes offered; `b.dsa.transparencyReport` (Art. 15 / 24(3)) aggregates the period counts into a report with the next-due date. The builders perform no network I/O and emit a best-effort audit event; they map to the `dsa` compliance posture."
14
+ },
15
+ {
16
+ "title": "b.pipl — China PIPL cross-border transfer record-builders",
17
+ "body": "`b.pipl.sccFilingAssessment` builds a PIPL Art. 38/40/55 cross-border transfer assessment and determines the lawful mechanism: it forces a CAC security assessment (over a self-selected standard contract or certification) when the exporter is a critical-information-infrastructure operator, exports important data, handles personal information of more than 1,000,000 individuals, or crosses the cumulative volume / sensitive-PI thresholds. `b.pipl.securityAssessmentCertificate` records an Art. 40 security-assessment self-declaration with a 3-year validity clock. Both return frozen dated records and map to the `pipl-cn` posture."
18
+ },
19
+ {
20
+ "title": "SSDF producer self-attestation shipped with every release",
21
+ "body": "Every release now attaches `ssdf-attestation.json` — a machine-readable NIST SP 800-218 (SSDF v1.1) / OMB M-22-18 producer self-attestation. It maps each secure-software-development practice to its implementing control in the tree (SLSA L3 provenance, SSH-signed tags, vendored zero-runtime-dep supply chain, OSV-Scanner gating, coordinated disclosure) and is deterministic from the release commit. Its sha256 is a subject of the SLSA L3 provenance, so verifying the provenance verifies the attestation has not been tampered with. Downstream consumers who require SSDF supplier-compliance evidence can download it from the release page."
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "heading": "Security",
27
+ "items": [
28
+ {
29
+ "title": "OTLP log sinks redact record and resource attributes before export",
30
+ "body": "`b.logStream`'s OTLP log sinks (HTTP-JSON and gRPC) shipped a log record's `meta` attributes and the sink's resource attributes to the collector UNREDACTED, so a log line whose meta carried a bearer token, password, or API key — or a credential placed in a resource attribute — reached the OTLP wire verbatim (CWE-532). The 0.15.4 change baked the telemetry redactor into the span and metric exporters but its detector was anchored on the span/metric encoder function names, leaving the log sinks uncovered. Both log sinks now run record and resource attributes through `b.observability.redactAttrs` before serialization, the same egress contract the span and metric exporters already hold."
31
+ },
32
+ {
33
+ "title": "Reproducible published tarball (SOURCE_DATE_EPOCH)",
34
+ "body": "The release workflow now exports `SOURCE_DATE_EPOCH` (derived from the tagged commit's author date) before `npm pack`, so the mtime stamped into every tar header is deterministic. An operator can re-pack the package from the same commit and match the published tarball's sha256 byte-for-byte, strengthening the source-to-artifact verification path alongside the existing SLSA L3 provenance and PQC signatures."
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "heading": "Detectors",
40
+ "items": [
41
+ {
42
+ "title": "otlp-log-sink-encodes-attrs-without-redactor",
43
+ "body": "Fires when an OTLP log-sink encoder hands a raw `record.meta` or resource-attribute map to serialization without routing it through `observability.redactAttrs` — the class the span/metric detector could not see because the log sinks carry the OTLP-logs schema function names."
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.15.9",
4
+ "date": "2026-06-13",
5
+ "headline": "Raises the Node floor to 24.16, adds SQLite parse-time resource caps, retries Windows rename locks on every atomic write and download, and ships a one-call secure logout that wipes client-side state",
6
+ "summary": "This release moves the engines floor to the current Node 24 LTS patch level and adds three hardening primitives. node:sqlite handles now construct with SQLITE_LIMIT_* caps: a statement over 1 MiB is rejected at parse time (a DoS floor on the raw-SQL surface, complementary to the existing row-count gate) and ATTACH DATABASE is denied. Every final temp-to-destination rename — the file written by an atomic write, a downloaded file, a sealed vault key, a rotated log, an extracted archive entry — now routes through a single retry that rides out a transient Windows lock (antivirus, the search indexer, or a file-sync client briefly holding the destination), instead of surfacing the lock as a hard failure; the retry, previously hand-rolled and unreachable, is now the reusable b.atomicFile.renameWithRetry. And b.session.logout destroys a session and tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data header and expires the session cookie before destroying the row, the secure-default logout that previously had to be assembled by hand.",
7
+ "sections": [
8
+ {
9
+ "heading": "Changed",
10
+ "items": [
11
+ {
12
+ "title": "Node engines floor raised to >=24.16.0",
13
+ "body": "The minimum supported Node is now 24.16.0 (the current Node 24 LTS patch level), up from 24.14.1. This is an LTS-currency bump — there are no Node CVE fixes between 24.14.1 and 24.16.0 (24.14.1 already carried the CVE-2026-21713 HMAC fix); it keeps the framework on the latest patched LTS and makes the node:sqlite resource-cap hardening below available everywhere. Pre-1.0, operators upgrade across the floor; Node 26 continues to satisfy it."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Security",
19
+ "items": [
20
+ {
21
+ "title": "SQLite parse-time statement-size cap",
22
+ "body": "Every node:sqlite database the framework opens — the main db handle and the CLI's handle — now constructs with a SQLITE_LIMIT_LENGTH cap: a SQL statement over 1 MiB is rejected at parse time. Because the query builder parameterizes every value, the size cap guards the raw-SQL surface (`b.db.runSql`) against an attacker-influenced megaquery the parser would otherwise process (SQLite's default is 1 GB); it is a parse-time DoS floor complementary to the existing row-count gate. Legitimate framework and operator statements are far under the cap."
23
+ },
24
+ {
25
+ "title": "Windows rename-lock retry on every atomic rename and download",
26
+ "body": "On Windows a freshly-written file's destination is briefly held by antivirus, the search indexer, or a file-sync client (Dropbox, OneDrive), surfacing as a transient EPERM / EACCES / EBUSY on rename even though the temp file is fine. `b.atomicFile.writeSync` already retried this, but `b.httpClient.downloadStream` did not — a download into a cloud-synced or AV-scanned directory could fail hard on the lock. The retry is now the reusable `b.atomicFile.renameWithRetry`, and every final temp-to-destination rename in the framework routes through it: downloads, sealed vault keys, CA key/cert writes, log rotation, archive extraction, config-drift sidecars, the self-update binary swap, and restore/rollback moves. A non-transient error still throws immediately; POSIX renames are unaffected."
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ "heading": "Added",
32
+ "items": [
33
+ {
34
+ "title": "b.session.logout — one-call secure logout",
35
+ "body": "`b.session.logout(res, token, opts?)` destroys the server-side session AND tells the browser to wipe its client-side state in one call: it emits an RFC 9527 Clear-Site-Data response header (cookies + storage + cache + execution contexts by default) and expires the session cookie, then destroys the session row. `b.session.destroy` alone is a store operation with no response object, so it could not wipe the browser's cached pages, storage, or a stale tab still holding the now-revoked cookie — that wiring previously had to be mounted by hand. Pass `cookieName` to match a non-default cookie and `types` to choose the Clear-Site-Data directives."
36
+ }
37
+ ]
38
+ },
39
+ {
40
+ "heading": "Fixed",
41
+ "items": [
42
+ {
43
+ "title": "b.watcher canonicalizes its root on Windows",
44
+ "body": "`b.watcher.create` now resolves its `root` to the real long path before watching. On Windows a root with an 8.3 short-name component (the system temp directory commonly resolves to one) made the native recursive backend deliver long-name event paths that no longer prefix-matched the watched root, which could abort the process under a strict libuv fs-event assertion. The watcher now canonicalizes the root (expanding short names and resolving symlinks), so events match the watched directory on Windows."
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "heading": "Detectors",
50
+ "items": [
51
+ {
52
+ "title": "Rename-retry, SQLite-limits, and Clear-Site-Data guards",
53
+ "body": "Three recurrence detectors ship with the fixes: a bare `nodeFs.renameSync` final rename that doesn't route through `atomicFile.renameWithRetry`; a main `DatabaseSync` handle constructed without the SQLITE_LIMIT_LENGTH `limits`; and a hand-rolled Clear-Site-Data header value that skips the shared RFC 9527 builder."
54
+ }
55
+ ]
56
+ }
57
+ ]
58
+ }
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ // Emit a NIST SP 800-218 (SSDF) / OMB M-22-18 producer self-attestation
3
+ // as a machine-readable JSON artifact, attached to each GitHub release.
4
+ //
5
+ // Run via:
6
+ // node scripts/generate-ssdf-attestation.js \
7
+ // --version 0.15.7 --commit <sha> --date 2026-06-13T00:00:00Z \
8
+ // > ssdf-attestation.json
9
+ //
10
+ // # or with --out:
11
+ // node scripts/generate-ssdf-attestation.js --out ssdf-attestation.json
12
+ //
13
+ // Wired into .github/workflows/npm-publish.yml alongside the SBOM +
14
+ // cosign + SLSA steps. Downstream consumers who require SSDF supplier-
15
+ // compliance evidence (OMB M-22-18 / M-23-16 self-attestation) download
16
+ // this from the release page.
17
+ //
18
+ // WHAT THIS IS — AND IS NOT.
19
+ // This is a PRODUCER SELF-ATTESTATION, the machine-readable companion
20
+ // to the CISA / OMB "Secure Software Development Attestation Form". It
21
+ // is the producer's own assertion that the SSDF practices below are in
22
+ // force, mapped to the framework's REAL implementing controls. It is
23
+ // NOT a third-party audit, NOT a FedRAMP authorization, and NOT a CMVP
24
+ // validation. Its trust derives entirely from the release boundary that
25
+ // carries it: the SSH-signed tag, the SLSA L3 npm provenance, the
26
+ // Sigstore-keyless SBOM signatures, and the ML-DSA-65 release-signing
27
+ // sidecar — the same four trust roots documented in SECURITY.md sign
28
+ // this file by signing the release that contains it. A consumer who
29
+ // verifies those roots is verifying that THIS attestation came from the
30
+ // producer of record; the claims inside are the producer's assertions,
31
+ // verifiable against the cited controls in the source tree at the
32
+ // release commit.
33
+ //
34
+ // Each statement carries its NIST SSDF practice IDs (PO/PS/PW/RV.*) and
35
+ // names the specific implementing control so the assertion is auditable,
36
+ // not aspirational. Output is deterministic: the timestamp comes from
37
+ // --date / SOURCE_DATE_EPOCH (never an unseeded clock), and the same
38
+ // inputs produce byte-identical output.
39
+
40
+ var fs = require("node:fs");
41
+ var path = require("node:path");
42
+
43
+ var ROOT = path.resolve(__dirname, "..");
44
+ var PKG_PATH = path.join(ROOT, "package.json");
45
+
46
+ // ---------------------------------------------------------------------
47
+ // Argument + environment resolution. Config-time inputs THROW on bad
48
+ // shape (operator catches a typo at invocation), per the three-tier
49
+ // validation discipline — this is an entry-point script, not a hot path.
50
+ // ---------------------------------------------------------------------
51
+
52
+ function _parseArgs(argv) {
53
+ var out = {};
54
+ for (var i = 0; i < argv.length; i++) {
55
+ var a = argv[i];
56
+ if (a === "--out") { out.out = argv[++i]; continue; }
57
+ if (a === "--version") { out.version = argv[++i]; continue; }
58
+ if (a === "--commit") { out.commit = argv[++i]; continue; }
59
+ if (a === "--date") { out.date = argv[++i]; continue; }
60
+ if (a === "--repository") { out.repository = argv[++i]; continue; }
61
+ if (a === "-h" || a === "--help") { out.help = true; continue; }
62
+ throw new Error("generate-ssdf-attestation: unrecognized argument: " + a);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ // Deterministic timestamp resolution: --date (RFC 3339) wins; else
68
+ // SOURCE_DATE_EPOCH (seconds since the Unix epoch, the reproducible-build
69
+ // convention) is parsed; else fail closed. We NEVER call an unseeded
70
+ // clock — a reproducible artifact must produce identical bytes from
71
+ // identical inputs.
72
+ function _resolveTimestamp(args, env) {
73
+ if (typeof args.date === "string" && args.date.length > 0) {
74
+ var t = new Date(args.date);
75
+ if (isNaN(t.getTime())) {
76
+ throw new TypeError("generate-ssdf-attestation: --date is not a valid date: " + args.date);
77
+ }
78
+ return t.toISOString();
79
+ }
80
+ var epoch = env.SOURCE_DATE_EPOCH;
81
+ if (typeof epoch === "string" && epoch.length > 0) {
82
+ if (!/^[0-9]+$/.test(epoch)) {
83
+ throw new TypeError("generate-ssdf-attestation: SOURCE_DATE_EPOCH must be integer seconds, got: " + epoch);
84
+ }
85
+ var secs = parseInt(epoch, 10);
86
+ if (!isFinite(secs) || secs < 0) {
87
+ throw new TypeError("generate-ssdf-attestation: SOURCE_DATE_EPOCH out of range: " + epoch);
88
+ }
89
+ return new Date(secs * 1000).toISOString();
90
+ }
91
+ throw new Error(
92
+ "generate-ssdf-attestation: no deterministic timestamp source. " +
93
+ "Pass --date <RFC3339> or set SOURCE_DATE_EPOCH (no unseeded clock is used)."
94
+ );
95
+ }
96
+
97
+ // Software version: --version wins; else package.json. Cross-check when
98
+ // both are present so a tag/package drift fails the cut here too.
99
+ function _resolveVersion(args, pkg) {
100
+ if (typeof args.version === "string" && args.version.length > 0) {
101
+ if (pkg.version && args.version !== pkg.version) {
102
+ throw new Error(
103
+ "generate-ssdf-attestation: --version (" + args.version +
104
+ ") does not match package.json version (" + pkg.version + ")"
105
+ );
106
+ }
107
+ return args.version;
108
+ }
109
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
110
+ throw new Error("generate-ssdf-attestation: no version (pass --version or set package.json version)");
111
+ }
112
+
113
+ // Source-control commit: --commit wins; else GITHUB_SHA; else null
114
+ // (a locally-generated attestation that omits the commit is honest about
115
+ // not knowing it rather than fabricating one).
116
+ function _resolveCommit(args, env) {
117
+ if (typeof args.commit === "string" && args.commit.length > 0) return args.commit;
118
+ var sha = env.GITHUB_SHA;
119
+ if (typeof sha === "string" && sha.length > 0) return sha;
120
+ return null;
121
+ }
122
+
123
+ // Normalize the package.json repository field to a bare https URL.
124
+ function _resolveRepository(args, pkg) {
125
+ if (typeof args.repository === "string" && args.repository.length > 0) return args.repository;
126
+ var r = pkg.repository;
127
+ var url = (r && typeof r === "object" && typeof r.url === "string") ? r.url
128
+ : (typeof r === "string" ? r : "");
129
+ url = url.replace(/^git\+/, "").replace(/\.git$/, "");
130
+ if (url.length === 0) return null;
131
+ return url;
132
+ }
133
+
134
+ // ---------------------------------------------------------------------
135
+ // The attestation document.
136
+ //
137
+ // Structure follows the OMB M-22-18 / CISA "Secure Software Development
138
+ // Attestation Form": producer identity, software identity, then the four
139
+ // attestation-statement groups the Form covers. Each statement is mapped
140
+ // to its NIST SP 800-218 v1.1 practice ID(s) and the framework control
141
+ // that implements it, so the assertion is checkable against the source
142
+ // tree at `commit`.
143
+ //
144
+ // SSDF practice families:
145
+ // PO Prepare the Organization
146
+ // PS Protect the Software
147
+ // PW Produce Well-Secured Software
148
+ // RV Respond to Vulnerabilities
149
+ // ---------------------------------------------------------------------
150
+
151
+ // The four M-22-18 Form attestation groups (the questions a producer
152
+ // answers "yes" to on the Form), each backed by SSDF-practice-mapped
153
+ // statements naming the framework's real implementing control.
154
+ function _attestationStatements() {
155
+ return [
156
+ {
157
+ "id": "secure-build-environment",
158
+ "form_section": "1. Secure software development environment",
159
+ "summary": "Software is developed and built in secure environments with separated, least-privilege, ephemeral CI.",
160
+ "statements": [
161
+ {
162
+ "ssdf": ["PO.5.1", "PO.5.2"],
163
+ "claim": "Builds run only in GitHub-hosted ephemeral runners; every release job declares the minimum permissions it needs and elevates per-job (workflow-level contents:read; id-token:write only where OIDC signing requires it).",
164
+ "control": ".github/workflows/npm-publish.yml job-level permissions blocks; no self-hosted runners.",
165
+ },
166
+ {
167
+ "ssdf": ["PO.3.1", "PO.3.2", "PS.1.1"],
168
+ "claim": "The build environment's integrity is established by SLSA Build L3 provenance: a non-falsifiable attestation binds the published artifact to the exact workflow run, commit, and tag that produced it.",
169
+ "control": "slsa-framework/slsa-github-generator generator_generic_slsa3.yml@v2.1.0 emits blamejs-<version>.intoto.jsonl; npm publish --provenance attaches the SLSA v1 provenance to the registry tarball.",
170
+ },
171
+ {
172
+ "ssdf": ["PO.5.1"],
173
+ "claim": "Third-party GitHub Actions are SHA-pinned (the one tag-pinned exception, the SLSA reusable workflow, is required by its builder-fetch and is documented + detector-allowlisted); a currency gate fails the release if any pin falls behind upstream.",
174
+ "control": ".github/workflows/*.yml SHA pins; scripts/check-actions-currency.js runs on every PR and in the release flow.",
175
+ },
176
+ ],
177
+ },
178
+ {
179
+ "id": "provenance-and-component-trust",
180
+ "form_section": "2. Provenance and trust of software components",
181
+ "summary": "The provenance of code and components is established and maintained; a complete SBOM accompanies every release.",
182
+ "statements": [
183
+ {
184
+ "ssdf": ["PW.4.1", "PW.4.4"],
185
+ "claim": "Zero npm runtime dependencies. Every third-party library is vendored under lib/vendor/ and pinned by SHA-256 in MANIFEST.json; the release refuses to publish if any runtime dependency component appears in the SBOM.",
186
+ "control": "lib/vendor/MANIFEST.json (per-artifact SHA-256 + version + license + source); npm-publish.yml runtime-deps gate; b.configDrift.verifyVendorIntegrity re-checks each artifact's SHA-256 at boot.",
187
+ },
188
+ {
189
+ "ssdf": ["PS.3.1", "PS.3.2"],
190
+ "claim": "Each release ships a complete CycloneDX 1.6 SBOM (npm-tree view + vendored-bundle view with per-file SHA-256 and purl) so consumers can inventory exactly what ships inside the tarball.",
191
+ "control": "sbom.cdx.json (npm tree) + sbom.vendored.cdx.json (scripts/build-vendored-sbom.js); both attached to the GitHub release.",
192
+ },
193
+ {
194
+ "ssdf": ["PS.2.1"],
195
+ "claim": "Release integrity is verifiable through four independent trust roots, each detecting tampering with the others: SLSA L3 npm provenance, Sigstore-keyless SBOM signatures, SSH-signed annotated tags, and an ML-DSA-65 (FIPS 204) release-signing sidecar over the tarball.",
196
+ "control": "cosign sign-blob (sbom.*.sigstore); SSH-signed tags enforced server-side by the release-tags ruleset; <tarball>.mldsa.sig via the framework's vendored ML-DSA-65 primitive. Verification recipes in SECURITY.md.",
197
+ },
198
+ ],
199
+ },
200
+ {
201
+ "id": "trusted-source-and-vuln-checking",
202
+ "form_section": "3. Trusted source-code supply chains and automated vulnerability checking",
203
+ "summary": "Good-faith effort to maintain trusted source-code supply chains and to perform automated vulnerability scanning on every release.",
204
+ "statements": [
205
+ {
206
+ "ssdf": ["RV.1.1", "RV.1.2", "PW.7.2"],
207
+ "claim": "Every release is scanned for known vulnerabilities before publish: OSV-Scanner runs against both SBOMs and the vendored tree, and the release fails on any finding. A vendored-dependency currency gate refuses a stale, potentially-vulnerable pin.",
208
+ "control": "OSV-Scanner step in npm-publish.yml (--sbom both + -r lib/vendor/); scripts/check-vendor-currency.js.",
209
+ },
210
+ {
211
+ "ssdf": ["PW.8.2", "PW.7.1"],
212
+ "claim": "Adversarial-input parsers are continuously fuzzed (coverage-guided libFuzzer via jazzer.js) and the pattern-catalog gate refuses any new parser primitive that lands without a matching fuzz harness.",
213
+ "control": "fuzz/*.fuzz.js harnesses; ClusterFuzzLite per-PR + daily batch; coverage gate in test/layer-0-primitives/codebase-patterns.test.js.",
214
+ },
215
+ {
216
+ "ssdf": ["PS.1.1", "PO.5.2"],
217
+ "claim": "Source-side supply-chain integrity is enforced at the repository boundary: protected default branch (no force-push, no non-linear merge, required status checks, required signed commits) and protected release tags (no deletion, no re-pointing) so a published tag cannot be silently rewritten.",
218
+ "control": "main-protection + release-tags GitHub rulesets; the sha-to-tag-verify workflow refuses a tag whose commit is not on main's first-parent PR-merged history.",
219
+ },
220
+ ],
221
+ },
222
+ {
223
+ "id": "vulnerability-disclosure-and-response",
224
+ "form_section": "4. Vulnerability disclosure and response",
225
+ "summary": "A vulnerability disclosure program and a process to respond to and remediate reported vulnerabilities are maintained.",
226
+ "statements": [
227
+ {
228
+ "ssdf": ["RV.1.3", "RV.2.1"],
229
+ "claim": "A coordinated vulnerability-disclosure process is published: a private reporting channel, an encryption option for sensitive reports, and committed first-response / triage / fix-release windows by severity.",
230
+ "control": "SECURITY.md (security@blamejs.com, maintainer PGP key, response-time table); GitHub private security advisories.",
231
+ },
232
+ {
233
+ "ssdf": ["RV.2.2", "RV.3.3"],
234
+ "claim": "Fixes are delivered through a stable, signed release path with a public LTS / deprecation policy; remediations are described in operator-facing release notes and the CHANGELOG drawn from a single structured source.",
235
+ "control": "scripts/release.js orchestrated flow; CHANGELOG.md + release-notes/<version>.json single source; LTS-CALENDAR.md.",
236
+ },
237
+ {
238
+ "ssdf": ["RV.3.1", "RV.3.4"],
239
+ "claim": "Root-cause analysis is institutional: a confirmed defect class is swept framework-wide and encoded as a recurrence detector so the same class cannot silently reappear in a later release.",
240
+ "control": "codebase-patterns class-level detectors (test/layer-0-primitives/codebase-patterns.test.js); behavioral regression tests ship with each fix.",
241
+ },
242
+ ],
243
+ },
244
+ ];
245
+ }
246
+
247
+ function buildAttestation(opts) {
248
+ var pkg = opts.pkg;
249
+ return {
250
+ "$schema_note": "NIST SP 800-218 (SSDF v1.1) / OMB M-22-18 producer self-attestation, machine-readable form.",
251
+ "attestation_type": "producer-self-attestation",
252
+ "attestation_format": "blamejs/ssdf-attestation",
253
+ "attestation_format_version": "1.0",
254
+ "framework": {
255
+ "name": "NIST SP 800-218",
256
+ "title": "Secure Software Development Framework (SSDF) Version 1.1",
257
+ "reference_form": "OMB M-22-18 / CISA Secure Software Development Attestation Form",
258
+ },
259
+ "generated": opts.timestamp,
260
+ "producer": {
261
+ // The producer of record. This is a self-attestation: the producer
262
+ // asserts the statements below, signed implicitly by the release
263
+ // boundary (SSH-signed tag + SLSA provenance + PQC sidecar).
264
+ "name": "blamejs",
265
+ "url": "https://blamejs.com/",
266
+ "security_contact": "security@blamejs.com",
267
+ "vulnerability_disclosure": "https://github.com/blamejs/blamejs/security",
268
+ },
269
+ "software": {
270
+ "name": pkg.name || "@blamejs/core",
271
+ "version": opts.version,
272
+ "repository": opts.repository,
273
+ "commit": opts.commit,
274
+ "license": pkg.license || null,
275
+ },
276
+ "attestation_statement":
277
+ "blamejs attests, as the producer of " + (pkg.name || "@blamejs/core") +
278
+ " version " + opts.version + ", that the secure software development " +
279
+ "practices enumerated below are followed for this release, mapped to " +
280
+ "NIST SP 800-218 (SSDF v1.1) practices. This is a self-attestation; " +
281
+ "its authenticity is bound to the signed release artifacts described " +
282
+ "in SECURITY.md (SSH-signed tag, SLSA L3 provenance, Sigstore SBOM " +
283
+ "signatures, ML-DSA-65 release-signing sidecar).",
284
+ "sections": _attestationStatements(),
285
+ "verification": {
286
+ "note": "This attestation is not independently signed; it is covered by the four release trust roots that sign the release containing it.",
287
+ "trust_roots": [
288
+ "SLSA L3 npm provenance (npm publish --provenance + blamejs-<version>.intoto.jsonl)",
289
+ "Sigstore-keyless SBOM signatures (sbom.cdx.json.sigstore, sbom.vendored.cdx.json.sigstore)",
290
+ "SSH-signed annotated git tag (release-tags ruleset, enforced server-side)",
291
+ "ML-DSA-65 release-signing sidecar (<tarball>.mldsa.sig, FIPS 204)",
292
+ ],
293
+ "recipes": "SECURITY.md -> 'Verifying release authenticity'",
294
+ },
295
+ };
296
+ }
297
+
298
+ function main() {
299
+ var args = _parseArgs(process.argv.slice(2));
300
+
301
+ if (args.help) {
302
+ process.stderr.write(
303
+ "Usage: node scripts/generate-ssdf-attestation.js " +
304
+ "[--version <v>] [--commit <sha>] [--date <RFC3339>] " +
305
+ "[--repository <url>] [--out <path>]\n" +
306
+ "Timestamp source (required, deterministic): --date or SOURCE_DATE_EPOCH.\n"
307
+ );
308
+ return;
309
+ }
310
+
311
+ var pkg;
312
+ try {
313
+ pkg = JSON.parse(fs.readFileSync(PKG_PATH, "utf8"));
314
+ } catch (e) {
315
+ process.stderr.write("[generate-ssdf-attestation] failed to read package.json: " + e.message + "\n");
316
+ process.exit(1);
317
+ return;
318
+ }
319
+
320
+ var doc = buildAttestation({
321
+ pkg: pkg,
322
+ version: _resolveVersion(args, pkg),
323
+ commit: _resolveCommit(args, process.env), // allow:raw-process-env — env-driven release script
324
+ repository: _resolveRepository(args, pkg),
325
+ timestamp: _resolveTimestamp(args, process.env), // allow:raw-process-env — env-driven release script
326
+ });
327
+
328
+ var json = JSON.stringify(doc, null, 2) + "\n";
329
+
330
+ if (typeof args.out === "string" && args.out.length > 0) {
331
+ fs.writeFileSync(args.out, json);
332
+ process.stderr.write("[generate-ssdf-attestation] wrote " + args.out + "\n");
333
+ } else {
334
+ process.stdout.write(json);
335
+ }
336
+ }
337
+
338
+ main();
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ // b.atomicFile.renameWithRetry — the bounded retry on a Windows-transient
3
+ // destination lock (EPERM/EACCES/EBUSY from AV / search indexer / Dropbox /
4
+ // OneDrive briefly holding the target). #146: httpClient.downloadStream and
5
+ // every other final temp->dest rename route through this instead of a bare
6
+ // nodeFs.renameSync, so a transient lock is retried, not surfaced as a hard
7
+ // failure. A non-transient error (ENOENT, etc.) still throws immediately.
8
+
9
+ var nodeFs = require("node:fs");
10
+ var os = require("node:os");
11
+ var path = require("node:path");
12
+ var helpers = require("../helpers");
13
+ var check = helpers.check;
14
+ var atomicFile = require("../../lib/atomic-file");
15
+
16
+ function run() {
17
+ check("renameWithRetry is exported", typeof atomicFile.renameWithRetry === "function");
18
+
19
+ var dir = nodeFs.mkdtempSync(path.join(os.tmpdir(), "renameretry-"));
20
+ var realRename = nodeFs.renameSync;
21
+
22
+ // ---- transient EPERM is retried, then the rename succeeds ----
23
+ var src1 = path.join(dir, "a.tmp");
24
+ var dst1 = path.join(dir, "a.dst");
25
+ nodeFs.writeFileSync(src1, "payload");
26
+ var calls = 0;
27
+ nodeFs.renameSync = function (from, to) {
28
+ calls += 1;
29
+ if (calls < 3) { var e = new Error("transient lock"); e.code = "EPERM"; throw e; }
30
+ return realRename(from, to);
31
+ };
32
+ try { atomicFile.renameWithRetry(src1, dst1); }
33
+ finally { nodeFs.renameSync = realRename; }
34
+ check("renameWithRetry retries past a transient EPERM (3 attempts)", calls === 3);
35
+ check("renameWithRetry: the rename ultimately succeeds",
36
+ nodeFs.existsSync(dst1) && !nodeFs.existsSync(src1));
37
+
38
+ // ---- a non-transient error throws immediately, with NO retry ----
39
+ var attempts = 0;
40
+ nodeFs.renameSync = function () {
41
+ attempts += 1;
42
+ var e = new Error("no such file"); e.code = "ENOENT"; throw e;
43
+ };
44
+ var threw = null;
45
+ try { atomicFile.renameWithRetry(path.join(dir, "missing"), path.join(dir, "x")); }
46
+ catch (e) { threw = e; }
47
+ finally { nodeFs.renameSync = realRename; }
48
+ check("renameWithRetry rethrows a non-transient error", threw !== null && threw.code === "ENOENT");
49
+ check("renameWithRetry does NOT retry a non-transient error", attempts === 1);
50
+
51
+ // ---- a persistently-transient lock eventually gives up (does not hang) ----
52
+ var stuck = 0;
53
+ nodeFs.renameSync = function () {
54
+ stuck += 1;
55
+ var e = new Error("still locked"); e.code = "EBUSY"; throw e;
56
+ };
57
+ var stuckThrew = null;
58
+ try { atomicFile.renameWithRetry(path.join(dir, "s"), path.join(dir, "d")); }
59
+ catch (e) { stuckThrew = e; }
60
+ finally { nodeFs.renameSync = realRename; }
61
+ check("renameWithRetry gives up after the bounded attempts (no infinite loop)",
62
+ stuckThrew !== null && stuckThrew.code === "EBUSY" && stuck === 5);
63
+ }
64
+
65
+ module.exports = { run: run };
66
+
67
+ if (require.main === module) {
68
+ try { run(); console.log("[atomic-file-rename-retry] OK"); }
69
+ catch (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
70
+ }