@blamejs/core 0.7.107 → 0.8.4

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 (100) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/NOTICE +17 -1
  3. package/README.md +4 -3
  4. package/index.js +15 -0
  5. package/lib/asyncapi-bindings.js +160 -0
  6. package/lib/asyncapi-traits.js +143 -0
  7. package/lib/asyncapi.js +531 -0
  8. package/lib/audit-sign.js +1 -1
  9. package/lib/audit.js +68 -2
  10. package/lib/auth/acr-vocabulary.js +265 -0
  11. package/lib/auth/auth-time-tracker.js +111 -0
  12. package/lib/auth/elevation-grant.js +306 -0
  13. package/lib/auth/jwt.js +13 -0
  14. package/lib/auth/lockout.js +16 -3
  15. package/lib/auth/oauth.js +15 -1
  16. package/lib/auth/password.js +22 -2
  17. package/lib/auth/sd-jwt-vc-issuer.js +2 -2
  18. package/lib/auth/sd-jwt-vc.js +7 -2
  19. package/lib/auth/step-up-policy.js +335 -0
  20. package/lib/auth/step-up.js +445 -0
  21. package/lib/break-glass.js +53 -14
  22. package/lib/cache-redis.js +1 -1
  23. package/lib/cache.js +6 -1
  24. package/lib/cli.js +3 -3
  25. package/lib/cluster.js +24 -1
  26. package/lib/compliance-ai-act-logging.js +190 -0
  27. package/lib/compliance-ai-act-prohibited.js +205 -0
  28. package/lib/compliance-ai-act-risk.js +189 -0
  29. package/lib/compliance-ai-act-transparency.js +200 -0
  30. package/lib/compliance-ai-act.js +558 -0
  31. package/lib/compliance.js +12 -2
  32. package/lib/config-drift.js +2 -2
  33. package/lib/crypto-field.js +21 -1
  34. package/lib/crypto.js +114 -1
  35. package/lib/db.js +35 -4
  36. package/lib/dev.js +30 -3
  37. package/lib/dual-control.js +19 -1
  38. package/lib/external-db.js +10 -0
  39. package/lib/file-upload.js +30 -3
  40. package/lib/flag-cache.js +136 -0
  41. package/lib/flag-evaluation-context.js +135 -0
  42. package/lib/flag-providers.js +279 -0
  43. package/lib/flag-targeting.js +210 -0
  44. package/lib/flag.js +284 -0
  45. package/lib/guard-all.js +33 -16
  46. package/lib/guard-csv.js +16 -2
  47. package/lib/guard-html.js +35 -0
  48. package/lib/guard-svg.js +20 -0
  49. package/lib/http-client.js +57 -11
  50. package/lib/inbox.js +391 -0
  51. package/lib/log-stream-syslog.js +8 -0
  52. package/lib/log-stream.js +1 -1
  53. package/lib/mail-arc-sign.js +372 -0
  54. package/lib/mail-auth.js +2 -0
  55. package/lib/mail.js +40 -0
  56. package/lib/middleware/ai-act-disclosure.js +166 -0
  57. package/lib/middleware/asyncapi-serve.js +136 -0
  58. package/lib/middleware/attach-user.js +25 -2
  59. package/lib/middleware/bearer-auth.js +71 -6
  60. package/lib/middleware/body-parser.js +13 -0
  61. package/lib/middleware/cors.js +10 -0
  62. package/lib/middleware/csrf-protect.js +34 -3
  63. package/lib/middleware/dpop.js +3 -3
  64. package/lib/middleware/flag-context.js +76 -0
  65. package/lib/middleware/host-allowlist.js +1 -1
  66. package/lib/middleware/index.js +15 -0
  67. package/lib/middleware/openapi-serve.js +143 -0
  68. package/lib/middleware/require-aal.js +2 -2
  69. package/lib/middleware/require-step-up.js +186 -0
  70. package/lib/middleware/trace-propagate.js +1 -1
  71. package/lib/mtls-ca.js +23 -29
  72. package/lib/mtls-engine-default.js +21 -1
  73. package/lib/network-tls.js +21 -6
  74. package/lib/object-store/sigv4-bucket-ops.js +41 -0
  75. package/lib/observability-otlp-exporter.js +35 -2
  76. package/lib/openapi-paths-builder.js +248 -0
  77. package/lib/openapi-schema-walk.js +192 -0
  78. package/lib/openapi-security.js +169 -0
  79. package/lib/openapi-yaml.js +154 -0
  80. package/lib/openapi.js +443 -0
  81. package/lib/outbox.js +3 -3
  82. package/lib/permissions.js +10 -1
  83. package/lib/pqc-agent.js +22 -1
  84. package/lib/pqc-software.js +195 -0
  85. package/lib/pubsub.js +8 -4
  86. package/lib/redact.js +26 -1
  87. package/lib/retention.js +26 -0
  88. package/lib/router.js +1 -0
  89. package/lib/scheduler.js +57 -1
  90. package/lib/session.js +3 -3
  91. package/lib/ssrf-guard.js +19 -4
  92. package/lib/static.js +12 -0
  93. package/lib/totp.js +16 -0
  94. package/lib/vault/index.js +3 -0
  95. package/lib/vault-aad.js +259 -0
  96. package/lib/vendor/MANIFEST.json +29 -0
  97. package/lib/vendor/noble-post-quantum.cjs +18 -0
  98. package/lib/ws-client.js +978 -0
  99. package/package.json +1 -1
  100. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,372 @@
