@blamejs/blamejs-shop 0.4.33 → 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 (48) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/vendor/MANIFEST.json +54 -38
  4. package/lib/vendor/blamejs/.github/workflows/ci.yml +12 -12
  5. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +37 -5
  6. package/lib/vendor/blamejs/.github/workflows/release-container.yml +2 -2
  7. package/lib/vendor/blamejs/CHANGELOG.md +4 -0
  8. package/lib/vendor/blamejs/README.md +5 -2
  9. package/lib/vendor/blamejs/SECURITY.md +3 -1
  10. package/lib/vendor/blamejs/api-snapshot.json +137 -2
  11. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
  12. package/lib/vendor/blamejs/index.js +4 -0
  13. package/lib/vendor/blamejs/lib/archive-read.js +2 -1
  14. package/lib/vendor/blamejs/lib/archive-tar-read.js +2 -1
  15. package/lib/vendor/blamejs/lib/atomic-file.js +5 -0
  16. package/lib/vendor/blamejs/lib/audit.js +2 -0
  17. package/lib/vendor/blamejs/lib/cli.js +8 -1
  18. package/lib/vendor/blamejs/lib/config-drift.js +2 -1
  19. package/lib/vendor/blamejs/lib/db.js +15 -2
  20. package/lib/vendor/blamejs/lib/dsa.js +482 -0
  21. package/lib/vendor/blamejs/lib/framework-error.js +14 -0
  22. package/lib/vendor/blamejs/lib/http-client.js +5 -2
  23. package/lib/vendor/blamejs/lib/local-db-thin.js +3 -2
  24. package/lib/vendor/blamejs/lib/log-stream-local.js +1 -1
  25. package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +9 -2
  26. package/lib/vendor/blamejs/lib/log-stream-otlp.js +16 -7
  27. package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +36 -11
  28. package/lib/vendor/blamejs/lib/mtls-ca.js +2 -2
  29. package/lib/vendor/blamejs/lib/observability.js +3 -2
  30. package/lib/vendor/blamejs/lib/pipl-cn.js +377 -0
  31. package/lib/vendor/blamejs/lib/restore-rollback.js +5 -5
  32. package/lib/vendor/blamejs/lib/self-update.js +1 -1
  33. package/lib/vendor/blamejs/lib/session.js +64 -0
  34. package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -3
  35. package/lib/vendor/blamejs/lib/watcher.js +8 -0
  36. package/lib/vendor/blamejs/package.json +2 -2
  37. package/lib/vendor/blamejs/release-notes/v0.15.8.json +48 -0
  38. package/lib/vendor/blamejs/release-notes/v0.15.9.json +58 -0
  39. package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +338 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-rename-retry.test.js +70 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +174 -3
  42. package/lib/vendor/blamejs/test/layer-0-primitives/db-init-extensions.test.js +32 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/dsa.test.js +169 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +40 -1
  45. package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +172 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +57 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +7 -3
  48. package/package.json +1 -1
@@ -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
- 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
  };