@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/vendor/MANIFEST.json +72 -52
  5. package/lib/vendor/blamejs/.github/workflows/ci.yml +12 -12
  6. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +37 -5
  7. package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
  8. package/lib/vendor/blamejs/CHANGELOG.md +6 -0
  9. package/lib/vendor/blamejs/MIGRATING.md +12 -0
  10. package/lib/vendor/blamejs/README.md +5 -2
  11. package/lib/vendor/blamejs/SECURITY.md +4 -2
  12. package/lib/vendor/blamejs/api-snapshot.json +137 -2
  13. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
  14. package/lib/vendor/blamejs/index.js +4 -0
  15. package/lib/vendor/blamejs/lib/archive-read.js +2 -1
  16. package/lib/vendor/blamejs/lib/archive-tar-read.js +2 -1
  17. package/lib/vendor/blamejs/lib/atomic-file.js +5 -0
  18. package/lib/vendor/blamejs/lib/audit.js +2 -0
  19. package/lib/vendor/blamejs/lib/auth/elevation-grant.js +6 -2
  20. package/lib/vendor/blamejs/lib/auth/oauth.js +13 -0
  21. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +5 -2
  22. package/lib/vendor/blamejs/lib/cli.js +8 -1
  23. package/lib/vendor/blamejs/lib/compliance.js +4 -0
  24. package/lib/vendor/blamejs/lib/config-drift.js +2 -1
  25. package/lib/vendor/blamejs/lib/credential-hash.js +9 -0
  26. package/lib/vendor/blamejs/lib/db.js +15 -2
  27. package/lib/vendor/blamejs/lib/dsa.js +482 -0
  28. package/lib/vendor/blamejs/lib/framework-error.js +14 -0
  29. package/lib/vendor/blamejs/lib/http-client.js +5 -2
  30. package/lib/vendor/blamejs/lib/local-db-thin.js +3 -2
  31. package/lib/vendor/blamejs/lib/log-stream-local.js +1 -1
  32. package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +9 -2
  33. package/lib/vendor/blamejs/lib/log-stream-otlp.js +16 -7
  34. package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +36 -11
  35. package/lib/vendor/blamejs/lib/mtls-ca.js +2 -2
  36. package/lib/vendor/blamejs/lib/observability.js +3 -2
  37. package/lib/vendor/blamejs/lib/pipl-cn.js +377 -0
  38. package/lib/vendor/blamejs/lib/restore-rollback.js +5 -5
  39. package/lib/vendor/blamejs/lib/retention.js +16 -2
  40. package/lib/vendor/blamejs/lib/scheduler.js +12 -0
  41. package/lib/vendor/blamejs/lib/self-update.js +1 -1
  42. package/lib/vendor/blamejs/lib/session.js +64 -0
  43. package/lib/vendor/blamejs/lib/ssrf-guard.js +25 -7
  44. package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -3
  45. package/lib/vendor/blamejs/lib/watcher.js +8 -0
  46. package/lib/vendor/blamejs/package.json +2 -2
  47. package/lib/vendor/blamejs/release-notes/v0.15.7.json +43 -0
  48. package/lib/vendor/blamejs/release-notes/v0.15.8.json +48 -0
  49. package/lib/vendor/blamejs/release-notes/v0.15.9.json +58 -0
  50. package/lib/vendor/blamejs/scripts/gen-migrating.js +16 -0
  51. package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +338 -0
  52. package/lib/vendor/blamejs/test/00-primitives.js +51 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-rename-retry.test.js +70 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -3
  55. package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +18 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/db-init-extensions.test.js +32 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/dsa.test.js +169 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +40 -1
  59. package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +172 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/retention-floor.test.js +59 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +64 -11
  62. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-watchdog-stale-settle.test.js +71 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +57 -0
  64. package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +7 -3
  65. package/package.json +2 -2
@@ -2000,9 +2000,12 @@ async function downloadStream(opts) {
2000
2000
  }
2001
2001
  }
2002
2002
 