1
+ "use strict";
2
+ /**
3
+ * b.mail.arc.sign — RFC 8617 ARC chain construction.
4
+ *
5
+ * Companion to b.mail.arc.verify (relay-side ARC chain validation).
6
+ * When a relay receives a message with optional prior ARC headers
7
+ * and is about to forward it, it signs and prepends three new
8
+ * headers per RFC 8617 §5.1:
9
+ *
10
+ * - ARC-Authentication-Results (AAR) — relay's verification verdict
11
+ * for SPF / DKIM / DMARC at this hop, formatted per RFC 8601.
12
+ * - ARC-Message-Signature (AMS) — DKIM-style signature over the
13
+ * message body + selected headers + the AAR.
14
+ * - ARC-Seal (AS) — signature over the catenation of every prior
15
+ * hop's three headers plus the current AAR + AMS, with cv= tag
16
+ * reporting the upstream chain validation outcome.
17
+ *
18
+ * The operator's verification result lives in opts.authResults; the
19
+ * cv= self-attestation comes from opts.cv (typically the result.cv
20
+ * of a prior arc.verify() call: "none" at i=1, "pass" / "fail" at
21
+ * i>=2).
22
+ *
23
+ * var signed = b.mail.arc.sign({
24
+ * rfc822: message,
25
+ * instance: i, // 1, 2, 3, ...
26
+ * authservId: "relay.example.com",
27
+ * domain: "relay.example.com",
28
+ * selector: "arc",
29
+ * privateKey: pem,
30
+ * algorithm: "rsa-sha256",
31
+ * cv: "none", // i=1: none; i>=2: pass / fail
32
+ * authResults: "spf=pass smtp.mailfrom=...",
33
+ * headersToSign: ["From", "To", "Subject", "Date", "Message-ID"],
34
+ * timestamp: Math.floor(Date.now() / 1000),
35
+ * });
36
+ * // signed.aar, signed.ams, signed.as → strings
37
+ * // signed.rfc822 → message with all three headers prepended
38
+ *
39
+ * Per RFC 8617:
40
+ * - i=1: cv=none REQUIRED.
41
+ * - i>=2: cv=pass | cv=fail; cv=none is invalid at i>=2.
42
+ * - Once any hop's AS reports cv=fail, no downstream hop may sign
43
+ * a cv=pass — the chain is permanently broken. Signers MUST
44
+ * report cv=fail when forwarding such a chain.
45
+ *
46
+ * Per the framework's validation-tier policy: sign() throws on bad
47
+ * input (config-time entry-point). Audit emissions on every signed
48
+ * hop: `dkim.arc.signed`.
49
+ */
50
+
51
+ var nodeCrypto = require("crypto");
52
+ var lazyRequire = require("./lazy-require");
53
+ var validateOpts = require("./validate-opts");
54
+ var safeBuffer = require("./safe-buffer");
55
+ var { defineClass } = require("./framework-error");
56
+
57
+ var MailAuthError = defineClass("MailAuthError", { alwaysPermanent: true });
58
+
59
+ var audit = lazyRequire(function () { return require("./audit"); });
60
+
61
+ var ALLOWED_ALGORITHMS = ["rsa-sha256", "ed25519-sha256"];
62
+ var ALLOWED_CV = ["none", "pass", "fail"];
63
+ var DEFAULT_HEADERS = ["From", "To", "Subject", "Date", "Message-ID",
64
+ "MIME-Version", "Content-Type"];
65
+
66
+ function _splitHeadersBody(rfc822) {
67
+ var idx = rfc822.indexOf("\r\n\r\n");
68
+ if (idx === -1) {
69
+ var lfIdx = rfc822.indexOf("\n\n");
70
+ if (lfIdx === -1) {
71
+ throw new MailAuthError("arc-sign/bad-rfc822",
72
+ "rfc822 body has no header/body separator (CRLF-CRLF or LF-LF)");
73
+ }
74
+ return { headers: rfc822.substring(0, lfIdx), body: rfc822.substring(lfIdx + 2) };
75
+ }
76
+ return { headers: rfc822.substring(0, idx), body: rfc822.substring(idx + 4) };
77
+ }
78
+
79
+ function _parseHeaderBlock(headerBlock) {
80
+ // Returns array of { name, value } in source order. Folds CRLF+WSP
81
+ // continuations.
82
+ var lines = headerBlock.split(/\r?\n/);
83
+ var headers = [];
84
+ var current = null;
85
+ for (var i = 0; i < lines.length; i += 1) {
86
+ var line = lines[i];
87
+ if (line.length === 0) continue;
88
+ if (line.charAt(0) === " " || line.charAt(0) === "\t") {
89
+ if (current) current.value += "\r\n" + line;
90
+ continue;
91
+ }
92
+ var colonIdx = line.indexOf(":");
93
+ if (colonIdx === -1) continue;
94
+ if (current) headers.push(current);
95
+ current = {
96
+ name: line.slice(0, colonIdx),
97
+ value: line.slice(colonIdx + 1).replace(/^[ \t]+/, ""), // allow:duplicate-regex — RFC 5322 leading-WSP strip; identical to mail-auth/mail-dkim by spec
98
+ };
99
+ }
100
+ if (current) headers.push(current);
101
+ return headers;
102
+ }
103
+
104
+ function _canonRelaxedHeader(name, value) {
105
+ var unfolded = String(value).replace(/\r?\n[ \t]+/g, " "); // allow:duplicate-regex allow:raw-byte-literal — DKIM/ARC RFC 6376 §3.4.2 unfolding
106
+ var trimmed = unfolded.replace(/[ \t]+/g, " ").replace(/^[ \t]+|[ \t]+$/g, ""); // allow:duplicate-regex allow:raw-byte-literal — DKIM/ARC RFC 6376 §3.4.2 WSP collapse
107
+ return name.toLowerCase() + ":" + trimmed + "\r\n";
108
+ }
109
+
110
+ function _canonRelaxedBody(body) {
111
+ // Relaxed body canon: collapse runs of WSP within lines, strip
112
+ // trailing WSP, remove all trailing empty lines, append single CRLF
113
+ // unless body is empty.
114
+ if (body.length === 0) return "";
115
+ var normalized = body.replace(/\r?\n/g, "\r\n");
116
+ var collapsed = normalized.replace(/[ \t]+/g, " ").replace(/[ \t]+\r\n/g, "\r\n");
117
+ collapsed = collapsed.replace(/(\r\n)+$/, "");
118
+ return collapsed + "\r\n";
119
+ }
120
+
121
+ function _bodyHashB64(body, algorithm) {
122
+ var hashAlgo = algorithm.indexOf("sha256") !== -1 ? "sha256" : "sha512";
123
+ var canonical = _canonRelaxedBody(body);
124
+ return nodeCrypto.createHash(hashAlgo).update(canonical).digest("base64");
125
+ }
126
+
127
+ function _arcExtractPriorHops(parsedHeaders) {
128
+ // Walk parsedHeaders; for each ARC-Authentication-Results /
129
+ // ARC-Message-Signature / ARC-Seal entry, extract instance via i=N
130
+ // and group by hop.
131
+ var hopMap = {};
132
+ for (var i = 0; i < parsedHeaders.length; i += 1) {
133
+ var h = parsedHeaders[i];
134
+ var lcName = h.name.toLowerCase();
135
+ if (lcName !== "arc-authentication-results" &&
136
+ lcName !== "arc-message-signature" &&
137
+ lcName !== "arc-seal") continue;
138
+ var iMatch = h.value.match(/(?:^|[;,\s])i=(\d+)/); // allow:regex-no-length-cap — ARC header bounded by RFC 5322 §2.1.1
139
+ if (!iMatch) continue;
140
+ var instance = parseInt(iMatch[1], 10);
141
+ if (!hopMap[instance]) hopMap[instance] = { instance: instance };
142
+ hopMap[instance][lcName] = h.value;
143
+ }
144
+ var hops = [];
145
+ var keys = Object.keys(hopMap).sort(function (a, b) { return Number(a) - Number(b); });
146
+ for (var k = 0; k < keys.length; k += 1) hops.push(hopMap[keys[k]]);
147
+ return hops;
148
+ }
149
+
150
+ function sign(opts) {
151
+ opts = opts || {};
152
+ validateOpts(opts, [
153
+ "rfc822", "instance", "authservId", "domain", "selector",
154
+ "privateKey", "algorithm", "cv", "authResults",
155
+ "headersToSign", "timestamp", "audit",
156
+ ], "mail.arc.sign");
157
+
158
+ validateOpts.requireNonEmptyString(opts.rfc822, "sign: rfc822",
159
+ MailAuthError, "arc-sign/bad-input");
160
+ if (typeof opts.instance !== "number" || !isFinite(opts.instance) ||
161
+ opts.instance < 1 || opts.instance > 50 || // allow:raw-byte-literal — RFC 8617 §5 chain bound
162
+ Math.floor(opts.instance) !== opts.instance) {
163
+ throw new MailAuthError("arc-sign/bad-instance",
164
+ "sign: instance must be an integer in [1, 50] — got " + JSON.stringify(opts.instance));
165
+ }
166
+ validateOpts.requireNonEmptyString(opts.authservId,
167
+ "sign: authservId", MailAuthError, "arc-sign/bad-authserv");
168
+ validateOpts.requireNonEmptyString(opts.domain,
169
+ "sign: domain", MailAuthError, "arc-sign/bad-domain");
170
+ validateOpts.requireNonEmptyString(opts.selector,
171
+ "sign: selector", MailAuthError, "arc-sign/bad-selector");
172
+ if (!opts.privateKey || (typeof opts.privateKey !== "string" &&
173
+ typeof opts.privateKey !== "object")) {
174
+ throw new MailAuthError("arc-sign/missing-private-key",
175
+ "sign: privateKey is required (PEM string or crypto.KeyObject)");
176
+ }
177
+ var algorithm = opts.algorithm || "rsa-sha256";
178
+ if (ALLOWED_ALGORITHMS.indexOf(algorithm) === -1) {
179
+ throw new MailAuthError("arc-sign/bad-algorithm",
180
+ "sign: algorithm must be one of " + ALLOWED_ALGORITHMS.join(", "));
181
+ }
182
+ if (ALLOWED_CV.indexOf(opts.cv) === -1) {
183
+ throw new MailAuthError("arc-sign/bad-cv",
184
+ "sign: cv must be one of " + ALLOWED_CV.join(", ") + " — got " + JSON.stringify(opts.cv));
185
+ }
186
+ if (opts.instance === 1 && opts.cv !== "none") {
187
+ throw new MailAuthError("arc-sign/cv-rule",
188
+ "sign: i=1 requires cv=none (per RFC 8617 §5.1.1)");
189
+ }
190
+ if (opts.instance >= 2 && opts.cv === "none") { // allow:raw-byte-literal — RFC 8617 chain rule
191
+ throw new MailAuthError("arc-sign/cv-rule",
192
+ "sign: i>=2 disallows cv=none — must be cv=pass or cv=fail (per RFC 8617 §5.1.1)");
193
+ }
194
+ validateOpts.requireNonEmptyString(opts.authResults, "sign: authResults",
195
+ MailAuthError, "arc-sign/bad-auth-results");
196
+ if (safeBuffer.hasCrlf(opts.authResults)) {
197
+ throw new MailAuthError("arc-sign/bad-auth-results",
198
+ "sign: authResults contains CR/LF (header injection refused)");
199
+ }
200
+ var headersToSign = opts.headersToSign || DEFAULT_HEADERS;
201
+ if (!Array.isArray(headersToSign) || headersToSign.length === 0) {
202
+ throw new MailAuthError("arc-sign/bad-headers",
203
+ "sign: headersToSign must be a non-empty array of header names");
204
+ }
205
+ for (var hi = 0; hi < headersToSign.length; hi += 1) {
206
+ if (typeof headersToSign[hi] !== "string" || headersToSign[hi].length === 0) {
207
+ throw new MailAuthError("arc-sign/bad-headers",
208
+ "sign: headersToSign[" + hi + "] must be a non-empty string");
209
+ }
210
+ }
211
+ var timestamp = (typeof opts.timestamp === "number" && opts.timestamp > 0) // allow:numeric-opt-Infinity
212
+ ? Math.floor(opts.timestamp) : Math.floor(Date.now() / 1000); // allow:raw-byte-literal — Unix epoch seconds divisor
213
+ var auditOn = opts.audit !== false;
214
+
215
+ var keyObject;
216
+ try {
217
+ keyObject = (typeof opts.privateKey === "string" || Buffer.isBuffer(opts.privateKey))
218
+ ? nodeCrypto.createPrivateKey({ key: opts.privateKey, format: "pem" })
219
+ : opts.privateKey;
220
+ } catch (e) {
221
+ throw new MailAuthError("arc-sign/bad-private-key",
222
+ "sign: privateKey could not be parsed: " + ((e && e.message) || String(e)));
223
+ }
224
+
225
+ var split = _splitHeadersBody(opts.rfc822);
226
+ var parsedHeaders = _parseHeaderBlock(split.headers);
227
+ var priorHops = _arcExtractPriorHops(parsedHeaders);
228
+
229
+ // Validate prior chain's instance numbering: hops must be 1..N-1
230
+ // contiguous, where N is opts.instance.
231
+ for (var ph = 0; ph < priorHops.length; ph += 1) {
232
+ if (priorHops[ph].instance !== ph + 1) {
233
+ throw new MailAuthError("arc-sign/chain-broken",
234
+ "sign: prior chain has gap or mismatch — expected i=" + (ph + 1) +
235
+ " at slot " + ph + ", got i=" + priorHops[ph].instance);
236
+ }
237
+ }
238
+ if (priorHops.length !== opts.instance - 1) {
239
+ throw new MailAuthError("arc-sign/chain-broken",
240
+ "sign: prior chain has " + priorHops.length + " hops but instance=" +
241
+ opts.instance + " requires " + (opts.instance - 1) + " prior hops");
242
+ }
243
+
244
+ var bh = _bodyHashB64(split.body, algorithm);
245
+
246
+ // ----- AAR (ARC-Authentication-Results) -----
247
+ // RFC 8617 §4.1.1 — `i=N; <auth-result-string>`.
248
+ var aarValue = "i=" + opts.instance + "; " + opts.authservId + "; " + opts.authResults;
249
+
250
+ // ----- AMS (ARC-Message-Signature) -----
251
+ // Looks like DKIM-Signature with `i=N` tag added.
252
+ // Tags in canonical order: i, a, c, d, s, t, h, bh, b
253
+ var amsTags = [
254
+ "i=" + opts.instance,
255
+ "a=" + algorithm,
256
+ "c=relaxed/relaxed",
257
+ "d=" + opts.domain,
258
+ "s=" + opts.selector,
259
+ "t=" + timestamp,
260
+ "h=" + headersToSign.join(":"),
261
+ "bh=" + bh,
262
+ ];
263
+ amsTags.push("b=");
264
+ var amsUnsigned = amsTags.join("; ");
265
+
266
+ var canonHeaders = "";
267
+ var headerNamesLc = parsedHeaders.map(function (h) { return h.name.toLowerCase(); });
268
+ for (var j = 0; j < headersToSign.length; j += 1) {
269
+ var wantLc = headersToSign[j].toLowerCase();
270
+ var idx = -1;
271
+ for (var k = 0; k < headerNamesLc.length; k += 1) {
272
+ if (headerNamesLc[k] === wantLc) idx = k;
273
+ }
274
+ if (idx === -1) continue;
275
+ var h = parsedHeaders[idx];
276
+ canonHeaders += _canonRelaxedHeader(h.name, h.value);
277
+ }
278
+ // Per RFC 8617 §5.1 — include the AAR for the current hop in the
279
+ // AMS canonicalization stream when h= covers it. Per AMS spec, h=
280
+ // typically does NOT include the AAR (it's a *prior* hop's
281
+ // attestation), so the canonical input is (h-listed headers) +
282
+ // (AMS with empty b=).
283
+ var amsCanonInput = canonHeaders +
284
+ _canonRelaxedHeader("ARC-Message-Signature", amsUnsigned).replace(/\r\n$/, "");
285
+
286
+ var amsSignatureB64 = _signOne(amsCanonInput, keyObject, algorithm);
287
+ var amsValue = amsUnsigned.replace(/\bb=$/, "b=" + amsSignatureB64);
288
+
289
+ // ----- AS (ARC-Seal) -----
290
+ // Tags: i, a, t, cv, d, s, b
291
+ var asTags = [
292
+ "i=" + opts.instance,
293
+ "a=" + algorithm,
294
+ "t=" + timestamp,
295
+ "cv=" + opts.cv,
296
+ "d=" + opts.domain,
297
+ "s=" + opts.selector,
298
+ ];
299
+ asTags.push("b=");
300
+ var asUnsigned = asTags.join("; ");
301
+
302
+ // AS canonical input: every prior hop's AAR + AMS + AS in instance
303
+ // order, then current AAR + AMS, then current AS with empty b=.
304
+ var asCanonInput = "";
305
+ for (var p = 0; p < priorHops.length; p += 1) {
306
+ var hop = priorHops[p];
307
+ asCanonInput += _canonRelaxedHeader("ARC-Authentication-Results", hop["arc-authentication-results"]);
308
+ asCanonInput += _canonRelaxedHeader("ARC-Message-Signature", hop["arc-message-signature"]);
309
+ asCanonInput += _canonRelaxedHeader("ARC-Seal", hop["arc-seal"]);
310
+ }
311
+ asCanonInput += _canonRelaxedHeader("ARC-Authentication-Results", aarValue);
312
+ asCanonInput += _canonRelaxedHeader("ARC-Message-Signature", amsValue);
313
+ asCanonInput += _canonRelaxedHeader("ARC-Seal", asUnsigned).replace(/\r\n$/, "");
314
+
315
+ var asSignatureB64 = _signOne(asCanonInput, keyObject, algorithm);
316
+ var asValue = asUnsigned.replace(/\bb=$/, "b=" + asSignatureB64);
317
+
318
+ // Prepend headers in the RFC-recommended order: AS, AMS, AAR.
319
+ var prependedHeaders =
320
+ "ARC-Seal: " + asValue + "\r\n" +
321
+ "ARC-Message-Signature: " + amsValue + "\r\n" +
322
+ "ARC-Authentication-Results: " + aarValue + "\r\n";
323
+ var sealedRfc822 = prependedHeaders + opts.rfc822;
324
+
325
+ if (auditOn) {
326
+ try {
327
+ audit().safeEmit({
328
+ action: "dkim.arc.signed",
329
+ outcome: "success",
330
+ actor: null,
331
+ metadata: {
332
+ instance: opts.instance,
333
+ domain: opts.domain,
334
+ selector: opts.selector,
335
+ algorithm: algorithm,
336
+ cv: opts.cv,
337
+ priorHops: priorHops.length,
338
+ },
339
+ });
340
+ } catch (_e) { /* drop-silent */ }
341
+ }
342
+
343
+ return {
344
+ aar: aarValue,
345
+ ams: amsValue,
346
+ as: asValue,
347
+ rfc822: sealedRfc822,
348
+ instance: opts.instance,
349
+ cv: opts.cv,
350
+ };
351
+ }
352
+
353
+ function _signOne(canonInput, keyObject, algorithm) {
354
+ if (algorithm === "ed25519-sha256") {
355
+ // Ed25519 prehash variant — Node's `crypto.sign(null, msg, key)`
356
+ // accepts the message directly.
357
+ return nodeCrypto.sign(null, Buffer.from(canonInput, "utf8"), keyObject)
358
+ .toString("base64");
359
+ }
360
+ // RSA-SHA256 / default — createSign + update + sign.
361
+ var signer = nodeCrypto.createSign("RSA-SHA256");
362
+ signer.update(canonInput);
363
+ return signer.sign(keyObject).toString("base64");
364
+ }
365
+
366
+ module.exports = {
367
+ sign: sign,
368
+ ALLOWED_CV: ALLOWED_CV,
369
+ ALLOWED_ALGORITHMS: ALLOWED_ALGORITHMS,
370
+ DEFAULT_HEADERS: DEFAULT_HEADERS,
371
+ MailAuthError: MailAuthError,
372
+ };
package/lib/mail-auth.js CHANGED
@@ -1106,6 +1106,8 @@ module.exports = {
1106
1106
  arc: Object.freeze({
1107
1107
  verify: arcVerify,
1108
1108
  evaluate: arcEvaluate,
1109
+ sign: require("./mail-arc-sign").sign, // allow:inline-require — re-export from sibling module
1110
+ ALLOWED_CV: require("./mail-arc-sign").ALLOWED_CV, // allow:inline-require — re-export from sibling module
1109
1111
  }),
1110
1112
  authResults: Object.freeze({
1111
1113
  emit: authResultsEmit,
package/lib/mail.js CHANGED
@@ -69,6 +69,8 @@ var safeBuffer = require("./safe-buffer");
69
69
  var audit = lazyRequire(function () { return require("./audit"); });
70
70
  var httpClient = lazyRequire(function () { return require("./http-client"); });
71
71
  var guardEmail = lazyRequire(function () { return require("./guard-email"); });
72
+ var guardFilename = lazyRequire(function () { return require("./guard-filename"); });
73
+ var fileType = lazyRequire(function () { return require("./file-type"); });
72
74
  var mailDkim = require("./mail-dkim");
73
75
  var mailAuth = require("./mail-auth");
74
76
  var mailBimi = require("./mail-bimi");
@@ -285,6 +287,44 @@ function _validateMessage(message) {
285
287
  throw new MailError("mail/invalid-attachment",
286
288
  "attachments[" + i + "].filename contains forbidden control characters", true);
287
289
  }
290
+ // Filename safety gate — path traversal / null-byte / NTFS ADS /
291
+ // RTLO bidi / Windows-reserved / overlong UTF-8 / shell-exec
292
+ // / double-extension. Without this, an operator forwarding a
293
+ // user-uploaded attachment passes attacker-controlled filenames
294
+ // straight to mail clients (which use the filename for "save
295
+ // as" prompts) where Excel + macOS Finder + Outlook honor the
296
+ // RTLO + reserved-name + Windows-strip semantics.
297
+ if (att.skipFilenameSafety !== true) {
298
+ var fnResult = guardFilename().validate(att.filename, { profile: "strict" });
299
+ if (!fnResult.ok) {
300
+ throw new MailError("mail/invalid-attachment",
301
+ "attachments[" + i + "].filename rejected by guardFilename: " +
302
+ (fnResult.issues && fnResult.issues[0] && fnResult.issues[0].kind || "filename-safety-fail"),
303
+ true);
304
+ }
305
+ }
306
+ // Magic-byte gate — refuse claimed/detected MIME mismatch when
307
+ // both are present. Operator can opt out per-attachment with
308
+ // `skipMagicByteCheck: true` and audited reason (e.g. encrypted
309
+ // payloads where the magic bytes intentionally don't match the
310
+ // claimed type).
311
+ if (att.skipMagicByteCheck !== true && att.contentType &&
312
+ Buffer.isBuffer(att.content)) {
313
+ try {
314
+ var detected = fileType().detect(att.content);
315
+ if (detected && detected.mime &&
316
+ detected.mime.split("/")[0] !==
317
+ att.contentType.split(";")[0].trim().toLowerCase().split("/")[0]) {
318
+ throw new MailError("mail/invalid-attachment",
319
+ "attachments[" + i + "].contentType '" + att.contentType +
320
+ "' disagrees with detected magic-byte MIME '" + detected.mime +
321
+ "' — refusing to send mis-typed attachment", true);
322
+ }
323
+ } catch (e) {
324
+ if (e && e.code === "mail/invalid-attachment") throw e;
325
+ // file-type detection error: drop-silent, treat as no-detection
326
+ }
327
+ }
288
328
  if (att.content === undefined || att.content === null) {
289
329
  throw new MailError("mail/invalid-attachment",
290
330
  "attachments[" + i + "].content is required (Buffer or string)", true);
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ /**
3
+ * ai-act-disclosure middleware — auto-inject the EU AI Act Article 50
4
+ * disclosure banner / meta tags into outgoing HTML responses.
5
+ *
6
+ * var disclose = b.middleware.aiActDisclosure({
7
+ * kind: "ai-interaction",
8
+ * deployerName: "myco",
9
+ * policyUri: "https://myco.example.com/ai-policy",
10
+ * });
11
+ * router.use(disclose);
12
+ *
13
+ * Two integration modes:
14
+ *
15
+ * - "header" (default) — adds the AI-Act-Notice + AI-Act-Article
16
+ * response headers. Cheapest; works for JSON
17
+ * APIs as well as HTML.
18
+ *
19
+ * - "html" — when the response Content-Type is HTML,
20
+ * injects a <div role="status" ...> banner
21
+ * immediately after the <body> tag plus a
22
+ * <meta> tag inside <head>. Skipped when
23
+ * response is already past headers OR not
24
+ * text/html.
25
+ *
26
+ * The middleware does NOT alter the response when:
27
+ * - response status >= 400 (operator's error pages stay clean)
28
+ * - response is a redirect (3xx)
29
+ * - operator has set the X-Skip-AI-Act header on the request
30
+ * (test fixtures, internal-traffic carve-out)
31
+ * - per-request opt-out via res.locals.aiActSkip = true
32
+ *
33
+ * Audit emission: `compliance.aiact.disclosed` on every successful
34
+ * injection. Operators with high-volume traffic can disable via
35
+ * `audit: false`.
36
+ */
37
+
38
+ var lazyRequire = require("../lazy-require");
39
+ var validateOpts = require("../validate-opts");
40
+ var requestHelpers = require("../request-helpers");
41
+
42
+ var aiActMod = lazyRequire(function () { return require("../compliance-ai-act"); });
43
+ var audit = lazyRequire(function () { return require("../audit"); });
44
+
45
+ function create(opts) {
46
+ opts = opts || {};
47
+ validateOpts(opts, [
48
+ "kind", "deployerName", "policyUri", "mode",
49
+ "audit", "lang", "skipHeader",
50
+ ], "middleware.aiActDisclosure");
51
+
52
+ var mode = (opts.mode === "html") ? "html" : "header";
53
+ // Pre-validate kind via the transparency catalog.
54
+ var probe = aiActMod().transparency.banner({
55
+ kind: opts.kind || "ai-interaction",
56
+ lang: opts.lang || "en",
57
+ });
58
+ // probe throws if kind is bad — operator catches at boot.
59
+ void probe;
60
+
61
+ var auditOn = opts.audit !== false;
62
+ var skipHeader = (typeof opts.skipHeader === "string" && opts.skipHeader.length > 0)
63
+ ? opts.skipHeader.toLowerCase()
64
+ : "x-skip-ai-act";
65
+
66
+ return function aiActDisclosureMiddleware(req, res, next) {
67
+ var headers = req.headers || {};
68
+ if (headers[skipHeader] != null) return next();
69
+
70
+ // Wrap response.writeHead so we can set headers + decide html mode.
71
+ var origWriteHead = res.writeHead;
72
+ var origEnd = res.end;
73
+ var injected = false;
74
+
75
+ res.writeHead = function (status, headersOrReason, headersMaybe) {
76
+ // Only inject for 2xx HTML or any 2xx for header mode.
77
+ if (typeof status !== "number" || status < 200 || status >= 300) {
78
+ return origWriteHead.apply(res, arguments);
79
+ }
80
+ if (res.locals && res.locals.aiActSkip === true) {
81
+ return origWriteHead.apply(res, arguments);
82
+ }
83
+ var article = _articleFor(opts.kind || "ai-interaction");
84
+ _setHeader(res, "AI-Act-Notice", opts.kind || "ai-interaction");
85
+ _setHeader(res, "AI-Act-Article", article);
86
+ if (typeof opts.policyUri === "string" && opts.policyUri.length > 0) {
87
+ _setHeader(res, "AI-Act-Policy", opts.policyUri);
88
+ }
89
+ injected = true;
90
+ return origWriteHead.apply(res, arguments);
91
+ };
92
+
93
+ if (mode === "html") {
94
+ res.end = function (chunk, encoding) {
95
+ try {
96
+ var ctype = (res.getHeader && res.getHeader("Content-Type")) || "";
97
+ if (typeof ctype === "string" && ctype.indexOf("text/html") !== -1 &&
98
+ chunk && Buffer.isBuffer(chunk) === false &&
99
+ typeof chunk === "string") {
100
+ var bannerHtml = aiActMod().transparency.htmlBanner({
101
+ kind: opts.kind || "ai-interaction",
102
+ lang: opts.lang || "en",
103
+ });
104
+ // Inject after <body> if present, else prepend.
105
+ var bodyOpen = chunk.indexOf("<body");
106
+ if (bodyOpen !== -1) {
107
+ var afterTag = chunk.indexOf(">", bodyOpen);
108
+ if (afterTag !== -1) {
109
+ chunk = chunk.slice(0, afterTag + 1) + bannerHtml + chunk.slice(afterTag + 1);
110
+ }
111
+ } else {
112
+ chunk = bannerHtml + chunk;
113
+ }
114
+ }
115
+ } catch (_e) { /* injection best-effort */ }
116
+ return origEnd.apply(res, [chunk, encoding]);
117
+ };
118
+ }
119
+
120
+ if (auditOn) {
121
+ res.on("close", function () {
122
+ if (!injected) return;
123
+ try {
124
+ audit().safeEmit({
125
+ action: "compliance.aiact.disclosed",
126
+ outcome: "success",
127
+ actor: {
128
+ clientIp: requestHelpers.clientIp(req),
129
+ path: req.url || null,
130
+ },
131
+ metadata: {
132
+ kind: opts.kind || "ai-interaction",
133
+ mode: mode,
134
+ deployerName: opts.deployerName || null,
135
+ },
136
+ });
137
+ } catch (_e) { /* drop-silent */ }
138
+ });
139
+ }
140
+
141
+ return next();
142
+ };
143
+ }
144
+
145
+ function _articleFor(kind) {
146
+ switch (kind) {
147
+ case "ai-interaction": return "Art. 50(1)";
148
+ case "ai-generated-content": return "Art. 50(2)";
149
+ case "emotion-recognition": return "Art. 50(3)";
150
+ case "biometric-categorisation": return "Art. 50(3)";
151
+ case "deep-fake": return "Art. 50(4)";
152
+ case "ai-text-public-interest": return "Art. 50(4)";
153
+ default: return null;
154
+ }
155
+ }
156
+
157
+ function _setHeader(res, name, value) {
158
+ if (typeof res.setHeader === "function") {
159
+ res.setHeader(name, value);
160
+ return;
161
+ }
162
+ res._headers = res._headers || {};
163
+ res._headers[name] = value;
164
+ }
165
+
166
+ module.exports = { create: create };