@blamejs/core 0.7.24 → 0.7.39

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/lib/crypto.js CHANGED
@@ -203,6 +203,21 @@ function decryptEnvelope(packed, privateKeys) {
203
203
  symmetricKey = kdf(Buffer.concat([mlkemSs, ecSs]), C.BYTES.bytes(32));
204
204
  } else if (kemId === C.KEM_IDS.ML_KEM_1024) {
205
205
  symmetricKey = kdf(mlkemSs, C.BYTES.bytes(32));
206
+ } else if (kemId === C.KEM_IDS.ML_KEM_768_X25519) {
207
+ // ML-KEM-768 + X25519 hybrid envelope. The mlkemPriv must be an
208
+ // ML-KEM-768 key (not 1024); operators are responsible for passing
209
+ // the correct keypair via privateKeys when the envelope was sealed
210
+ // with this algorithm. Same length-prefixed shape as the P-384
211
+ // hybrid: 2-byte ec-eph-len + DER X25519 pubkey + nonce + ct.
212
+ var x25519EphLen = packed.readUInt16BE(pos); pos += 2;
213
+ var x25519EphDer = packed.subarray(pos, pos + x25519EphLen); pos += x25519EphLen;
214
+ var x25519PrivPem = typeof privateKeys === "string" ? null : privateKeys.x25519PrivateKey;
215
+ if (!x25519PrivPem) throw new Error("ML-KEM-768 + X25519 hybrid envelope requires x25519PrivateKey");
216
+ var x25519Ss = nodeCrypto.diffieHellman({
217
+ privateKey: nodeCrypto.createPrivateKey(x25519PrivPem),
218
+ publicKey: nodeCrypto.createPublicKey({ key: x25519EphDer, type: "spki", format: "der" }),
219
+ });
220
+ symmetricKey = kdf(Buffer.concat([mlkemSs, x25519Ss]), C.BYTES.bytes(32));
206
221
  } else {
207
222
  throw new Error("Invalid envelope: unsupported KEM ID " + kemId);
208
223
  }
@@ -241,6 +256,80 @@ function decryptPacked(packed, key, aad) {
241
256
  );
242
257
  }
243
258
 
