@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
|
@@ -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
|
-
|
|
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:
|
|
302
|
+
create: create,
|
|
295
303
|
// Exposed for tests + advanced operator wiring.
|
|
296
|
-
_resolveUrl:
|
|
297
|
-
_encodeAttrs:
|
|
298
|
-
_toLogRecord:
|
|
299
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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("
|
|
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
|
-
"
|
|
118
|
+
label + ": unknown type '" + t +
|
|
102
119
|
"' (expected one of: " + Object.keys(KNOWN_TYPES).join(", ") + ")");
|
|
103
120
|
}
|
|
104
121
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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",
|
|
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
|
-
|
|
340
|
-
|
|
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
|
|
203
|
-
* attribute value crosses the egress boundary unscrubbed
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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)) +
|
|
@@ -676,7 +676,7 @@ async function swap(opts) {
|
|
|
676
676
|
// don't silently lose both binaries (the prior best-effort comment
|
|
677
677
|
// swallowed the rollback exception — SSDF RV.1 violation).
|
|
678
678
|
try {
|
|
679
|
-
|
|
679
|
+
atomicFile.renameWithRetry(from, to);
|
|
680
680
|
} catch (e) {
|
|
681
681
|
if (e && e.code === "EXDEV") {
|
|
682
682
|
// Cross-device — copy + unlink. Use atomicFile.copy for the safety
|
|
@@ -65,6 +65,9 @@ var { SessionError } = require("./framework-error");
|
|
|
65
65
|
// the cookie-side sid so the wire token is ciphertext rather than
|
|
66
66
|
// plaintext (sealed-cookie default since v0.8.61).
|
|
67
67
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
68
|
+
// Lazy — b.session.logout composes the Clear-Site-Data header builder; keep it
|
|
69
|
+
// out of the boot require graph (no cycle, but session is a low-level primitive).
|
|
70
|
+
var clearSiteData = lazyRequire(function () { return require("./middleware/clear-site-data"); });
|
|
68
71
|
|
|
69
72
|
// Pluggable session-storage backend. Default uses cluster-storage (which
|
|
70
73
|
// in turn dispatches to the framework's main DB or external DB). An
|
|
@@ -705,6 +708,66 @@ async function destroy(token) {
|
|
|
705
708
|
return await _deleteBySidHash(_hashSid(sid));
|
|
706
709
|
}
|
|
707
710
|
|
|
711
|
+
/**
|
|
712
|
+
* @primitive b.session.logout
|
|
713
|
+
* @signature b.session.logout(res, token, opts?)
|
|
714
|
+
* @since 0.15.9
|
|
715
|
+
* @status stable
|
|
716
|
+
* @related b.session.destroy, b.middleware.clearSiteData
|
|
717
|
+
*
|
|
718
|
+
* Secure logout in one call: destroy the server-side session AND tell the
|
|
719
|
+
* browser to wipe its client-side state. It emits an RFC 9527 Clear-Site-Data
|
|
720
|
+
* response header (cookies + storage + cache + executionContexts by default)
|
|
721
|
+
* and expires the session cookie, then destroys the session row. `destroy()`
|
|
722
|
+
* alone is a store operation with no `res`, so it cannot wipe the browser's
|
|
723
|
+
* cached pages / storage / any stale tab still holding the now-revoked cookie;
|
|
724
|
+
* this composes the secure-default logout the middleware otherwise had to be
|
|
725
|
+
* mounted by hand. Returns whether a session was destroyed. Leader-only.
|
|
726
|
+
*
|
|
727
|
+
* @opts
|
|
728
|
+
* cookieName: string, // default: "sid" — the session cookie to expire
|
|
729
|
+
* types: string[], // default: the RFC 9527 Clear-Site-Data directive set
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* app.post("/logout", async function (req, res) {
|
|
733
|
+
* await b.session.logout(res, req.cookies.sid);
|
|
734
|
+
* res.end("logged out");
|
|
735
|
+
* });
|
|
736
|
+
* // → emits Clear-Site-Data + expires the sid cookie + destroys the session
|
|
737
|
+
*/
|
|
738
|
+
async function logout(res, token, opts) {
|
|
739
|
+
if (!res || typeof res.setHeader !== "function") {
|
|
740
|
+
throw new SessionError("session/bad-res",
|
|
741
|
+
"b.session.logout: res must be an HTTP response with setHeader()");
|
|
742
|
+
}
|
|
743
|
+
opts = opts || {};
|
|
744
|
+
var cookieName = opts.cookieName === undefined ? "sid" : opts.cookieName;
|
|
745
|
+
if (typeof cookieName !== "string" || cookieName.length === 0) {
|
|
746
|
+
throw new SessionError("session/bad-cookie-name",
|
|
747
|
+
"b.session.logout: opts.cookieName must be a non-empty string");
|
|
748
|
+
}
|
|
749
|
+
var csd = clearSiteData();
|
|
750
|
+
var types = opts.types === undefined ? csd.DEFAULT_TYPES : opts.types;
|
|
751
|
+
// Build (and validate) the RFC 9527 header BEFORE any side effect — an
|
|
752
|
+
// unknown directive throws here, queuing nothing.
|
|
753
|
+
var clearSiteDataValue = csd.headerValue(types, "b.session.logout");
|
|
754
|
+
|
|
755
|
+
// Revoke the server-side session FIRST. If destroy() throws (a follower
|
|
756
|
+
// failing cluster.requireLeader(), or a store/DB error), no client-wipe
|
|
757
|
+
// headers have been queued — an error response can't then expire the
|
|
758
|
+
// browser cookie + Clear-Site-Data while the session row is still live,
|
|
759
|
+
// which would leave a copied token usable server-side.
|
|
760
|
+
var destroyed = await destroy(token);
|
|
761
|
+
|
|
762
|
+
// Now wipe the client-side state: RFC 9527 Clear-Site-Data (cookies /
|
|
763
|
+
// storage / cache) + expire the session cookie (belt-and-suspenders with the
|
|
764
|
+
// "cookies" directive, and effective even if the client ignores the header).
|
|
765
|
+
res.setHeader("Clear-Site-Data", clearSiteDataValue);
|
|
766
|
+
res.setHeader("Set-Cookie",
|
|
767
|
+
cookieName + "=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0");
|
|
768
|
+
return destroyed;
|
|
769
|
+
}
|
|
770
|
+
|
|
708
771
|
async function _deleteBySidHash(sidHash) {
|
|
709
772
|
var built = sql.delete(_sessionSqlTable(), _sessionSqlOpts())
|
|
710
773
|
.where("sidHash", sidHash)
|
|
@@ -1245,6 +1308,7 @@ module.exports = {
|
|
|
1245
1308
|
create: create,
|
|
1246
1309
|
verify: verify,
|
|
1247
1310
|
destroy: destroy,
|
|
1311
|
+
logout: logout,
|
|
1248
1312
|
destroyAllForUser: destroyAllForUser,
|
|
1249
1313
|
touch: touch,
|
|
1250
1314
|
rotate: rotate,
|