2003
- // Atomic rename + dir fsync.
2003
+ // Atomic rename + dir fsync. Route the final rename through
2004
+ // atomicFile.renameWithRetry so a Windows-transient lock on the destination
2005
+ // (AV / search indexer / Dropbox / OneDrive) is retried rather than surfaced
2006
+ // as a hard download failure — the same retry b.atomicFile.writeSync applies.
2004
2007
  try {
2005
- nodeFs.renameSync(tmpPath, dest);
2008
+ atomicFile.renameWithRetry(tmpPath, dest);
2006
2009
  atomicFile.fsyncDir(dir);
2007
2010
  } catch (e) {
2008
2011
  try { nodeFs.unlinkSync(tmpPath); } catch (_u) { /* best-effort cleanup */ }
@@ -59,6 +59,7 @@ var lazyRequire = require("./lazy-require");
59
59
  var validateOpts = require("./validate-opts");
60
60
  var safeSql = require("./safe-sql");
61
61
  var { LocalDbThinError } = require("./framework-error");
62
+ var atomicFile = require("./atomic-file");
62
63
 
63
64
  var audit = lazyRequire(function () { return require("./audit"); });
64
65
 
@@ -203,7 +204,7 @@ function thin(opts) {
203
204
  var lastRenameErr = null;
204
205
  for (var attempt = 0; attempt < 20 && !renamed; attempt += 1) {
205
206
  try {
206
- if (nodeFs.existsSync(file)) nodeFs.renameSync(file, renamedTo);
207
+ if (nodeFs.existsSync(file)) atomicFile.renameWithRetry(file, renamedTo);
207
208
  renamed = true;
208
209
  } catch (re) {
209
210
  lastRenameErr = re;
@@ -228,7 +229,7 @@ function thin(opts) {
228
229
  ["-wal", "-shm"].forEach(function (suffix) {
229
230
  var sibling = file + suffix;
230
231
  if (nodeFs.existsSync(sibling)) {
231
- try { nodeFs.renameSync(sibling, sibling + ".corrupt-" + stamp); }
232
+ try { atomicFile.renameWithRetry(sibling, sibling + ".corrupt-" + stamp); }
232
233
  catch (_se) { /* best-effort */ }
233
234
  }
234
235
  });
@@ -85,7 +85,7 @@ function create(config) {
85
85
  var stamp = time.toIso8601NoMs(new Date()).replace(/[-:]/g, "");
86
86
  var rotated = nodePath.join(dir, cfg.fileNamePrefix + "-" + stamp + ".log");
87
87
  if (nodeFs.existsSync(activePath)) {
88
- nodeFs.renameSync(activePath, rotated);
88
+ atomicFile.renameWithRetry(activePath, rotated);
89
89
  if (cfg.compressRotations) {
90
90
  var data = nodeFs.readFileSync(rotated);
91
91
  var gz = zlib.gzipSync(data);
@@ -33,6 +33,11 @@ var safeAsync = require("./safe-async");
33
33
  var safeUrl = require("./safe-url");
34
34
  var { tearDownH2Session } = require("./http2-teardown");
35
35
  var { LogStreamError } = require("./framework-error");
36
+ var lazyRequire = require("./lazy-require");
37
+ // Lazy to break the observability <-> log-stream require cycle. Used only to
38
+ // scrub attribute values through the telemetry redactor before they cross the
39
+ // OTLP egress boundary (CWE-532).
40
+ var observability = lazyRequire(function () { return require("./observability"); });
36
41
 
37
42
  var _err = LogStreamError.factory;
38
43
  var _log = boot("log-stream-otlp-grpc");
@@ -133,7 +138,9 @@ function _encodeLogRecord(record) {
133
138
  // operators emitting > year-2255 timestamps. For ms-resolution
134
139
  // records the LSB nanos are 0; we still send fixed64.
135
140
  var tsNs = BigInt(tsMs) * 1000000n;
136
- var attrPieces = _encodeAttributes(record.meta).map(function (kvBody) {
141
+ // Scrub meta values through the telemetry redactor before the wire (CWE-532),
142
+ // matching the span/metric exporters' egress contract.
143
+ var attrPieces = _encodeAttributes(observability().redactAttrs(record.meta)).map(function (kvBody) {
137
144
  return pb.embeddedMessage(6, kvBody);
138
145
  });
139
146
  var msg = (record.message != null ? String(record.message) : "");
@@ -162,7 +169,7 @@ function _encodeScopeLogs(records, scopeName, scopeVersion) {
162
169
  // ResourceLogs (logs.proto): resource=1 (Resource),
163
170
  // scope_logs=2 (repeated ScopeLogs), schema_url=3
164
171
  function _encodeResourceLogs(records, cfg) {
165
- var resourceBody = _encodeResource(_resourceAttrs(cfg));
172
+ var resourceBody = _encodeResource(observability().redactAttrs(_resourceAttrs(cfg)));
166
173
  var scopeLogsBody = _encodeScopeLogs(records, cfg.scopeName, cfg.scopeVersion);
167
174
  return Buffer.concat([
168
175
  pb.embeddedMessage(1, resourceBody),
@@ -58,6 +58,11 @@ var httpClient = require("./http-client");
58
58
  var safeAsync = require("./safe-async");
59
59
  var safeUrl = require("./safe-url");
60
60
  var authHeader = require("./auth-header");
61
+ var lazyRequire = require("./lazy-require");
62
+ // Lazy to break the observability <-> log-stream require cycle (observability's
63
+ // log path can reach a log-stream sink). Used only to scrub attribute values
64
+ // through the telemetry redactor before they cross the OTLP egress boundary.
65
+ var observability = lazyRequire(function () { return require("./observability"); });
61
66
 
62
67
  var MAX_RESPONSE_BYTES = C.BYTES.mib(1);
63
68
  var FRAMEWORK_VERSION = (pkg && pkg.version) || "unknown";
@@ -141,7 +146,10 @@ function _toLogRecord(record) {
141
146
  var sev = SEVERITY[record.level] || SEVERITY.info;
142
147
  // OTel timeUnixNano is a string (JSON can't safely represent 64-bit ints).
143
148
  var nanos = String(BigInt(record.ts) * 1000000n);
144
- var attrs = record.meta ? _encodeAttrs(record.meta) : [];
149
+ // Telemetry is a first-class EGRESS sink: scrub every meta value through the
150
+ // redactor before it reaches the collector wire (CWE-532), the same contract
151
+ // the span/metric exporters hold.
152
+ var attrs = record.meta ? _encodeAttrs(observability().redactAttrs(record.meta)) : [];
145
153
  return {
146
154
  timeUnixNano: nanos,
147
155
  observedTimeUnixNano: nanos,
@@ -158,7 +166,7 @@ function _serializeBatch(records, cfg, scopeVersion) {
158
166
  resourceLogs: [
159
167
  {
160
168
  resource: {
161
- attributes: _encodeAttrs(resourceAttrs),
169
+ attributes: _encodeAttrs(observability().redactAttrs(resourceAttrs)),
162
170
  },
163
171
  scopeLogs: [
164
172
  {
@@ -291,10 +299,11 @@ function create(config) {
291
299
  }
292
300
 
293
301
  module.exports = {
294
- create: create,
302
+ create: create,
295
303
  // Exposed for tests + advanced operator wiring.
296
- _resolveUrl: _resolveUrl,
297
- _encodeAttrs: _encodeAttrs,
298
- _toLogRecord: _toLogRecord,
299
- SEVERITY: SEVERITY,
304
+ _resolveUrl: _resolveUrl,
305
+ _encodeAttrs: _encodeAttrs,
306
+ _toLogRecord: _toLogRecord,
307
+ _serializeBatch: _serializeBatch,
308
+ SEVERITY: SEVERITY,
300
309
  };
@@ -87,29 +87,51 @@ var DEFAULT_TYPES = ["cookies", "storage", "cache", "executionContexts"];
87
87
  * },
88
88
  * ]);
89
89
  */
90
- function create(opts) {
91
- opts = opts || {};
92
- validateOpts(opts, ["types"], "middleware.clearSiteData");
93
- var types = opts.types === undefined ? DEFAULT_TYPES : opts.types;
90
+ /**
91
+ * @primitive b.middleware.clearSiteData.headerValue
92
+ * @signature b.middleware.clearSiteData.headerValue(types, label?)
93
+ * @since 0.15.9
94
+ * @status stable
95
+ * @related b.middleware.clearSiteData, b.session.logout
96
+ *
97
+ * Build the RFC 9527 §3 Clear-Site-Data header value from a list of directive
98
+ * types — a comma-separated list of double-quoted tokens — validating each
99
+ * against the known set (`cookies`, `storage`, `cache`, `executionContexts`).
100
+ * The middleware factory and `b.session.logout` both compose it so every
101
+ * emitter produces the same validated header instead of hand-rolling the
102
+ * quoting. Throws a `TypeError` on an unknown directive or a non-array input
103
+ * (config-time / entry-point tier).
104
+ *
105
+ * @example
106
+ * b.middleware.clearSiteData.headerValue(["cookies", "storage"]);
107
+ * // → '"cookies", "storage"'
108
+ */
109
+ function headerValue(types, label) {
110
+ label = label || "middleware.clearSiteData";
94
111
  if (!Array.isArray(types) || types.length === 0) {
95
- throw new TypeError("middleware.clearSiteData: opts.types must be a non-empty array");
112
+ throw new TypeError(label + ": types must be a non-empty array");
96
113
  }
97
114
  for (var i = 0; i < types.length; i += 1) {
98
115
  var t = types[i];
99
116
  if (typeof t !== "string" || !KNOWN_TYPES[t]) {
100
117
  throw new TypeError(
101
- "middleware.clearSiteData: unknown type '" + t +
118
+ label + ": unknown type '" + t +
102
119
  "' (expected one of: " + Object.keys(KNOWN_TYPES).join(", ") + ")");
103
120
  }
104
121
  }
105
- // Header value is a comma-separated list of double-quoted tokens
106
- // per RFC 9527 §3 (Structured Field Value List of Strings). Build
107
- // once at construction time — runtime cost is one setHeader call.
108
- var headerValue = types.map(function (t) { return '"' + t + '"'; }).join(", ");
122
+ return types.map(function (t) { return '"' + t + '"'; }).join(", ");
123
+ }
124
+
125
+ function create(opts) {
126
+ opts = opts || {};
127
+ validateOpts(opts, ["types"], "middleware.clearSiteData");
128
+ var types = opts.types === undefined ? DEFAULT_TYPES : opts.types;
129
+ // Header value built once at construction; runtime cost is one setHeader.
130
+ var headerVal = headerValue(types, "middleware.clearSiteData");
109
131
 
110
132
  return function clearSiteData(req, res, next) {
111
133
  if (typeof res.setHeader === "function") {
112
- res.setHeader("Clear-Site-Data", headerValue);
134
+ res.setHeader("Clear-Site-Data", headerVal);
113
135
  }
114
136
  next();
115
137
  };
@@ -117,6 +139,9 @@ function create(opts) {
117
139
 
118
140
  module.exports = {
119
141
  create: create,
142
+ // The shared RFC 9527 header-value builder — b.session.logout composes it so
143
+ // the logout path emits the same validated Clear-Site-Data header.
144
+ headerValue: headerValue,
120
145
  KNOWN_TYPES: Object.keys(KNOWN_TYPES),
121
146
  DEFAULT_TYPES: DEFAULT_TYPES,
122
147
  };
@@ -336,8 +336,8 @@ function create(opts) {
336
336
  _writeExclusive(keyTmp, opts2.caKeyPem, 0o600);
337
337
  }
338
338
  _writeExclusive(certTmp, opts2.caCertPem, 0o644);
339
- nodeFs.renameSync(keyTmp, keyDest);
340
- nodeFs.renameSync(certTmp, paths.caCert);
339
+ atomicFile.renameWithRetry(keyTmp, keyDest);
340
+ atomicFile.renameWithRetry(certTmp, paths.caCert);
341
341
  } catch (e) {
342
342
  // Best-effort cleanup of half-written tmp files; the original
343
343
  // commit error is what we re-raise. Log cleanup failures at debug
@@ -199,8 +199,9 @@ function getRedactor() {
199
199
  *
200
200
  * Run every value of a telemetry attribute map through the active redactor and
201
201
  * return a NEW `{ key: redactedValue }` object. The OTLP exporters call this on
202
- * span, span-event, metric, and resource attributes before serialization so no
203
- * attribute value crosses the egress boundary unscrubbed (CWE-532: insertion of
202
+ * span, span-event, metric, log-record, and resource attributes before
203
+ * serialization so no attribute value crosses the egress boundary unscrubbed
204
+ * (the HTTP-JSON and gRPC log sinks included) (CWE-532: insertion of
204
205
  * sensitive information into an externally-shipped sink). A key whose redactor
205
206
  * throws is DROPPED — failing toward dropping, never exporting the raw value;
206
207
  * `null` / `undefined` values pass through for the type-encoder to handle.
@@ -0,0 +1,377 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.pipl
4
+ * @nav Compliance
5
+ * @title PIPL (China)
6
+ *
7
+ * @intro
8
+ * China PIPL (Personal Information Protection Law) cross-border
9
+ * transfer record-builders. PIPL Art. 38 sets three lawful bases for
10
+ * transferring personal information outside the PRC: a CAC security
11
+ * assessment (Art. 40), the CAC standard contract (SCC), or
12
+ * certification by a CAC-accredited body. The CAC security assessment
13
+ * is MANDATORY — the operator may not self-select the standard
14
+ * contract — when the exporter is a critical-information-infrastructure
15
+ * operator (CIIO), handles "important data", or crosses the volume /
16
+ * sensitive-PI thresholds in the Measures for Security Assessment of
17
+ * Outbound Data Transfers.
18
+ *
19
+ * These primitives follow the operator-feeds-metadata pattern: the
20
+ * operator supplies the transfer's facts and the builder returns a
21
+ * frozen, dated record (plus a best-effort audit event) that composes
22
+ * into the operator's own retention / export sink. They perform NO
23
+ * network I/O and do NOT file anything with the CAC — they document
24
+ * the legal basis the operator must be able to produce on inspection.
25
+ *
26
+ * @card
27
+ * China PIPL cross-border transfer records — Art. 38/40/55 SCC + CAC security-assessment basis (sccFilingAssessment, securityAssessmentCertificate).
28
+ */
29
+
30
+ var lazyRequire = require("./lazy-require");
31
+ var validateOpts = require("./validate-opts");
32
+ var C = require("./constants");
33
+ var { PiplError } = require("./framework-error");
34
+
35
+ var audit = lazyRequire(function () { return require("./audit"); });
36
+
37
+ // PIPL Art. 38(1) lawful cross-border mechanisms. A standard contract or
38
+ // certification is a self-selectable basis; a security assessment is the
39
+ // mechanism the CAC Measures impose when a mandatory trigger is present.
40
+ var LEGAL_BASES = Object.freeze(["standard-contract", "security-assessment", "certification"]);
41
+
42
+ // Mandatory-security-assessment thresholds from the CAC 2024 Provisions on
43
+ // Promoting and Regulating Cross-Border Data Flows (Art. 7/8), which relaxed
44
+ // the original 2022 Measures. Crossing ANY of these forces the
45
+ // security-assessment mechanism — the operator cannot fall back to the
46
+ // standard contract or certification.
47
+ // - CIIO exporter, or "important data" in scope: always mandatory.
48
+ // - cumulative outbound NON-sensitive PI of MORE THAN 1,000,000 individuals
49
+ // since 1 January of the current year (the 100,000–1,000,000 band is the
50
+ // standard-contract / certification tier, NOT a security-assessment
51
+ // trigger).
52
+ // - cumulative outbound SENSITIVE PI of MORE THAN 10,000 individuals in that
53
+ // window.
54
+ // The thresholds are CUMULATIVE since 1 January and THIS transfer counts toward
55
+ // them — the transfer's own `volume` is sorted into the sensitive or
56
+ // non-sensitive bucket by `sensitivePI` and added to the running cumulative
57
+ // before the comparison.
58
+ var SECURITY_ASSESSMENT_NONSENSITIVE_PI_THRESHOLD = 1000000;
59
+ var SECURITY_ASSESSMENT_SENSITIVE_PI_THRESHOLD = 10000;
60
+
61
+ // Re-assessment / re-filing cadence. The CAC security assessment result
62
+ // is valid for 3 years (Measures Art. 14); the standard-contract +
63
+ // certification bases carry a PIPIA (Art. 55) that should be refreshed
64
+ // at least annually or on any material change. We stamp the longer 3-year
65
+ // clock for a mandated security assessment and a 1-year clock otherwise.
66
+ var SECURITY_ASSESSMENT_VALIDITY_DAYS = 365 * 3;
67
+ var STANDARD_REVIEW_DAYS = 365;
68
+
69
+ var SCC_ASSESSMENT_ALLOWED_KEYS = [
70
+ "assessmentId", "transferType", "recipientJurisdiction", "dataCategories",
71
+ "legalBasis", "volume", "sensitivePI", "ciio", "importantData",
72
+ "cumulativePI", "cumulativeSensitivePI", "recordedAt", "audit",
73
+ ];
74
+
75
+ var RISK_RATINGS = Object.freeze(["low", "medium", "high"]);
76
+
77
+ var SECURITY_CERT_ALLOWED_KEYS = [
78
+ "certId", "assessmentScope", "dataExporter", "overseasRecipient",
79
+ "riskRating", "safeguards", "filingRef", "recordedAt", "audit",
80
+ ];
81
+
82
+ // Resolve the audit sink: an operator-supplied b.audit-shaped object wins
83
+ // (so the call is captured even without a DB-backed global handler);
84
+ // otherwise fall back to the framework's global audit. Validated for shape
85
+ // at the call site so a malformed sink throws rather than silently no-ops.
86
+ function _resolveAudit(optsAudit, label) {
87
+ if (optsAudit === undefined || optsAudit === null) return audit();
88
+ return validateOpts.auditShape(optsAudit, label, PiplError, "pipl/bad-audit");
89
+ }
90
+
91
+ function _requireRecordedAt(value, label) {
92
+ if (typeof value !== "number" || !isFinite(value) || value <= 0) {
93
+ throw new PiplError("pipl/bad-recorded-at",
94
+ label + " must be a positive epoch-ms number");
95
+ }
96
+ return value;
97
+ }
98
+
99
+ /**
100
+ * @primitive b.pipl.sccFilingAssessment
101
+ * @signature b.pipl.sccFilingAssessment(opts)
102
+ * @since 0.15.8
103
+ * @status stable
104
+ * @compliance pipl-cn
105
+ * @related b.pipl.securityAssessmentCertificate, b.compliance.isCrossBorderRegulated, b.privacy.vendorReview
106
+ *
107
+ * Build a dated PIPL Art. 38 / Art. 55 cross-border transfer assessment
108
+ * and determine the lawful mechanism the transfer requires. PIPL Art. 38(1)
109
+ * permits three bases for moving personal information out of the PRC — the
110
+ * CAC standard contract (SCC), a CAC security assessment (Art. 40), or
111
+ * certification by a CAC-accredited body — but the Measures for Security
112
+ * Assessment of Outbound Data Transfers make the security assessment
113
+ * MANDATORY (the operator may NOT self-select the standard contract or
114
+ * certification) when the exporter is a critical-information-infrastructure
115
+ * operator (CIIO), exports "important data", handles personal information
116
+ * of more than 1,000,000 individuals, or has cumulatively exported PI of
117
+ * more than 100,000 individuals or sensitive PI of more than 10,000
118
+ * individuals since 1 January of the preceding year.
119
+ *
120
+ * The builder validates the operator-supplied facts, computes
121
+ * `securityAssessmentRequired` against those thresholds, resolves the
122
+ * `mechanismRequired` (forcing `security-assessment` when any trigger is
123
+ * present, otherwise honoring the operator's declared `legalBasis`), and
124
+ * stamps `recordedAt` plus a `nextReviewDueBy` re-assessment clock (3 years
125
+ * for a mandated security assessment per Measures Art. 14, otherwise the
126
+ * annual PIPIA refresh under Art. 55). The returned record is frozen and
127
+ * is NOT framework-persisted — compose it into your retention / audit /
128
+ * export sink. A best-effort `pipl.transfer.assessed` audit event fires.
129
+ *
130
+ * @opts
131
+ * assessmentId: string, // required — operator's identifier for this assessment
132
+ * transferType: string, // required — e.g. "intra-group", "processor", "controller-to-controller"
133
+ * recipientJurisdiction: string, // required — destination jurisdiction (e.g. "US", "EU", "SG")
134
+ * dataCategories: string[], // required — non-empty list of PI categories transferred
135
+ * legalBasis: string, // required — "standard-contract" | "security-assessment" | "certification"
136
+ * volume: number, // required — count of data subjects in this transfer (>= 0)
137
+ * sensitivePI: boolean, // required — whether the transfer includes sensitive PI (Art. 28)
138
+ * ciio: boolean, // optional — exporter is a CIIO (forces security assessment); default false
139
+ * importantData: boolean, // optional — transfer includes "important data" (forces it); default false
140
+ * cumulativePI: number, // optional — cumulative PI subjects exported since 1 Jan prior year; default 0
141
+ * cumulativeSensitivePI: number, // optional — cumulative sensitive-PI subjects exported in that window; default 0
142
+ * recordedAt: number, // required — epoch ms of this assessment
143
+ * audit: object, // optional — b.audit-shaped sink; default global b.audit
144
+ *
145
+ * @example
146
+ * var rec = b.pipl.sccFilingAssessment({
147
+ * assessmentId: "xfer-2026-001",
148
+ * transferType: "processor",
149
+ * recipientJurisdiction: "US",
150
+ * dataCategories: ["contact", "billing"],
151
+ * legalBasis: "standard-contract",
152
+ * volume: 5000,
153
+ * sensitivePI: false,
154
+ * recordedAt: Date.now(),
155
+ * });
156
+ * // → { assessmentId, mechanismRequired: "standard-contract",
157
+ * // securityAssessmentRequired: false, recordedAt, nextReviewDueBy, ... }
158
+ */
159
+ function sccFilingAssessment(opts) {
160
+ validateOpts.requireObject(opts, "b.pipl.sccFilingAssessment: opts", PiplError, "pipl/bad-opts");
161
+ validateOpts(opts, SCC_ASSESSMENT_ALLOWED_KEYS, "b.pipl.sccFilingAssessment");
162
+ validateOpts.requireNonEmptyString(opts.assessmentId,
163
+ "b.pipl.sccFilingAssessment: opts.assessmentId", PiplError, "pipl/bad-assessment-id");
164
+ validateOpts.requireNonEmptyString(opts.transferType,
165
+ "b.pipl.sccFilingAssessment: opts.transferType", PiplError, "pipl/bad-transfer-type");
166
+ validateOpts.requireNonEmptyString(opts.recipientJurisdiction,
167
+ "b.pipl.sccFilingAssessment: opts.recipientJurisdiction", PiplError, "pipl/bad-recipient");
168
+
169
+ if (!Array.isArray(opts.dataCategories) || opts.dataCategories.length === 0) {
170
+ throw new PiplError("pipl/bad-data-categories",
171
+ "b.pipl.sccFilingAssessment: opts.dataCategories must be a non-empty array of strings");
172
+ }
173
+ validateOpts.optionalNonEmptyStringArray(opts.dataCategories,
174
+ "b.pipl.sccFilingAssessment: opts.dataCategories", PiplError, "pipl/bad-data-categories");
175
+
176
+ if (LEGAL_BASES.indexOf(opts.legalBasis) === -1) {
177
+ throw new PiplError("pipl/bad-legal-basis",
178
+ "b.pipl.sccFilingAssessment: opts.legalBasis must be one of " +
179
+ LEGAL_BASES.join(" | ") + " (PIPL Art. 38(1)) — got " + JSON.stringify(opts.legalBasis));
180
+ }
181
+
182
+ if (typeof opts.volume !== "number" || !isFinite(opts.volume) || opts.volume < 0) {
183
+ throw new PiplError("pipl/bad-volume",
184
+ "b.pipl.sccFilingAssessment: opts.volume must be a non-negative finite number (data-subject count)");
185
+ }
186
+ if (typeof opts.sensitivePI !== "boolean") {
187
+ throw new PiplError("pipl/bad-sensitive-pi",
188
+ "b.pipl.sccFilingAssessment: opts.sensitivePI must be a boolean");
189
+ }
190
+
191
+ var ciio = opts.ciio === undefined ? false
192
+ : validateOpts.optionalBoolean(opts.ciio, "b.pipl.sccFilingAssessment: opts.ciio", PiplError, "pipl/bad-ciio");
193
+ var importantData = opts.importantData === undefined ? false
194
+ : validateOpts.optionalBoolean(opts.importantData, "b.pipl.sccFilingAssessment: opts.importantData", PiplError, "pipl/bad-important-data");
195
+ var cumulativePI = opts.cumulativePI === undefined ? 0
196
+ : validateOpts.optionalFiniteNonNegative(opts.cumulativePI, "b.pipl.sccFilingAssessment: opts.cumulativePI", PiplError, "pipl/bad-cumulative-pi");
197
+ var cumulativeSensitivePI = opts.cumulativeSensitivePI === undefined ? 0
198
+ : validateOpts.optionalFiniteNonNegative(opts.cumulativeSensitivePI, "b.pipl.sccFilingAssessment: opts.cumulativeSensitivePI", PiplError, "pipl/bad-cumulative-sensitive-pi");
199
+
200
+ var recordedAt = _requireRecordedAt(opts.recordedAt, "b.pipl.sccFilingAssessment: opts.recordedAt");
201
+ // Resolve + shape-validate the audit sink at the entry-point tier (THROWS
202
+ // on a malformed sink) — NOT inside the drop-silent emission try/catch
203
+ // below, which would swallow the config error.
204
+ var auditSink = _resolveAudit(opts.audit, "b.pipl.sccFilingAssessment: opts.audit");
205
+
206
+ // Mandatory-security-assessment determination (CAC 2024 Provisions, Art. 7/8).
207
+ // Crossing ANY trigger forces the security-assessment mechanism regardless of
208
+ // the operator's declared legalBasis — the operator cannot self-downgrade to
209
+ // the standard contract or certification once a trigger is present. The
210
+ // thresholds are cumulative since 1 January and THIS transfer counts: sort its
211
+ // own volume into the sensitive or non-sensitive bucket and add it to the
212
+ // running cumulative before comparing, so a first/planned transfer that alone
213
+ // crosses a threshold is classified correctly without the caller having to
214
+ // pre-add it to the cumulative field.
215
+ var effectiveSensitivePI = cumulativeSensitivePI + (opts.sensitivePI ? opts.volume : 0);
216
+ var effectiveNonSensitivePI = cumulativePI + (opts.sensitivePI ? 0 : opts.volume);
217
+ var triggers = [];
218
+ if (ciio) triggers.push("ciio");
219
+ if (importantData) triggers.push("important-data");
220
+ if (effectiveNonSensitivePI > SECURITY_ASSESSMENT_NONSENSITIVE_PI_THRESHOLD) triggers.push("non-sensitive-pi-volume");
221
+ if (effectiveSensitivePI > SECURITY_ASSESSMENT_SENSITIVE_PI_THRESHOLD) triggers.push("sensitive-pi-volume");
222
+
223
+ var securityAssessmentRequired = triggers.length > 0;
224
+ var mechanismRequired = securityAssessmentRequired ? "security-assessment" : opts.legalBasis;
225
+ var nextReviewDays = securityAssessmentRequired ? SECURITY_ASSESSMENT_VALIDITY_DAYS : STANDARD_REVIEW_DAYS;
226
+
227
+ var record = Object.freeze({
228
+ assessmentId: opts.assessmentId,
229
+ transferType: opts.transferType,
230
+ recipientJurisdiction: opts.recipientJurisdiction,
231
+ dataCategories: Object.freeze(opts.dataCategories.slice()),
232
+ legalBasis: opts.legalBasis,
233
+ volume: opts.volume,
234
+ sensitivePI: opts.sensitivePI,
235
+ mechanismRequired: mechanismRequired,
236
+ securityAssessmentRequired: securityAssessmentRequired,
237
+ securityAssessmentTriggers: Object.freeze(triggers),
238
+ legalReference: "PIPL Art. 38 / Art. 40 / Art. 55",
239
+ recordedAt: recordedAt,
240
+ nextReviewDueBy: recordedAt + C.TIME.days(nextReviewDays),
241
+ });
242
+
243
+ try {
244
+ auditSink.safeEmit({
245
+ action: "pipl.transfer.assessed",
246
+ outcome: "success",
247
+ resource: { kind: "pipl-cross-border-transfer", id: opts.assessmentId },
248
+ metadata: {
249
+ transferType: opts.transferType,
250
+ recipientJurisdiction: opts.recipientJurisdiction,
251
+ mechanismRequired: mechanismRequired,
252
+ securityAssessmentRequired: securityAssessmentRequired,
253
+ triggers: triggers,
254
+ recordedAt: recordedAt,
255
+ },
256
+ });
257
+ } catch (_e) { /* drop-silent — audit is best-effort, never block the builder */ }
258
+
259
+ return record;
260
+ }
261
+
262
+ /**
263
+ * @primitive b.pipl.securityAssessmentCertificate
264
+ * @signature b.pipl.securityAssessmentCertificate(opts)
265
+ * @since 0.15.8
266
+ * @status stable
267
+ * @compliance pipl-cn
268
+ * @related b.pipl.sccFilingAssessment, b.compliance.isCrossBorderRegulated
269
+ *
270
+ * Record a dated PIPL Art. 40 / CAC security-assessment self-declaration
271
+ * for an outbound data transfer. PIPL Art. 40 and the Measures for
272
+ * Security Assessment of Outbound Data Transfers require an operator who
273
+ * must pass (or has passed) the CAC security assessment to document the
274
+ * assessment scope, the data exporter, the overseas recipient, a risk
275
+ * rating, and the safeguards relied on — the evidence the operator must be
276
+ * able to produce on CAC inspection. This builder validates the supplied
277
+ * facts and returns a frozen, dated certificate record stamped with a
278
+ * 3-year `validUntil` clock (the CAC security-assessment result validity
279
+ * period, Measures Art. 14). It performs NO network I/O and files nothing
280
+ * with the CAC — it documents the assessment the operator conducted. A
281
+ * best-effort `pipl.security_assessment.recorded` audit event fires.
282
+ *
283
+ * @opts
284
+ * certId: string, // required — operator's identifier for this certificate
285
+ * assessmentScope: string, // required — scope of the security assessment (systems / data flows covered)
286
+ * dataExporter: string, // required — the PRC data exporter (controller / processor)
287
+ * overseasRecipient: string, // required — the overseas recipient receiving the PI
288
+ * riskRating: string, // required — "low" | "medium" | "high"
289
+ * safeguards: string[], // required — non-empty list of safeguards relied on (encryption, DPA, etc.)
290
+ * filingRef: string, // optional — CAC filing / acceptance reference number
291
+ * recordedAt: number, // required — epoch ms of this declaration
292
+ * audit: object, // optional — b.audit-shaped sink; default global b.audit
293
+ *
294
+ * @example
295
+ * var cert = b.pipl.securityAssessmentCertificate({
296
+ * certId: "sa-2026-014",
297
+ * assessmentScope: "CRM outbound replication to US region",
298
+ * dataExporter: "Acme (Shanghai) Co., Ltd.",
299
+ * overseasRecipient: "Acme Inc. (Delaware)",
300
+ * riskRating: "medium",
301
+ * safeguards: ["XChaCha20 at rest", "standard contractual clauses", "data minimization"],
302
+ * recordedAt: Date.now(),
303
+ * });
304
+ * // → { certId, assessmentScope, riskRating, recordedAt, validUntil }
305
+ */
306
+ function securityAssessmentCertificate(opts) {
307
+ validateOpts.requireObject(opts, "b.pipl.securityAssessmentCertificate: opts", PiplError, "pipl/bad-opts");
308
+ validateOpts(opts, SECURITY_CERT_ALLOWED_KEYS, "b.pipl.securityAssessmentCertificate");
309
+ validateOpts.requireNonEmptyString(opts.certId,
310
+ "b.pipl.securityAssessmentCertificate: opts.certId", PiplError, "pipl/bad-cert-id");
311
+ validateOpts.requireNonEmptyString(opts.assessmentScope,
312
+ "b.pipl.securityAssessmentCertificate: opts.assessmentScope", PiplError, "pipl/bad-scope");
313
+ validateOpts.requireNonEmptyString(opts.dataExporter,
314
+ "b.pipl.securityAssessmentCertificate: opts.dataExporter", PiplError, "pipl/bad-exporter");
315
+ validateOpts.requireNonEmptyString(opts.overseasRecipient,
316
+ "b.pipl.securityAssessmentCertificate: opts.overseasRecipient", PiplError, "pipl/bad-recipient");
317
+
318
+ if (RISK_RATINGS.indexOf(opts.riskRating) === -1) {
319
+ throw new PiplError("pipl/bad-risk-rating",
320
+ "b.pipl.securityAssessmentCertificate: opts.riskRating must be one of " +
321
+ RISK_RATINGS.join(" | ") + " — got " + JSON.stringify(opts.riskRating));
322
+ }
323
+
324
+ if (!Array.isArray(opts.safeguards) || opts.safeguards.length === 0) {
325
+ throw new PiplError("pipl/bad-safeguards",
326
+ "b.pipl.securityAssessmentCertificate: opts.safeguards must be a non-empty array of strings");
327
+ }
328
+ validateOpts.optionalNonEmptyStringArray(opts.safeguards,
329
+ "b.pipl.securityAssessmentCertificate: opts.safeguards", PiplError, "pipl/bad-safeguards");
330
+
331
+ var filingRef = validateOpts.optionalNonEmptyString(opts.filingRef,
332
+ "b.pipl.securityAssessmentCertificate: opts.filingRef", PiplError, "pipl/bad-filing-ref");
333
+
334
+ var recordedAt = _requireRecordedAt(opts.recordedAt, "b.pipl.securityAssessmentCertificate: opts.recordedAt");
335
+ // Entry-point shape-validate the audit sink (THROWS) before the drop-silent
336
+ // emission try/catch below.
337
+ var auditSink = _resolveAudit(opts.audit, "b.pipl.securityAssessmentCertificate: opts.audit");
338
+
339
+ var record = Object.freeze({
340
+ certId: opts.certId,
341
+ assessmentScope: opts.assessmentScope,
342
+ dataExporter: opts.dataExporter,
343
+ overseasRecipient: opts.overseasRecipient,
344
+ riskRating: opts.riskRating,
345
+ safeguards: Object.freeze(opts.safeguards.slice()),
346
+ filingRef: filingRef || null,
347
+ legalReference: "PIPL Art. 40 / CAC Measures for Security Assessment of Outbound Data Transfers",
348
+ recordedAt: recordedAt,
349
+ validUntil: recordedAt + C.TIME.days(SECURITY_ASSESSMENT_VALIDITY_DAYS),
350
+ });
351
+
352
+ try {
353
+ auditSink.safeEmit({
354
+ action: "pipl.security_assessment.recorded",
355
+ outcome: "success",
356
+ resource: { kind: "pipl-security-assessment", id: opts.certId },
357
+ metadata: {
358
+ assessmentScope: opts.assessmentScope,
359
+ dataExporter: opts.dataExporter,
360
+ overseasRecipient: opts.overseasRecipient,
361
+ riskRating: opts.riskRating,
362
+ filingRef: filingRef || null,
363
+ recordedAt: recordedAt,
364
+ },
365
+ });
366
+ } catch (_e) { /* drop-silent — audit is best-effort, never block the builder */ }
367
+
368
+ return record;
369
+ }
370
+
371
+ module.exports = {
372
+ sccFilingAssessment: sccFilingAssessment,
373
+ securityAssessmentCertificate: securityAssessmentCertificate,
374
+ LEGAL_BASES: LEGAL_BASES,
375
+ RISK_RATINGS: RISK_RATINGS,
376
+ PiplError: PiplError,
377
+ };
@@ -119,7 +119,7 @@ function swap(opts) {
119
119
  // Step 1: rename current dataDir → rollback nodePath. Skipped on first
120
120
  // restore (no existing dataDir).
121
121
  if (hadDataDir) {
122
- try { nodeFs.renameSync(opts.dataDir, rollbackPath); }
122
+ try { atomicFile.renameWithRetry(opts.dataDir, rollbackPath); }
123
123
  catch (e) {
124
124
  throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
125
125
  "swap: could not move existing dataDir to rollback: " + ((e && e.message) || String(e)));
@@ -127,11 +127,11 @@ function swap(opts) {
127
127
  }
128
128
 
129
129
  // Step 2: rename staging → dataDir
130
- try { nodeFs.renameSync(opts.stagingDir, opts.dataDir); }
130
+ try { atomicFile.renameWithRetry(opts.stagingDir, opts.dataDir); }
131
131
  catch (e) {
132
132
  // Step 2 failed — try to undo step 1 so the operator's dataDir is back
133
133
  if (hadDataDir) {
134
- try { nodeFs.renameSync(rollbackPath, opts.dataDir); }
134
+ try { atomicFile.renameWithRetry(rollbackPath, opts.dataDir); }
135
135
  catch (_e) { /* dataDir is now in rollbackPath; operator must recover manually */ }
136
136
  }
137
137
  throw new RestoreRollbackError("restore-rollback/rename-staging-failed",
@@ -220,7 +220,7 @@ async function rollback(opts) {
220
220
  atomicFile.ensureDir(rollbackRoot);
221
221
  discardedAt = atomicFile.pathTimestamp();
222
222
  var discardedPath = nodePath.join(rollbackRoot, "discarded-" + discardedAt);
223
- try { nodeFs.renameSync(opts.dataDir, discardedPath); }
223
+ try { atomicFile.renameWithRetry(opts.dataDir, discardedPath); }
224
224
  catch (e) {
225
225
  throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
226
226
  "rollback: could not move current dataDir aside: " + ((e && e.message) || String(e)));
@@ -229,7 +229,7 @@ async function rollback(opts) {
229
229
  }
230
230
 
231
231
  // Rename the rollback dir back into dataDir's place
232
- try { nodeFs.renameSync(opts.rollbackPath, opts.dataDir); }
232
+ try { atomicFile.renameWithRetry(opts.rollbackPath, opts.dataDir); }
233
233
  catch (e) {
234
234
  throw new RestoreRollbackError("restore-rollback/rollback-rename-failed",
235
235
  "rollback: could not move rollback into dataDir: " + ((e && e.message) || String(e)) +