259
+ // ---- ML-KEM-768 + X25519 hybrid (TLS-interop envelope) ----
260
+ //
261
+ // The IETF / Cloudflare / Chrome standardized hybrid for TLS 1.3
262
+ // (codepoint 0x11EC). Smaller payload than ML-KEM-1024 + P-384
263
+ // (~1.1 KB vs ~1.6 KB), wider interop with peers using the same
264
+ // hybrid (Cloudflare Workers, Chrome, blamejs-on-the-other-side).
265
+ //
266
+ // Operators wire this when the recipient publishes ML-KEM-768 +
267
+ // X25519 keys. Generation:
268
+ //
269
+ // var pair = b.crypto.generateMlkem768X25519KeyPair();
270
+ // // → { mlkemPublicKey, mlkemPrivateKey,
271
+ // // x25519PublicKey, x25519PrivateKey }
272
+ //
273
+ // var envelope = b.crypto.encryptMlkem768X25519(plaintext, {
274
+ // mlkemPublicKey: recipient.mlkemPublicKey,
275
+ // x25519PublicKey: recipient.x25519PublicKey,
276
+ // });
277
+ //
278
+ // Decryption goes through the existing b.crypto.decrypt(envelope,
279
+ // privateKeys) — the envelope-magic dispatch handles KEM_IDS.
280
+ // ML_KEM_768_X25519. privateKeys MUST shape as { privateKey,
281
+ // x25519PrivateKey } — privateKey is the ML-KEM-768 PEM, NOT the
282
+ // default ML-KEM-1024.
283
+
284
+ function generateMlkem768X25519KeyPair() {
285
+ var mlkem = generateKeyPair("ml-kem-768");
286
+ var x25519 = generateKeyPair("x25519");
287
+ return {
288
+ mlkemPublicKey: mlkem.publicKey,
289
+ mlkemPrivateKey: mlkem.privateKey,
290
+ x25519PublicKey: x25519.publicKey,
291
+ x25519PrivateKey: x25519.privateKey,
292
+ };
293
+ }
294
+
295
+ function encryptMlkem768X25519(plaintext, recipient) {
296
+ if (!recipient || !recipient.mlkemPublicKey || !recipient.x25519PublicKey) {
297
+ throw new Error("encryptMlkem768X25519 requires { mlkemPublicKey, x25519PublicKey }");
298
+ }
299
+ var mlkemPub = nodeCrypto.createPublicKey(recipient.mlkemPublicKey);
300
+ var kem = nodeCrypto.encapsulate(mlkemPub);
301
+ var ephX25519 = generateKeyPair("x25519", {
302
+ publicKeyEncoding: { type: "spki", format: "der" },
303
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
304
+ });
305
+ var x25519Ss = nodeCrypto.diffieHellman({
306
+ privateKey: nodeCrypto.createPrivateKey(ephX25519.privateKey),
307
+ publicKey: nodeCrypto.createPublicKey(recipient.x25519PublicKey),
308
+ });
309
+ var key = kdf(Buffer.concat([kem.sharedKey, x25519Ss]), C.BYTES.bytes(32));
310
+ var nonce = generateBytes(C.BYTES.bytes(24));
311
+ var ct = xchacha20poly1305(key, nonce).encrypt(Buffer.from(plaintext, "utf8"));
312
+
313
+ var kemCtLen = Buffer.alloc(2); kemCtLen.writeUInt16BE(kem.ciphertext.length);
314
+ var x25519EphDer = ephX25519.publicKey;
315
+ var x25519EphLen = Buffer.alloc(2); x25519EphLen.writeUInt16BE(x25519EphDer.length);
316
+
317
+ return Buffer.concat([
318
+ Buffer.from([C.ENVELOPE_MAGIC, C.KEM_IDS.ML_KEM_768_X25519,
319
+ C.ACTIVE.CIPHER, C.ACTIVE.KDF]),
320
+ kemCtLen, kem.ciphertext, x25519EphLen, x25519EphDer, nonce, Buffer.from(ct),
321
+ ]).toString("base64");
322
+ }
323
+
324
+ // Operator-audit accessor — exposes every supported KEM hybrid for
325
+ // compliance audit visibility ("which envelopes does this deploy
326
+ // accept on decrypt?").
327
+ var SUPPORTED_KEM_ALGORITHMS = Object.freeze([
328
+ { id: "ml-kem-1024", envelopeId: C.KEM_IDS.ML_KEM_1024, description: "ML-KEM-1024 KEM-only (legacy single-component)" },
329
+ { id: "ml-kem-1024-p384", envelopeId: C.KEM_IDS.ML_KEM_1024_P384, description: "ML-KEM-1024 + ECDH P-384 hybrid (framework default)" },
330
+ { id: "ml-kem-768-x25519", envelopeId: C.KEM_IDS.ML_KEM_768_X25519, description: "ML-KEM-768 + X25519 hybrid (IETF / Cloudflare / Chrome TLS 1.3 codepoint 0x11EC)" },
331
+ ]);
332
+
244
333
  module.exports = {
245
334
  // Hashing
246
335
  sha3Hash: sha3Hash,
@@ -254,12 +343,15 @@ module.exports = {
254
343
  // Keys
255
344
  generateEncryptionKeyPair: generateEncryptionKeyPair,
256
345
  generateSigningKeyPair: generateSigningKeyPair,
346
+ generateMlkem768X25519KeyPair: generateMlkem768X25519KeyPair,
257
347
  // Signatures
258
348
  sign: sign,
259
349
  verify: verify,
260
350
  // Envelope encrypt/decrypt
261
351
  encrypt: encrypt,
262
352
  decrypt: decrypt,
353
+ encryptMlkem768X25519: encryptMlkem768X25519,
354
+ SUPPORTED_KEM_ALGORITHMS: SUPPORTED_KEM_ALGORITHMS,
263
355
  // Symmetric buffer encrypt/decrypt
264
356
  encryptPacked: encryptPacked,
265
357
  decryptPacked: decryptPacked,
package/lib/dora.js ADDED
@@ -0,0 +1,347 @@
1
+ "use strict";
2
+ /**
3
+ * b.dora — DORA Article 17 ICT-related incident-reporting workflow.
4
+ *
5
+ * Digital Operational Resilience Act (Regulation (EU) 2022/2554)
6
+ * Article 17 requires every "financial entity" subject to DORA to
7
+ * classify, document, and report ICT-related incidents according to
8
+ * the harmonized RTS template (Commission Delegated Regulation
9
+ * 2024/1772). This primitive is the framework hook — operators wire
10
+ * it into their incident-management workflow; the framework owns the
11
+ * classification rubric, the three-stage report shape (initial /
12
+ * intermediate / final), and the audit-chain integration.
13
+ *
14
+ * var dora = b.dora.create({ audit: b.audit });
15
+ *
16
+ * var classification = dora.classify({
17
+ * dataAffected: "phi" | "financial" | "personal" | "operational" | "none",
18
+ * systemsAffected: ["payments-gateway", "core-ledger"],
19
+ * durationMs: C.TIME.hours(4),
20
+ * severityIndicator: "critical" | "high" | "medium" | "low",
21
+ * economicImpact: { eur: 50000 },
22
+ * affectedClients: 1200,
23
+ * geographicScope: ["DE", "FR"],
24
+ * reputationalImpact: "media" | "internal" | "none",
25
+ * });
26
+ * // → { classification: "major" | "significant" | "minor",
27
+ * // mustReport: true|false, mustReportInitialBy: ms-since-detection,
28
+ * // reasons: [...] }
29
+ *
30
+ * var initial = dora.report({
31
+ * incidentId: "INC-2026-0042",
32
+ * classification: "major",
33
+ * stage: "initial",
34
+ * detectedAt: Date.now() - C.TIME.minutes(60),
35
+ * description: "Payment-gateway outage — 2-hour customer-facing impact",
36
+ * causeKnown: false,
37
+ * mitigationStarted: true,
38
+ * });
39
+ *
40
+ * // 72h after detection: intermediate update
41
+ * dora.report(Object.assign({}, initial, { stage: "intermediate", ... }));
42
+ * // 1 month later (or upon closure): final report
43
+ * dora.report(Object.assign({}, initial, { stage: "final", rootCause: "...", ... }));
44
+ *
45
+ * Audit posture (audit namespace "dora"):
46
+ * - dora.incident.classified — every classify() call
47
+ * - dora.incident.reported — every report() submission
48
+ * - dora.incident.draftFinal — every draftFinalReport() generation
49
+ *
50
+ * The primitive does NOT submit to ESAs / national supervisors — that
51
+ * step is operator-side (channel + credentials are operator-specific).
52
+ * The primitive produces the RTS-template-shaped record that the
53
+ * operator's submission code drops into the regulator's API.
54
+ */
55
+
56
+ var lazyRequire = require("./lazy-require");
57
+ var validateOpts = require("./validate-opts");
58
+ var C = require("./constants");
59
+ var { DoraError } = require("./framework-error");
60
+
61
+ var audit = lazyRequire(function () { return require("./audit"); });
62
+ var observability = lazyRequire(function () { return require("./observability"); });
63
+
64
+ // ---- Classification thresholds (RTS 2024/1772 Articles 1-12) ----
65
+
66
+ // Article 1 — major incident criteria. An incident classifies as
67
+ // "major" when ANY of the following thresholds is met:
68
+ // - Critical/severe data integrity / confidentiality / availability
69
+ // impact
70
+ // - >= 100k clients affected OR >= 10% of clients
71
+ // - Economic impact >= 100k EUR
72
+ // - Cross-border (>= 2 EU member states) impact
73
+ // - Critical-process disruption >= 8h
74
+ // - Reputational impact (media coverage)
75
+ var MAJOR_INCIDENT_THRESHOLDS = Object.freeze({
76
+ affectedClientsAbsolute: 100000, // allow:raw-byte-literal — RTS 2024/1772 Art. 1(1)(a) regulator-fixed cap (100k clients)
77
+ affectedClientsPercentile: 0.10, // RTS Art. 1(1)(a) — 10% client base
78
+ economicImpactEur: 100000, // allow:raw-byte-literal — RTS 2024/1772 Art. 1(1)(c) regulator-fixed cap (100k EUR)
79
+ geographicMemberStates: 2, // RTS Art. 1(1)(d) — 2+ member states
80
+ durationCriticalProcessMs: C.TIME.hours(8), // RTS Art. 1(1)(e) — 8h
81
+ });
82
+
83
+ // Article 8 — significant incident (one threshold below major).
84
+ // Default threshold values per ESA guidelines.
85
+ var SIGNIFICANT_INCIDENT_THRESHOLDS = Object.freeze({
86
+ affectedClientsAbsolute: 10000, // allow:raw-byte-literal — ESA-guideline regulator-fixed cap (10k clients)
87
+ affectedClientsPercentile: 0.01, // 1% client base
88
+ economicImpactEur: 10000, // allow:raw-byte-literal — ESA-guideline regulator-fixed cap (10k EUR)
89
+ durationCriticalProcessMs: C.TIME.hours(2), // 2h
90
+ });
91
+
92
+ // Article 19 — initial report deadline: 24h from "first awareness".
93
+ // Article 19(4) — intermediate update: within 72h of initial.
94
+ // Article 19(6) — final report: within 1 month of initial.
95
+ var INITIAL_REPORT_DEADLINE_MS = C.TIME.hours(24);
96
+ var INTERMEDIATE_REPORT_DEADLINE_MS = C.TIME.hours(72);
97
+ var FINAL_REPORT_DEADLINE_MS = C.TIME.days(30);
98
+
99
+ var VALID_DATA_AFFECTED = ["phi", "financial", "personal", "operational", "none"];
100
+ var VALID_SEVERITY = ["critical", "high", "medium", "low"];
101
+ var VALID_REPUTATIONAL = ["media", "internal", "none"];
102
+ var VALID_STAGES = ["initial", "intermediate", "final"];
103
+ var VALID_CLASSIFICATIONS = ["major", "significant", "minor"];
104
+
105
+ // ---- Classification rubric ----
106
+
107
+ function _classifyImpl(input) {
108
+ var reasons = [];
109
+ var hitsMajor = 0;
110
+ var hitsSignificant = 0;
111
+
112
+ // 1. Severity indicator — critical alone qualifies as major.
113
+ if (input.severityIndicator === "critical") {
114
+ hitsMajor += 1;
115
+ reasons.push("severity-critical");
116
+ } else if (input.severityIndicator === "high") {
117
+ hitsSignificant += 1;
118
+ reasons.push("severity-high");
119
+ }
120
+
121
+ // 2. Affected clients (absolute).
122
+ if (typeof input.affectedClients === "number" && input.affectedClients > 0) {
123
+ if (input.affectedClients >= MAJOR_INCIDENT_THRESHOLDS.affectedClientsAbsolute) {
124
+ hitsMajor += 1;
125
+ reasons.push("clients-major-absolute");
126
+ } else if (input.affectedClients >= SIGNIFICANT_INCIDENT_THRESHOLDS.affectedClientsAbsolute) {
127
+ hitsSignificant += 1;
128
+ reasons.push("clients-significant-absolute");
129
+ }
130
+ }
131
+
132
+ // 3. Economic impact.
133
+ if (input.economicImpact && typeof input.economicImpact.eur === "number") {
134
+ if (input.economicImpact.eur >= MAJOR_INCIDENT_THRESHOLDS.economicImpactEur) {
135
+ hitsMajor += 1;
136
+ reasons.push("economic-major");
137
+ } else if (input.economicImpact.eur >= SIGNIFICANT_INCIDENT_THRESHOLDS.economicImpactEur) {
138
+ hitsSignificant += 1;
139
+ reasons.push("economic-significant");
140
+ }
141
+ }
142
+
143
+ // 4. Geographic scope (cross-border).
144
+ if (Array.isArray(input.geographicScope) &&
145
+ input.geographicScope.length >= MAJOR_INCIDENT_THRESHOLDS.geographicMemberStates) {
146
+ hitsMajor += 1;
147
+ reasons.push("geographic-cross-border");
148
+ }
149
+
150
+ // 5. Duration (critical-process disruption).
151
+ if (typeof input.durationMs === "number" && input.durationMs > 0) {
152
+ if (input.durationMs >= MAJOR_INCIDENT_THRESHOLDS.durationCriticalProcessMs) {
153
+ hitsMajor += 1;
154
+ reasons.push("duration-major");
155
+ } else if (input.durationMs >= SIGNIFICANT_INCIDENT_THRESHOLDS.durationCriticalProcessMs) {
156
+ hitsSignificant += 1;
157
+ reasons.push("duration-significant");
158
+ }
159
+ }
160
+
161
+ // 6. Reputational impact (Article 1(1)(f)).
162
+ if (input.reputationalImpact === "media") {
163
+ hitsMajor += 1;
164
+ reasons.push("reputational-media");
165
+ }
166
+
167
+ // 7. Sensitive data classes — phi / financial → at minimum significant.
168
+ if (input.dataAffected === "phi" || input.dataAffected === "financial") {
169
+ hitsSignificant += 1;
170
+ reasons.push("data-sensitive-" + input.dataAffected);
171
+ }
172
+
173
+ var classification;
174
+ if (hitsMajor >= 1) {
175
+ classification = "major";
176
+ } else if (hitsSignificant >= 1) {
177
+ classification = "significant";
178
+ } else {
179
+ classification = "minor";
180
+ }
181
+ var mustReport = classification !== "minor";
182
+ return {
183
+ classification: classification,
184
+ mustReport: mustReport,
185
+ mustReportInitialByMs: mustReport ? INITIAL_REPORT_DEADLINE_MS : null,
186
+ reasons: reasons,
187
+ };
188
+ }
189
+
190
+ // ---- Report-shape validators (RTS 2024/1772 templates) ----
191
+
192
+ function _validateReportInput(input) {
193
+ if (!input || typeof input !== "object") {
194
+ throw new DoraError("dora/bad-report",
195
+ "report: input must be an object");
196
+ }
197
+ if (typeof input.incidentId !== "string" || input.incidentId.length === 0) {
198
+ throw new DoraError("dora/missing-incident-id",
199
+ "report: incidentId is required (non-empty string)");
200
+ }
201
+ if (VALID_CLASSIFICATIONS.indexOf(input.classification) === -1) {
202
+ throw new DoraError("dora/bad-classification",
203
+ "report: classification must be one of " +
204
+ VALID_CLASSIFICATIONS.join(", ") + ", got " + JSON.stringify(input.classification));
205
+ }
206
+ if (VALID_STAGES.indexOf(input.stage) === -1) {
207
+ throw new DoraError("dora/bad-stage",
208
+ "report: stage must be one of " + VALID_STAGES.join(", ") +
209
+ ", got " + JSON.stringify(input.stage));
210
+ }
211
+ if (typeof input.detectedAt !== "number" || !isFinite(input.detectedAt) || input.detectedAt <= 0) {
212
+ throw new DoraError("dora/bad-detected-at",
213
+ "report: detectedAt must be a positive ms-since-epoch number");
214
+ }
215
+ if (typeof input.description !== "string" || input.description.length === 0) {
216
+ throw new DoraError("dora/missing-description",
217
+ "report: description is required");
218
+ }
219
+ }
220
+
221
+ // ---- Public surface ----
222
+
223
+ function create(opts) {
224
+ opts = opts || {};
225
+ validateOpts(opts, ["audit", "observability"], "dora.create");
226
+ var auditOn = opts.audit !== false;
227
+
228
+ function _emit(action, info) {
229
+ if (!auditOn) return;
230
+ try {
231
+ audit().safeEmit({
232
+ action: action,
233
+ outcome: info.outcome || "success",
234
+ metadata: info.metadata || {},
235
+ });
236
+ } catch (_e) { /* audit best-effort */ }
237
+ }
238
+
239
+ function classify(input) {
240
+ if (!input || typeof input !== "object") {
241
+ throw new DoraError("dora/bad-classify-input",
242
+ "classify: input must be an object");
243
+ }
244
+ if (input.dataAffected !== undefined &&
245
+ VALID_DATA_AFFECTED.indexOf(input.dataAffected) === -1) {
246
+ throw new DoraError("dora/bad-data-affected",
247
+ "classify: dataAffected must be one of " +
248
+ VALID_DATA_AFFECTED.join(", "));
249
+ }
250
+ if (input.severityIndicator !== undefined &&
251
+ VALID_SEVERITY.indexOf(input.severityIndicator) === -1) {
252
+ throw new DoraError("dora/bad-severity",
253
+ "classify: severityIndicator must be one of " + VALID_SEVERITY.join(", "));
254
+ }
255
+ if (input.reputationalImpact !== undefined &&
256
+ VALID_REPUTATIONAL.indexOf(input.reputationalImpact) === -1) {
257
+ throw new DoraError("dora/bad-reputational",
258
+ "classify: reputationalImpact must be one of " + VALID_REPUTATIONAL.join(", "));
259
+ }
260
+ var rv = _classifyImpl(input);
261
+ _emit("dora.incident.classified", {
262
+ metadata: {
263
+ classification: rv.classification,
264
+ mustReport: rv.mustReport,
265
+ reasons: rv.reasons,
266
+ },
267
+ });
268
+ return rv;
269
+ }
270
+
271
+ function report(input) {
272
+ _validateReportInput(input);
273
+ var record = {
274
+ incidentId: input.incidentId,
275
+ classification: input.classification,
276
+ stage: input.stage,
277
+ detectedAt: input.detectedAt,
278
+ reportedAt: Date.now(),
279
+ description: input.description,
280
+ causeKnown: input.causeKnown !== undefined ? !!input.causeKnown : null,
281
+ rootCause: input.rootCause || null,
282
+ mitigationStarted: input.mitigationStarted !== undefined ? !!input.mitigationStarted : null,
283
+ systemsAffected: input.systemsAffected || [],
284
+ affectedClients: input.affectedClients || null,
285
+ economicImpact: input.economicImpact || null,
286
+ geographicScope: input.geographicScope || [],
287
+ durationMs: input.durationMs || null,
288
+ reputationalImpact: input.reputationalImpact || null,
289
+ contactPoint: input.contactPoint || null,
290
+ // Article 19 deadline — operator-side scheduler uses this.
291
+ nextStageDueAt: null,
292
+ };
293
+ if (input.stage === "initial") {
294
+ record.nextStageDueAt = input.detectedAt + INTERMEDIATE_REPORT_DEADLINE_MS;
295
+ } else if (input.stage === "intermediate") {
296
+ record.nextStageDueAt = input.detectedAt + FINAL_REPORT_DEADLINE_MS;
297
+ }
298
+ _emit("dora.incident.reported", {
299
+ metadata: {
300
+ incidentId: record.incidentId,
301
+ classification: record.classification,
302
+ stage: record.stage,
303
+ },
304
+ });
305
+ try { observability().count("dora.incident.reported", 1, {
306
+ classification: record.classification, stage: record.stage,
307
+ }); } catch (_e) { /* obs best-effort */ }
308
+ return record;
309
+ }
310
+
311
+ function draftFinalReport(initialOrIntermediate) {
312
+ if (!initialOrIntermediate || typeof initialOrIntermediate !== "object") {
313
+ throw new DoraError("dora/bad-draft-input",
314
+ "draftFinalReport: input must be a prior report record");
315
+ }
316
+ var draft = Object.assign({}, initialOrIntermediate, {
317
+ stage: "final",
318
+ reportedAt: Date.now(),
319
+ // RTS Article 19(6) final-report shape — operator must fill before
320
+ // submission.
321
+ rootCause: initialOrIntermediate.rootCause || null,
322
+ remediationActions: [],
323
+ lessonsLearned: "",
324
+ preventiveMeasures: [],
325
+ });
326
+ _emit("dora.incident.draftFinal", {
327
+ metadata: { incidentId: draft.incidentId },
328
+ });
329
+ return draft;
330
+ }
331
+
332
+ return {
333
+ classify: classify,
334
+ report: report,
335
+ draftFinalReport: draftFinalReport,
336
+ };
337
+ }
338
+
339
+ module.exports = {
340
+ create: create,
341
+ MAJOR_INCIDENT_THRESHOLDS: MAJOR_INCIDENT_THRESHOLDS,
342
+ SIGNIFICANT_INCIDENT_THRESHOLDS: SIGNIFICANT_INCIDENT_THRESHOLDS,
343
+ INITIAL_REPORT_DEADLINE_MS: INITIAL_REPORT_DEADLINE_MS,
344
+ INTERMEDIATE_REPORT_DEADLINE_MS: INTERMEDIATE_REPORT_DEADLINE_MS,
345
+ FINAL_REPORT_DEADLINE_MS: FINAL_REPORT_DEADLINE_MS,
346
+ DoraError: DoraError,
347
+ };
@@ -249,6 +249,24 @@ var GuardMarkdownError = defineClass("GuardMarkdownError", { alwaysPermane
249
249
  // in addresses, bidi/null/control chars in headers + addresses, header-
250
250
  // folding smuggling, BOM injection. alwaysPermanent.
251
251
  var GuardEmailError = defineClass("GuardEmailError", { alwaysPermanent: true });
252
+ // DoraError covers DORA Article 17 incident-reporting workflow errors
253
+ // (classification refusal, report-shape validation, ESA-template
254
+ // generation, audit-chain integration). Permanent — these are
255
+ // configuration / submission errors, not transient.
256
+ var DoraError = defineClass("DoraError", { alwaysPermanent: true });
257
+ // ComplianceError covers compliance-coordinator misuse: unknown
258
+ // posture name, runtime-switch refusal, assertion failures.
259
+ // Permanent — these are configuration errors, not transient.
260
+ var ComplianceError = defineClass("ComplianceError", { alwaysPermanent: true });
261
+ // SmtpPolicyError covers MTA-STS / DANE / TLS-RPT misuse: bad-policy
262
+ // shape, fetch failures, TLSA-record format errors, missing records.
263
+ // Permanent — these are policy / DNS configuration errors, not
264
+ // transient.
265
+ var SmtpPolicyError = defineClass("SmtpPolicyError", { alwaysPermanent: true });
266
+ // MailAuthError covers SPF / DKIM-verify / DMARC / ARC misuse: bad
267
+ // record shape, fetch failures, missing keys, alignment issues.
268
+ // Permanent — DNS-config / message-shape errors, not transient.
269
+ var MailAuthError = defineClass("MailAuthError", { alwaysPermanent: true });
252
270
 
253
271
  module.exports = {
254
272
  FrameworkError: FrameworkError,
@@ -290,4 +308,8 @@ module.exports = {
290
308
  GuardXmlError: GuardXmlError,
291
309
  GuardMarkdownError: GuardMarkdownError,
292
310
  GuardEmailError: GuardEmailError,
311
+ DoraError: DoraError,
312
+ ComplianceError: ComplianceError,
313
+ SmtpPolicyError: SmtpPolicyError,
314
+ MailAuthError: MailAuthError,
293
315
  };
@@ -66,6 +66,7 @@ var validateOpts = require("./validate-opts");
66
66
  var { GateContractError } = require("./framework-error");
67
67
 
68
68
  var observability = lazyRequire(function () { return require("./observability"); });
69
+ var compliance = lazyRequire(function () { return require("./compliance"); });
69
70
 
70
71
  // Forensic-id token width (bytes); 64 bits is enough for cross-gate
71
72
  // correlation in a single request scope.
@@ -804,12 +805,27 @@ function resolveProfileAndPosture(opts, cfg) {
804
805
  }
805
806
  overlay = cfg.profiles[opts.profile];
806
807
  }
807
- if (typeof opts.compliancePosture === "string") {
808
- if (!cfg.compliancePostures || !cfg.compliancePostures[opts.compliancePosture]) {
808
+ // Compliance-posture resolution operator-supplied opt wins; if not
809
+ // given, fall back to the global posture set via b.compliance.set().
810
+ // The fallback IS the value-add of the top-level coordinator: every
811
+ // primitive with a compliancePosture opt picks up the deployment's
812
+ // declared posture without per-call wiring.
813
+ var posture = opts.compliancePosture;
814
+ if (typeof posture !== "string") {
815
+ var globalPosture;
816
+ try { globalPosture = compliance().current(); }
817
+ catch (_e) { globalPosture = null; }
818
+ if (typeof globalPosture === "string" &&
819
+ cfg.compliancePostures && cfg.compliancePostures[globalPosture]) {
820
+ posture = globalPosture;
821
+ }
822
+ }
823
+ if (typeof posture === "string") {
824
+ if (!cfg.compliancePostures || !cfg.compliancePostures[posture]) {
809
825
  throw ErrorClass.factory(prefix + ".bad-posture",
810
- "unknown compliancePosture " + JSON.stringify(opts.compliancePosture));
826
+ "unknown compliancePosture " + JSON.stringify(posture));
811
827
  }
812
- overlay = Object.assign({}, overlay, cfg.compliancePostures[opts.compliancePosture]);
828
+ overlay = Object.assign({}, overlay, cfg.compliancePostures[posture]);
813
829
  }
814
830
  return Object.assign({}, cfg.defaults || {}, overlay, opts);
815
831
  }