@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
|
@@ -1156,8 +1156,21 @@ async function init(opts) {
|
|
|
1156
1156
|
encKey = null;
|
|
1157
1157
|
}
|
|
1158
1158
|
|
|
1159
|
-
// Open the database
|
|
1160
|
-
|
|
1159
|
+
// Open the database. The node:sqlite `limits` option sets SQLITE_LIMIT_*
|
|
1160
|
+
// caps at construction — a parse-time DoS floor complementary to the
|
|
1161
|
+
// streamLimit row-count gate (one bounds statement size, the other bounds
|
|
1162
|
+
// result cardinality). sqlLength rejects a megaquery (>1 MiB) before the
|
|
1163
|
+
// parser chews CPU/memory on it; the framework never legitimately emits a
|
|
1164
|
+
// statement anywhere near 1 MiB, and a 1 GB attacker-influenced statement
|
|
1165
|
+
// would otherwise be parsed. The limits option is part of node:sqlite from
|
|
1166
|
+
// Node 24.10+, comfortably under the engines floor. (SQLITE_LIMIT_ATTACHED is
|
|
1167
|
+
// left at the SQLite default — the snapshot / backup path relies on the
|
|
1168
|
+
// attach mechanism.)
|
|
1169
|
+
database = new DatabaseSync(dbPath, {
|
|
1170
|
+
limits: {
|
|
1171
|
+
sqlLength: C.BYTES.mib(1),
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1161
1174
|
|
|
1162
1175
|
// Performance pragmas
|
|
1163
1176
|
runSql(database, "PRAGMA journal_mode=WAL");
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.dsa
|
|
4
|
+
* @nav Compliance
|
|
5
|
+
* @title Digital Services Act
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Record-builders for the operator workflows the EU Digital Services
|
|
9
|
+
* Act (Regulation (EU) 2022/2065) requires an online intermediary or
|
|
10
|
+
* platform to keep on file. Three dated, frozen attestation records
|
|
11
|
+
* cover the regulation's core content-governance loop:
|
|
12
|
+
*
|
|
13
|
+
* - `noticeAndAction` (Art. 16) records a notice a third party
|
|
14
|
+
* submits against a piece of content and computes the window
|
|
15
|
+
* inside which the provider must act on it.
|
|
16
|
+
* - `statementOfReasons` (Art. 17) records the moderation decision
|
|
17
|
+
* taken on a piece of content, its legal or contractual ground,
|
|
18
|
+
* the facts relied on, whether it was automated, and the redress
|
|
19
|
+
* routes offered to the affected recipient.
|
|
20
|
+
* - `transparencyReport` (Art. 15 / Art. 24(3)) aggregates the
|
|
21
|
+
* period counts a provider must publish — notices received,
|
|
22
|
+
* actions taken, automated decisions, appeals — into a report
|
|
23
|
+
* record with the next due date.
|
|
24
|
+
*
|
|
25
|
+
* The builders follow the operator-feeds-metadata pattern: the
|
|
26
|
+
* operator supplies the facts and each function returns a frozen,
|
|
27
|
+
* timestamped record that composes into the operator's own retention /
|
|
28
|
+
* audit / export sink. None of them persist to the framework or touch
|
|
29
|
+
* the network. A best-effort `dsa.*` audit event fires when an audit
|
|
30
|
+
* sink is wired. They map to the `dsa` compliance posture, which
|
|
31
|
+
* cascades ML-DSA-87 audit-chain signing and a TLS 1.3 floor.
|
|
32
|
+
*
|
|
33
|
+
* @card
|
|
34
|
+
* EU Digital Services Act (Reg 2022/2065) record-builders — Art. 16 notice-and-action, Art. 17 statement of reasons, Art. 15/24(3) transparency report.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var validateOpts = require("./validate-opts");
|
|
38
|
+
var lazyRequire = require("./lazy-require");
|
|
39
|
+
var C = require("./constants");
|
|
40
|
+
var { DsaError } = require("./framework-error");
|
|
41
|
+
|
|
42
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
43
|
+
|
|
44
|
+
// ---- Art. 16 notice-and-action ----
|
|
45
|
+
|
|
46
|
+
// The notice categories Art. 16(2) expects a notice-and-action
|
|
47
|
+
// mechanism to distinguish. A notice that alleges illegal content
|
|
48
|
+
// (Art. 16(2)(a)-(d)) starts the diligent-and-timely action clock and
|
|
49
|
+
// MUST be answered with an Art. 17 statement of reasons when the
|
|
50
|
+
// provider acts on it; a terms-of-service notice need not be.
|
|
51
|
+
var NOTICE_TYPES = Object.freeze({
|
|
52
|
+
"illegal-content": { statementOfReasonsRequired: true, description: "Notice alleges the content is illegal under Union or member-state law (Art. 16(2))." },
|
|
53
|
+
"terms-violation": { statementOfReasonsRequired: false, description: "Notice alleges the content breaches the provider's terms and conditions." },
|
|
54
|
+
"ip-infringement": { statementOfReasonsRequired: true, description: "Notice alleges intellectual-property infringement (a sub-case of illegal content)." },
|
|
55
|
+
"other": { statementOfReasonsRequired: false, description: "Any other notice category the provider's mechanism accepts." },
|
|
56
|
+
});
|
|
57
|
+
var NOTICE_TYPE_IDS = Object.keys(NOTICE_TYPES);
|
|
58
|
+
|
|
59
|
+
// Who submitted the notice. A trusted flagger (Art. 22) is processed
|
|
60
|
+
// with priority; the field is recorded so the provider can evidence
|
|
61
|
+
// the Art. 22(1) priority-handling obligation.
|
|
62
|
+
var SUBMITTER_TYPES = Object.freeze(["individual", "trusted-flagger", "authority", "rights-holder", "other"]);
|
|
63
|
+
|
|
64
|
+
// Default action window. Art. 16(6) requires action "in a timely,
|
|
65
|
+
// diligent, non-arbitrary and objective manner"; it sets no fixed
|
|
66
|
+
// hour count, so the framework default is a conservative 24h SLA that
|
|
67
|
+
// operators override per their own published policy via actionWindowMs.
|
|
68
|
+
var DEFAULT_ACTION_WINDOW_MS = C.TIME.hours(24);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @primitive b.dsa.noticeAndAction
|
|
72
|
+
* @signature b.dsa.noticeAndAction(opts)
|
|
73
|
+
* @since 0.15.8
|
|
74
|
+
* @status stable
|
|
75
|
+
* @compliance dsa
|
|
76
|
+
* @related b.dsa.statementOfReasons, b.dsa.transparencyReport, b.compliance.describe
|
|
77
|
+
*
|
|
78
|
+
* Record an Art. 16 notice-and-action notice and compute the window
|
|
79
|
+
* inside which the provider must act on it. The operator supplies the
|
|
80
|
+
* notice facts — the content it targets, the alleged category, the
|
|
81
|
+
* substantiating reason, when it was submitted, and who submitted it —
|
|
82
|
+
* and `noticeAndAction` validates the shape, stamps `recordedAt`,
|
|
83
|
+
* derives `actionDueBy` from the submission time plus the action
|
|
84
|
+
* window, and flags whether acting on the notice will require an
|
|
85
|
+
* Art. 17 statement of reasons (true for illegal-content / IP notices).
|
|
86
|
+
* The returned record is frozen and is NOT framework-persisted —
|
|
87
|
+
* compose it into your retention / audit / export sink. A best-effort
|
|
88
|
+
* `dsa.notice.recorded` audit event fires when an audit sink is wired.
|
|
89
|
+
*
|
|
90
|
+
* @opts
|
|
91
|
+
* contentId: string, // required — the content the notice targets
|
|
92
|
+
* noticeType: string, // required — illegal-content | terms-violation | ip-infringement | other
|
|
93
|
+
* reason: string, // required — the notice's substantiation (Art. 16(2)(a))
|
|
94
|
+
* submittedAt: number, // required — epoch ms the notice was submitted
|
|
95
|
+
* submitterType: string, // required — individual | trusted-flagger | authority | rights-holder | other
|
|
96
|
+
* noticeId: string, // optional — operator notice id; defaults to "dsa-notice-<submittedAt>"
|
|
97
|
+
* actionWindowMs: number, // optional — SLA window; default 24h (Art. 16(6) "timely")
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* var n = b.dsa.noticeAndAction({
|
|
101
|
+
* contentId: "post-9931",
|
|
102
|
+
* noticeType: "illegal-content",
|
|
103
|
+
* reason: "Depicts a sale prohibited under national law.",
|
|
104
|
+
* submittedAt: Date.now(),
|
|
105
|
+
* submitterType: "trusted-flagger",
|
|
106
|
+
* });
|
|
107
|
+
* // → { noticeId, contentId, noticeType, status: "recorded",
|
|
108
|
+
* // recordedAt, actionDueBy, statementOfReasonsRequired: true }
|
|
109
|
+
*/
|
|
110
|
+
function noticeAndAction(opts) {
|
|
111
|
+
validateOpts.requireObject(opts, "b.dsa.noticeAndAction: opts", DsaError, "dsa/bad-opts");
|
|
112
|
+
validateOpts(opts, [
|
|
113
|
+
"contentId", "noticeType", "reason", "submittedAt", "submitterType",
|
|
114
|
+
"noticeId", "actionWindowMs",
|
|
115
|
+
], "b.dsa.noticeAndAction");
|
|
116
|
+
validateOpts.requireNonEmptyString(opts.contentId, "b.dsa.noticeAndAction: opts.contentId", DsaError, "dsa/bad-content-id");
|
|
117
|
+
validateOpts.requireNonEmptyString(opts.noticeType, "b.dsa.noticeAndAction: opts.noticeType", DsaError, "dsa/bad-notice-type");
|
|
118
|
+
if (NOTICE_TYPE_IDS.indexOf(opts.noticeType) === -1) {
|
|
119
|
+
throw new DsaError("dsa/unknown-notice-type",
|
|
120
|
+
"b.dsa.noticeAndAction: unknown noticeType '" + opts.noticeType +
|
|
121
|
+
"' (allowed: " + NOTICE_TYPE_IDS.join(", ") + ")");
|
|
122
|
+
}
|
|
123
|
+
validateOpts.requireNonEmptyString(opts.reason, "b.dsa.noticeAndAction: opts.reason", DsaError, "dsa/bad-reason");
|
|
124
|
+
if (typeof opts.submittedAt !== "number" || !isFinite(opts.submittedAt) || opts.submittedAt <= 0) {
|
|
125
|
+
throw new DsaError("dsa/bad-submitted-at",
|
|
126
|
+
"b.dsa.noticeAndAction: opts.submittedAt must be a positive epoch-ms number");
|
|
127
|
+
}
|
|
128
|
+
validateOpts.requireNonEmptyString(opts.submitterType, "b.dsa.noticeAndAction: opts.submitterType", DsaError, "dsa/bad-submitter-type");
|
|
129
|
+
if (SUBMITTER_TYPES.indexOf(opts.submitterType) === -1) {
|
|
130
|
+
throw new DsaError("dsa/unknown-submitter-type",
|
|
131
|
+
"b.dsa.noticeAndAction: unknown submitterType '" + opts.submitterType +
|
|
132
|
+
"' (allowed: " + SUBMITTER_TYPES.join(", ") + ")");
|
|
133
|
+
}
|
|
134
|
+
validateOpts.optionalNonEmptyString(opts.noticeId, "b.dsa.noticeAndAction: opts.noticeId", DsaError, "dsa/bad-notice-id");
|
|
135
|
+
var actionWindowMs = opts.actionWindowMs === undefined
|
|
136
|
+
? DEFAULT_ACTION_WINDOW_MS
|
|
137
|
+
: validateOpts.optionalPositiveFinite(opts.actionWindowMs, "b.dsa.noticeAndAction: opts.actionWindowMs", DsaError, "dsa/bad-action-window");
|
|
138
|
+
|
|
139
|
+
var recordedAt = Date.now();
|
|
140
|
+
var sorRequired = NOTICE_TYPES[opts.noticeType].statementOfReasonsRequired;
|
|
141
|
+
var record = Object.freeze({
|
|
142
|
+
noticeId: opts.noticeId || ("dsa-notice-" + opts.submittedAt),
|
|
143
|
+
contentId: opts.contentId,
|
|
144
|
+
noticeType: opts.noticeType,
|
|
145
|
+
submitterType: opts.submitterType,
|
|
146
|
+
reason: opts.reason,
|
|
147
|
+
submittedAt: opts.submittedAt,
|
|
148
|
+
status: "recorded",
|
|
149
|
+
recordedAt: recordedAt,
|
|
150
|
+
actionDueBy: opts.submittedAt + actionWindowMs,
|
|
151
|
+
statementOfReasonsRequired: sorRequired,
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
audit().safeEmit({
|
|
155
|
+
action: "dsa.notice.recorded",
|
|
156
|
+
outcome: "success",
|
|
157
|
+
metadata: {
|
|
158
|
+
noticeId: record.noticeId,
|
|
159
|
+
contentId: record.contentId,
|
|
160
|
+
noticeType: record.noticeType,
|
|
161
|
+
submitterType: record.submitterType,
|
|
162
|
+
actionDueBy: record.actionDueBy,
|
|
163
|
+
statementOfReasonsRequired: record.statementOfReasonsRequired,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
} catch (_e) { /* drop-silent — audit is best-effort, never block the builder */ }
|
|
167
|
+
return record;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- Art. 17 statement of reasons ----
|
|
171
|
+
|
|
172
|
+
// The moderation decisions Art. 17(1) covers. Each restricts the
|
|
173
|
+
// content or the recipient's account; the statement of reasons must
|
|
174
|
+
// state which (Art. 17(3)(a)).
|
|
175
|
+
var DECISIONS = Object.freeze([
|
|
176
|
+
"content-removed",
|
|
177
|
+
"content-disabled",
|
|
178
|
+
"content-demoted",
|
|
179
|
+
"age-restricted",
|
|
180
|
+
"monetisation-removed",
|
|
181
|
+
"account-suspended",
|
|
182
|
+
"account-terminated",
|
|
183
|
+
"no-action",
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
// The redress routes Art. 17(3)(f) requires the statement to point the
|
|
187
|
+
// recipient to. At least one must be offered for a restrictive
|
|
188
|
+
// decision.
|
|
189
|
+
var REDRESS_OPTIONS = Object.freeze([
|
|
190
|
+
"internal-complaint", // Art. 20 internal complaint-handling system
|
|
191
|
+
"out-of-court-settlement", // Art. 21 out-of-court dispute settlement
|
|
192
|
+
"judicial-redress", // Art. 17(3)(f) — judicial remedy
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @primitive b.dsa.statementOfReasons
|
|
197
|
+
* @signature b.dsa.statementOfReasons(opts)
|
|
198
|
+
* @since 0.15.8
|
|
199
|
+
* @status stable
|
|
200
|
+
* @compliance dsa
|
|
201
|
+
* @related b.dsa.noticeAndAction, b.dsa.transparencyReport, b.compliance.describe
|
|
202
|
+
*
|
|
203
|
+
* Record an Art. 17 statement of reasons for a content-moderation
|
|
204
|
+
* decision. Whenever a provider restricts content (or a recipient's
|
|
205
|
+
* account) it must give the affected recipient a clear, specific
|
|
206
|
+
* statement of reasons; this builder records that statement as a frozen
|
|
207
|
+
* dated record. The operator supplies the decision, the legal ground
|
|
208
|
+
* (Art. 17(3)(d)) or the contractual ground (Art. 17(3)(e)) it rests
|
|
209
|
+
* on, the facts relied on (Art. 17(3)(c)), whether the decision was
|
|
210
|
+
* taken by automated means (Art. 17(3)(c)), and the redress routes
|
|
211
|
+
* offered (Art. 17(3)(f)). Exactly one of `legalGround` /
|
|
212
|
+
* `contractualGround` is required so the ground is never left implicit.
|
|
213
|
+
* The returned record is frozen and is NOT framework-persisted — also
|
|
214
|
+
* submit it to the Commission's DSA Transparency Database per Art. 24(5)
|
|
215
|
+
* from your own pipeline. A best-effort `dsa.sor.recorded` audit event
|
|
216
|
+
* fires when an audit sink is wired.
|
|
217
|
+
*
|
|
218
|
+
* @opts
|
|
219
|
+
* contentId: string, // required — the content the decision concerns
|
|
220
|
+
* decision: string, // required — content-removed | content-disabled | ... | no-action
|
|
221
|
+
* facts: string, // required — the facts and circumstances relied on (Art. 17(3)(c))
|
|
222
|
+
* automated: boolean, // required — was the decision taken by automated means (Art. 17(3)(c))
|
|
223
|
+
* redressOptions: string[], // required — internal-complaint | out-of-court-settlement | judicial-redress
|
|
224
|
+
* legalGround: string, // one-of-two — the legal ground when the decision rests on illegality (Art. 17(3)(d))
|
|
225
|
+
* contractualGround: string, // one-of-two — the T&C clause when the decision rests on the contract (Art. 17(3)(e))
|
|
226
|
+
* sorId: string, // optional — operator id; defaults to "dsa-sor-<recordedAt>"
|
|
227
|
+
* noticeId: string, // optional — the Art. 16 notice this answers, if any
|
|
228
|
+
* territorialScope: string, // optional — geographic scope of the restriction (Art. 17(3)(b))
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* var s = b.dsa.statementOfReasons({
|
|
232
|
+
* contentId: "post-9931",
|
|
233
|
+
* decision: "content-removed",
|
|
234
|
+
* legalGround: "National law prohibiting the depicted sale.",
|
|
235
|
+
* facts: "Listing offered a prohibited item for sale.",
|
|
236
|
+
* automated: false,
|
|
237
|
+
* redressOptions: ["internal-complaint", "judicial-redress"],
|
|
238
|
+
* });
|
|
239
|
+
* // → { sorId, contentId, decision, recordedAt, groundType, automated, ... }
|
|
240
|
+
*/
|
|
241
|
+
function statementOfReasons(opts) {
|
|
242
|
+
validateOpts.requireObject(opts, "b.dsa.statementOfReasons: opts", DsaError, "dsa/bad-opts");
|
|
243
|
+
validateOpts(opts, [
|
|
244
|
+
"contentId", "decision", "facts", "automated", "redressOptions",
|
|
245
|
+
"legalGround", "contractualGround", "sorId", "noticeId", "territorialScope",
|
|
246
|
+
], "b.dsa.statementOfReasons");
|
|
247
|
+
validateOpts.requireNonEmptyString(opts.contentId, "b.dsa.statementOfReasons: opts.contentId", DsaError, "dsa/bad-content-id");
|
|
248
|
+
validateOpts.requireNonEmptyString(opts.decision, "b.dsa.statementOfReasons: opts.decision", DsaError, "dsa/bad-decision");
|
|
249
|
+
if (DECISIONS.indexOf(opts.decision) === -1) {
|
|
250
|
+
throw new DsaError("dsa/unknown-decision",
|
|
251
|
+
"b.dsa.statementOfReasons: unknown decision '" + opts.decision +
|
|
252
|
+
"' (allowed: " + DECISIONS.join(", ") + ")");
|
|
253
|
+
}
|
|
254
|
+
validateOpts.requireNonEmptyString(opts.facts, "b.dsa.statementOfReasons: opts.facts", DsaError, "dsa/bad-facts");
|
|
255
|
+
if (typeof opts.automated !== "boolean") {
|
|
256
|
+
throw new DsaError("dsa/bad-automated",
|
|
257
|
+
"b.dsa.statementOfReasons: opts.automated must be a boolean (Art. 17(3)(c) — was the decision automated)");
|
|
258
|
+
}
|
|
259
|
+
// Exactly one ground — never both, never neither. Art. 17(3)(d)/(e)
|
|
260
|
+
// require the statement to state the specific ground; leaving it
|
|
261
|
+
// implicit or asserting two grounds at once is the compliance-theater
|
|
262
|
+
// shape this refuses.
|
|
263
|
+
validateOpts.optionalNonEmptyString(opts.legalGround, "b.dsa.statementOfReasons: opts.legalGround", DsaError, "dsa/bad-legal-ground");
|
|
264
|
+
validateOpts.optionalNonEmptyString(opts.contractualGround, "b.dsa.statementOfReasons: opts.contractualGround", DsaError, "dsa/bad-contractual-ground");
|
|
265
|
+
var hasLegal = typeof opts.legalGround === "string" && opts.legalGround.length > 0;
|
|
266
|
+
var hasContractual = typeof opts.contractualGround === "string" && opts.contractualGround.length > 0;
|
|
267
|
+
if (hasLegal === hasContractual) {
|
|
268
|
+
throw new DsaError("dsa/ground-required",
|
|
269
|
+
"b.dsa.statementOfReasons: supply exactly one of legalGround (Art. 17(3)(d)) or " +
|
|
270
|
+
"contractualGround (Art. 17(3)(e)) — got " + (hasLegal ? "both" : "neither"));
|
|
271
|
+
}
|
|
272
|
+
if (!Array.isArray(opts.redressOptions) || opts.redressOptions.length === 0) {
|
|
273
|
+
throw new DsaError("dsa/redress-required",
|
|
274
|
+
"b.dsa.statementOfReasons: opts.redressOptions must be a non-empty array (Art. 17(3)(f)) — " +
|
|
275
|
+
"allowed: " + REDRESS_OPTIONS.join(", "));
|
|
276
|
+
}
|
|
277
|
+
opts.redressOptions.forEach(function (r, i) {
|
|
278
|
+
if (typeof r !== "string" || REDRESS_OPTIONS.indexOf(r) === -1) {
|
|
279
|
+
throw new DsaError("dsa/unknown-redress-option",
|
|
280
|
+
"b.dsa.statementOfReasons: redressOptions[" + i + "] '" + r +
|
|
281
|
+
"' is not a recognised redress route (allowed: " + REDRESS_OPTIONS.join(", ") + ")");
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
validateOpts.optionalNonEmptyString(opts.sorId, "b.dsa.statementOfReasons: opts.sorId", DsaError, "dsa/bad-sor-id");
|
|
285
|
+
validateOpts.optionalNonEmptyString(opts.noticeId, "b.dsa.statementOfReasons: opts.noticeId", DsaError, "dsa/bad-notice-id");
|
|
286
|
+
validateOpts.optionalNonEmptyString(opts.territorialScope, "b.dsa.statementOfReasons: opts.territorialScope", DsaError, "dsa/bad-territorial-scope");
|
|
287
|
+
|
|
288
|
+
var recordedAt = Date.now();
|
|
289
|
+
var record = Object.freeze({
|
|
290
|
+
sorId: opts.sorId || ("dsa-sor-" + recordedAt),
|
|
291
|
+
contentId: opts.contentId,
|
|
292
|
+
noticeId: opts.noticeId || null,
|
|
293
|
+
decision: opts.decision,
|
|
294
|
+
groundType: hasLegal ? "legal" : "contractual",
|
|
295
|
+
legalGround: hasLegal ? opts.legalGround : null,
|
|
296
|
+
contractualGround: hasContractual ? opts.contractualGround : null,
|
|
297
|
+
facts: opts.facts,
|
|
298
|
+
automated: opts.automated,
|
|
299
|
+
redressOptions: Object.freeze(opts.redressOptions.slice()),
|
|
300
|
+
territorialScope: opts.territorialScope || null,
|
|
301
|
+
recordedAt: recordedAt,
|
|
302
|
+
});
|
|
303
|
+
try {
|
|
304
|
+
audit().safeEmit({
|
|
305
|
+
action: "dsa.sor.recorded",
|
|
306
|
+
outcome: "success",
|
|
307
|
+
metadata: {
|
|
308
|
+
sorId: record.sorId,
|
|
309
|
+
contentId: record.contentId,
|
|
310
|
+
decision: record.decision,
|
|
311
|
+
groundType: record.groundType,
|
|
312
|
+
automated: record.automated,
|
|
313
|
+
noticeId: record.noticeId,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
} catch (_e) { /* drop-silent — audit is best-effort, never block the builder */ }
|
|
317
|
+
return record;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---- Art. 15 / Art. 24(3) transparency report ----
|
|
321
|
+
|
|
322
|
+
// The metric fields Art. 15(1) + Art. 24 expect a transparency report
|
|
323
|
+
// to carry. Every metric is a non-negative integer count over the
|
|
324
|
+
// reporting period; an omitted metric defaults to 0 so a partial
|
|
325
|
+
// report still produces a complete, comparable shape.
|
|
326
|
+
var METRIC_FIELDS = Object.freeze([
|
|
327
|
+
"noticesReceived", // Art. 15(1)(b) — notices submitted under Art. 16
|
|
328
|
+
"actionsTaken", // Art. 15(1)(b) — actions taken on those notices
|
|
329
|
+
"automatedDecisions", // Art. 15(1)(e) — content moderation by automated means
|
|
330
|
+
"ownInitiativeActions", // Art. 15(1)(c) — own-initiative content moderation
|
|
331
|
+
"statementsOfReasons", // Art. 24(1) — statements of reasons issued
|
|
332
|
+
"appeals", // Art. 24(1)(a) — Art. 20 internal complaints received
|
|
333
|
+
"appealsUpheld", // Art. 24(1)(a) — complaints decided in the recipient's favour
|
|
334
|
+
"outOfCourtDisputes", // Art. 24(1)(b) — Art. 21 out-of-court settlements
|
|
335
|
+
"accountSuspensions", // Art. 23 — suspensions for misuse
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
// The annual re-report clock. Art. 15(1) requires reporting "at least
|
|
339
|
+
// once a year"; the next-due default is one year after the period end.
|
|
340
|
+
var REPORT_PERIOD_MS = C.TIME.days(365);
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @primitive b.dsa.transparencyReport
|
|
344
|
+
* @signature b.dsa.transparencyReport(opts)
|
|
345
|
+
* @since 0.15.8
|
|
346
|
+
* @status stable
|
|
347
|
+
* @compliance dsa
|
|
348
|
+
* @related b.dsa.noticeAndAction, b.dsa.statementOfReasons, b.compliance.describe
|
|
349
|
+
*
|
|
350
|
+
* Build an Art. 15 (all intermediary services) / Art. 24(3) (online
|
|
351
|
+
* platforms) transparency report. The operator supplies the reporting
|
|
352
|
+
* period and the period counts — notices received, actions taken,
|
|
353
|
+
* automated decisions, appeals, and so on — and `transparencyReport`
|
|
354
|
+
* validates the shape, normalises every metric to a non-negative
|
|
355
|
+
* integer (omitted metrics default to 0 so a partial report still has a
|
|
356
|
+
* complete, comparable shape), stamps `generatedAt`, and computes
|
|
357
|
+
* `nextReportDueBy` one year after the period end (Art. 15(1) "at least
|
|
358
|
+
* once a year"). The returned report is frozen and is NOT
|
|
359
|
+
* framework-persisted — publish it from your own pipeline. A
|
|
360
|
+
* best-effort `dsa.transparency_report.generated` audit event fires
|
|
361
|
+
* when an audit sink is wired.
|
|
362
|
+
*
|
|
363
|
+
* @opts
|
|
364
|
+
* period: object, // required — { from: number, to: number } epoch-ms window (from < to)
|
|
365
|
+
* metrics: object, // optional — { <metric>: number } period counts; see b.dsa.listTransparencyMetrics()
|
|
366
|
+
* reportId: string, // optional — operator id; defaults to "dsa-transparency-<to>"
|
|
367
|
+
* service: string, // optional — the service the report covers
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* var r = b.dsa.transparencyReport({
|
|
371
|
+
* period: { from: Date.UTC(2025, 0, 1), to: Date.UTC(2025, 11, 31) },
|
|
372
|
+
* metrics: { noticesReceived: 1200, actionsTaken: 940, automatedDecisions: 610, appeals: 75 },
|
|
373
|
+
* });
|
|
374
|
+
* // → { reportId, period, metrics: {...all 9 normalised...}, generatedAt, nextReportDueBy }
|
|
375
|
+
*/
|
|
376
|
+
function transparencyReport(opts) {
|
|
377
|
+
validateOpts.requireObject(opts, "b.dsa.transparencyReport: opts", DsaError, "dsa/bad-opts");
|
|
378
|
+
validateOpts(opts, ["period", "metrics", "reportId", "service"], "b.dsa.transparencyReport");
|
|
379
|
+
if (!opts.period || typeof opts.period !== "object" || Array.isArray(opts.period)) {
|
|
380
|
+
throw new DsaError("dsa/bad-period",
|
|
381
|
+
"b.dsa.transparencyReport: opts.period must be a { from, to } object of epoch-ms numbers");
|
|
382
|
+
}
|
|
383
|
+
var from = opts.period.from;
|
|
384
|
+
var to = opts.period.to;
|
|
385
|
+
if (typeof from !== "number" || !isFinite(from) || from <= 0 ||
|
|
386
|
+
typeof to !== "number" || !isFinite(to) || to <= 0) {
|
|
387
|
+
throw new DsaError("dsa/bad-period",
|
|
388
|
+
"b.dsa.transparencyReport: opts.period.from and opts.period.to must be positive epoch-ms numbers");
|
|
389
|
+
}
|
|
390
|
+
if (from >= to) {
|
|
391
|
+
throw new DsaError("dsa/bad-period-order",
|
|
392
|
+
"b.dsa.transparencyReport: opts.period.from must be strictly before opts.period.to");
|
|
393
|
+
}
|
|
394
|
+
validateOpts.optionalNonEmptyString(opts.reportId, "b.dsa.transparencyReport: opts.reportId", DsaError, "dsa/bad-report-id");
|
|
395
|
+
validateOpts.optionalNonEmptyString(opts.service, "b.dsa.transparencyReport: opts.service", DsaError, "dsa/bad-service");
|
|
396
|
+
|
|
397
|
+
var supplied = opts.metrics;
|
|
398
|
+
if (supplied !== undefined && supplied !== null &&
|
|
399
|
+
(typeof supplied !== "object" || Array.isArray(supplied))) {
|
|
400
|
+
throw new DsaError("dsa/bad-metrics",
|
|
401
|
+
"b.dsa.transparencyReport: opts.metrics must be a plain object of metric counts");
|
|
402
|
+
}
|
|
403
|
+
supplied = supplied || {};
|
|
404
|
+
// Reject unknown metric keys — a misspelled metric would otherwise
|
|
405
|
+
// silently drop out of the published report.
|
|
406
|
+
Object.keys(supplied).forEach(function (k) {
|
|
407
|
+
if (METRIC_FIELDS.indexOf(k) === -1) {
|
|
408
|
+
throw new DsaError("dsa/unknown-metric",
|
|
409
|
+
"b.dsa.transparencyReport: unknown metric '" + k +
|
|
410
|
+
"' (see b.dsa.listTransparencyMetrics())");
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
var metrics = {};
|
|
414
|
+
METRIC_FIELDS.forEach(function (field) {
|
|
415
|
+
var v = supplied[field];
|
|
416
|
+
if (v === undefined || v === null) { metrics[field] = 0; return; }
|
|
417
|
+
if (typeof v !== "number" || !isFinite(v) || v < 0 || Math.floor(v) !== v) {
|
|
418
|
+
throw new DsaError("dsa/bad-metric-value",
|
|
419
|
+
"b.dsa.transparencyReport: metrics." + field +
|
|
420
|
+
" must be a non-negative integer, got " +
|
|
421
|
+
(typeof v === "number" ? String(v) : typeof v));
|
|
422
|
+
}
|
|
423
|
+
metrics[field] = v;
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
var generatedAt = Date.now();
|
|
427
|
+
var report = Object.freeze({
|
|
428
|
+
reportId: opts.reportId || ("dsa-transparency-" + to),
|
|
429
|
+
service: opts.service || null,
|
|
430
|
+
period: Object.freeze({ from: from, to: to }),
|
|
431
|
+
metrics: Object.freeze(metrics),
|
|
432
|
+
generatedAt: generatedAt,
|
|
433
|
+
nextReportDueBy: to + REPORT_PERIOD_MS,
|
|
434
|
+
});
|
|
435
|
+
try {
|
|
436
|
+
audit().safeEmit({
|
|
437
|
+
action: "dsa.transparency_report.generated",
|
|
438
|
+
outcome: "success",
|
|
439
|
+
metadata: {
|
|
440
|
+
reportId: report.reportId,
|
|
441
|
+
service: report.service,
|
|
442
|
+
periodFrom: from,
|
|
443
|
+
periodTo: to,
|
|
444
|
+
noticesReceived: metrics.noticesReceived,
|
|
445
|
+
actionsTaken: metrics.actionsTaken,
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
} catch (_e) { /* drop-silent — audit is best-effort, never block the builder */ }
|
|
449
|
+
return report;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* @primitive b.dsa.listTransparencyMetrics
|
|
454
|
+
* @signature b.dsa.listTransparencyMetrics()
|
|
455
|
+
* @since 0.15.8
|
|
456
|
+
* @status stable
|
|
457
|
+
* @related b.dsa.transparencyReport
|
|
458
|
+
*
|
|
459
|
+
* Return the frozen list of metric field names a `transparencyReport`
|
|
460
|
+
* aggregates — each maps to an Art. 15 / Art. 24 reporting obligation.
|
|
461
|
+
* Use it to render a data-entry form or to enumerate the counts the
|
|
462
|
+
* report normalises.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* b.dsa.listTransparencyMetrics();
|
|
466
|
+
* // → ["noticesReceived", "actionsTaken", "automatedDecisions", ...]
|
|
467
|
+
*/
|
|
468
|
+
function listTransparencyMetrics() {
|
|
469
|
+
return METRIC_FIELDS;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
module.exports = {
|
|
473
|
+
noticeAndAction: noticeAndAction,
|
|
474
|
+
statementOfReasons: statementOfReasons,
|
|
475
|
+
transparencyReport: transparencyReport,
|
|
476
|
+
listTransparencyMetrics: listTransparencyMetrics,
|
|
477
|
+
NOTICE_TYPES: NOTICE_TYPES,
|
|
478
|
+
DECISIONS: DECISIONS,
|
|
479
|
+
REDRESS_OPTIONS: REDRESS_OPTIONS,
|
|
480
|
+
METRIC_FIELDS: METRIC_FIELDS,
|
|
481
|
+
DsaError: DsaError,
|
|
482
|
+
};
|
|
@@ -389,6 +389,18 @@ var ComplianceError = defineClass("ComplianceError", { alwaysPermane
|
|
|
389
389
|
// vendorReview opts object, a non-boolean clause attestation, or an
|
|
390
390
|
// unknown clause key. Permanent — operator configuration, not transient.
|
|
391
391
|
var PrivacyError = defineClass("PrivacyError", { alwaysPermanent: true });
|
|
392
|
+
// DsaError covers b.dsa config-time misuse (EU Digital Services Act,
|
|
393
|
+
// Reg 2022/2065): malformed noticeAndAction / statementOfReasons /
|
|
394
|
+
// transparencyReport opts, an unknown notice-type / decision / redress /
|
|
395
|
+
// metric key, a statement of reasons with neither or both grounds, an
|
|
396
|
+
// out-of-order reporting period. Permanent — operator-supplied record shape.
|
|
397
|
+
var DsaError = defineClass("DsaError", { alwaysPermanent: true });
|
|
398
|
+
// PiplError covers b.pipl config-time misuse (China PIPL cross-border
|
|
399
|
+
// transfer): a malformed sccFilingAssessment / securityAssessmentCertificate
|
|
400
|
+
// opts object, an unknown legalBasis / riskRating enum, an empty required
|
|
401
|
+
// array, a bad recordedAt clock, or a malformed injected audit sink.
|
|
402
|
+
// Permanent — operator configuration, not transient.
|
|
403
|
+
var PiplError = defineClass("PiplError", { alwaysPermanent: true });
|
|
392
404
|
// SmtpPolicyError covers MTA-STS / DANE / TLS-RPT misuse: bad-policy
|
|
393
405
|
// shape, fetch failures, TLSA-record format errors, missing records.
|
|
394
406
|
// Permanent — these are policy / DNS configuration errors, not
|
|
@@ -722,6 +734,8 @@ module.exports = {
|
|
|
722
734
|
DoraError: DoraError,
|
|
723
735
|
ComplianceError: ComplianceError,
|
|
724
736
|
PrivacyError: PrivacyError,
|
|
737
|
+
DsaError: DsaError,
|
|
738
|
+
PiplError: PiplError,
|
|
725
739
|
SmtpPolicyError: SmtpPolicyError,
|
|
726
740
|
MailAuthError: MailAuthError,
|
|
727
741
|
MailArfError: MailArfError,
|
|
@@ -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
|
-
|
|
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))
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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),
|