@blamejs/blamejs-shop 0.4.32 → 0.4.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +72 -52
- 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 +6 -0
- package/lib/vendor/blamejs/MIGRATING.md +12 -0
- package/lib/vendor/blamejs/README.md +5 -2
- package/lib/vendor/blamejs/SECURITY.md +4 -2
- 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/auth/elevation-grant.js +6 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +13 -0
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +5 -2
- package/lib/vendor/blamejs/lib/cli.js +8 -1
- package/lib/vendor/blamejs/lib/compliance.js +4 -0
- package/lib/vendor/blamejs/lib/config-drift.js +2 -1
- package/lib/vendor/blamejs/lib/credential-hash.js +9 -0
- 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/retention.js +16 -2
- package/lib/vendor/blamejs/lib/scheduler.js +12 -0
- package/lib/vendor/blamejs/lib/self-update.js +1 -1
- package/lib/vendor/blamejs/lib/session.js +64 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +25 -7
- 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.7.json +43 -0
- 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/gen-migrating.js +16 -0
- package/lib/vendor/blamejs/scripts/generate-ssdf-attestation.js +338 -0
- package/lib/vendor/blamejs/test/00-primitives.js +51 -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 +250 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/credential-hash.test.js +18 -0
- 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/retention-floor.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +64 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-watchdog-stale-settle.test.js +71 -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 +2 -2
|
@@ -19,8 +19,12 @@
|
|
|
19
19
|
* iat / exp / jti
|
|
20
20
|
*
|
|
21
21
|
* Tokens are revocable: revoke(jti) adds the jti to an in-process
|
|
22
|
-
* deny-set checked by verify().
|
|
23
|
-
*
|
|
22
|
+
* deny-set checked by verify(). Revocation is in-process only and does
|
|
23
|
+
* NOT propagate across cluster nodes — a jti revoked on one node is not
|
|
24
|
+
* denied on another. On a multi-node deployment, keep grant TTLs short
|
|
25
|
+
* (the iat/exp window is the cross-node bound) and route revocation
|
|
26
|
+
* through a shared, externally-checked store of your own; this module
|
|
27
|
+
* does not yet accept a backing revoked-set.
|
|
24
28
|
*
|
|
25
29
|
* Token format: base64url(JSON-payload) + "." + base64url(HMAC).
|
|
26
30
|
*
|
|
@@ -1896,6 +1896,19 @@ function create(opts) {
|
|
|
1896
1896
|
throw new OAuthError("auth-oauth/aud-mismatch",
|
|
1897
1897
|
"ID token aud does not contain clientId '" + clientId + "'");
|
|
1898
1898
|
}
|
|
1899
|
+
// OIDC Core §3.1.3.7: a multi-audience ID token MUST carry an azp
|
|
1900
|
+
// (authorized party), and a present azp MUST equal our client_id.
|
|
1901
|
+
// Without this, a token whose authorized party is a DIFFERENT client but
|
|
1902
|
+
// whose aud array also lists this RP would verify clean — a confused-deputy
|
|
1903
|
+
// / token-substitution hole.
|
|
1904
|
+
if (aud.length > 1 && typeof payload.azp !== "string") {
|
|
1905
|
+
throw new OAuthError("auth-oauth/azp-required",
|
|
1906
|
+
"ID token has multiple audiences but no azp (authorized party) claim");
|
|
1907
|
+
}
|
|
1908
|
+
if (payload.azp !== undefined && payload.azp !== clientId) {
|
|
1909
|
+
throw new OAuthError("auth-oauth/azp-mismatch",
|
|
1910
|
+
"ID token azp '" + payload.azp + "' is not clientId '" + clientId + "'");
|
|
1911
|
+
}
|
|
1899
1912
|
if (vopts.nonce && !vopts.skipNonceCheck) {
|
|
1900
1913
|
// Constant-time nonce compare — secret-shaped value matched
|
|
1901
1914
|
// against attacker-controlled payload.
|
|
@@ -638,11 +638,14 @@ async function verify(presentation, opts) {
|
|
|
638
638
|
jwtExternal._assertAlgKtyMatch(kbAlg, holderKey);
|
|
639
639
|
var holderKeyObj = nodeCrypto.createPublicKey({ key: holderKey, format: "jwk" });
|
|
640
640
|
var kbParsed = _verifyJwt(maybeKbJwt, holderKeyObj, kbAlg);
|
|
641
|
-
|
|
641
|
+
// Constant-time compares: the nonce is a verifier-issued replay-defense
|
|
642
|
+
// value, so a short-circuiting !== leaks a matching-prefix timing oracle.
|
|
643
|
+
// Matches the sd_hash check below (the framework's hash/token discipline).
|
|
644
|
+
if (opts.audience && !_timingSafeEqStr(kbParsed.payload.aud, opts.audience)) {
|
|
642
645
|
throw new AuthError("auth-sd-jwt-vc/wrong-audience",
|
|
643
646
|
"verify: KB-JWT aud mismatch");
|
|
644
647
|
}
|
|
645
|
-
if (opts.nonce && kbParsed.payload.nonce
|
|
648
|
+
if (opts.nonce && !_timingSafeEqStr(kbParsed.payload.nonce, opts.nonce)) {
|
|
646
649
|
throw new AuthError("auth-sd-jwt-vc/wrong-nonce",
|
|
647
650
|
"verify: KB-JWT nonce mismatch (replay defense)");
|
|
648
651
|
}
|
|
@@ -94,7 +94,14 @@ function _openSqlite(dbPath) {
|
|
|
94
94
|
// Lazy-required so the CLI doesn't crash on `blamejs version` or
|
|
95
95
|
// `blamejs help` if node:sqlite isn't usable for some reason.
|
|
96
96
|
var { DatabaseSync } = require("node:sqlite");
|
|
97
|
-
|
|
97
|
+
// Same SQLITE_LIMIT_ sqlLength cap as db.init's main handle — the CLI opens
|
|
98
|
+
// the operator's real database for migrate / inspect, so the parse-time DoS
|
|
99
|
+
// floor applies here too.
|
|
100
|
+
return new DatabaseSync(dbPath, {
|
|
101
|
+
limits: {
|
|
102
|
+
sqlLength: C.BYTES.mib(1),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
// ---- Subcommand: migrate ----
|
|
@@ -594,6 +594,10 @@ function clear() {
|
|
|
594
594
|
}
|
|
595
595
|
STATE.posture = null;
|
|
596
596
|
STATE.setAt = null;
|
|
597
|
+
// Cascade the reset the same way set() cascades the posture — otherwise a
|
|
598
|
+
// primitive that inherits the active posture (e.g. retention.complianceFloor)
|
|
599
|
+
// keeps applying the stale floor after the global posture was cleared.
|
|
600
|
+
_applyPostureCascade(null);
|
|
597
601
|
}
|
|
598
602
|
|
|
599
603
|
function _resetForTest() {
|
|
@@ -44,6 +44,7 @@ var lazyRequire = require("./lazy-require");
|
|
|
44
44
|
var safeJson = require("./safe-json");
|
|
45
45
|
var validateOpts = require("./validate-opts");
|
|
46
46
|
var { defineClass } = require("./framework-error");
|
|
47
|
+
var atomicFile = require("./atomic-file");
|
|
47
48
|
|
|
48
49
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
49
50
|
|
|
@@ -201,7 +202,7 @@ function create(opts) {
|
|
|
201
202
|
};
|
|
202
203
|
var tmp = sidecarPath + ".tmp";
|
|
203
204
|
nodeFs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
204
|
-
|
|
205
|
+
atomicFile.renameWithRetry(tmp, sidecarPath);
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
function _verifySidecar(parsed) {
|
|
@@ -380,6 +380,15 @@ function needsRehash(envelope, opts) {
|
|
|
380
380
|
try { return passwordModule().needsRehash(phc, opts && opts.params); }
|
|
381
381
|
catch (_e) { return true; }
|
|
382
382
|
}
|
|
383
|
+
if (decoded.algoId === C.CRED_HASH_IDS.SHAKE256) {
|
|
384
|
+
// Length-rotation: rehash when the stored digest is SHORTER than the
|
|
385
|
+
// configured/default output length. Upgrade-only (`<`, matching the Argon2
|
|
386
|
+
// needsRehash convention) — a longer-than-target digest is not actively
|
|
387
|
+
// shortened. Without this compare, raising the SHAKE256 length never
|
|
388
|
+
// triggered a rehash and the advertised rotation was a silent no-op.
|
|
389
|
+
var targetLength = (opts && opts.params && opts.params.length) || SHAKE256_DEFAULT_LENGTH;
|
|
390
|
+
if (decoded.payload.length < targetLength) return true;
|
|
391
|
+
}
|
|
383
392
|
return false;
|
|
384
393
|
}
|
|
385
394
|
|
|
@@ -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,
|