@blamejs/core 0.7.106 → 0.8.0
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 +19 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +16 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit.js +6 -0
- package/lib/auth/acr-vocabulary.js +265 -0
- package/lib/auth/auth-time-tracker.js +111 -0
- package/lib/auth/elevation-grant.js +306 -0
- package/lib/auth/sd-jwt-vc-disclosure.js +95 -0
- package/lib/auth/sd-jwt-vc-holder.js +203 -0
- package/lib/auth/sd-jwt-vc-issuer.js +197 -0
- package/lib/auth/sd-jwt-vc.js +526 -0
- package/lib/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- package/lib/compliance-ai-act-logging.js +186 -0
- package/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/compliance-ai-act-risk.js +189 -0
- package/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/compliance-ai-act.js +558 -0
- package/lib/compliance.js +2 -0
- package/lib/crypto.js +32 -0
- package/lib/flag-cache.js +136 -0
- package/lib/flag-evaluation-context.js +135 -0
- package/lib/flag-providers.js +279 -0
- package/lib/flag-targeting.js +210 -0
- package/lib/flag.js +284 -0
- package/lib/inbox.js +367 -0
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -0
- package/lib/middleware/flag-context.js +76 -0
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-step-up.js +186 -0
- package/lib/openapi-paths-builder.js +248 -0
- package/lib/openapi-schema-walk.js +192 -0
- package/lib/openapi-security.js +169 -0
- package/lib/openapi-yaml.js +154 -0
- package/lib/openapi.js +443 -0
- package/lib/pqc-software.js +195 -0
- package/lib/vault/index.js +3 -0
- package/lib/vault-aad.js +259 -0
- package/lib/vendor/MANIFEST.json +29 -0
- package/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/ws-client.js +829 -0
- package/package.json +1 -1
- 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,
|
|
@@ -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 };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* asyncapi-serve middleware — expose an AsyncAPI 3.0 document at a
|
|
4
|
+
* mount point.
|
|
5
|
+
*
|
|
6
|
+
* var aapi = b.asyncapi.create({ ... });
|
|
7
|
+
* ...add channels / operations / schemas / security...
|
|
8
|
+
*
|
|
9
|
+
* router.use(b.middleware.asyncapiServe({
|
|
10
|
+
* document: aapi,
|
|
11
|
+
* pathJson: "/asyncapi.json",
|
|
12
|
+
* pathYaml: "/asyncapi.yaml",
|
|
13
|
+
* pretty: true,
|
|
14
|
+
* accessControl: "public",
|
|
15
|
+
* }));
|
|
16
|
+
*
|
|
17
|
+
* Behaviour matches openapiServe: GET / HEAD only, SHA3-512 ETag with
|
|
18
|
+
* conditional 304, configurable CORS gate, falls through on other
|
|
19
|
+
* paths / methods.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
var nodeCrypto = require("crypto");
|
|
23
|
+
var validateOpts = require("../validate-opts");
|
|
24
|
+
var lazyRequire = require("../lazy-require");
|
|
25
|
+
var { defineClass } = require("../framework-error");
|
|
26
|
+
var AsyncApiError = defineClass("AsyncApiError", { alwaysPermanent: true });
|
|
27
|
+
|
|
28
|
+
var openapiYaml = lazyRequire(function () { return require("../openapi-yaml"); });
|
|
29
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
30
|
+
|
|
31
|
+
function create(opts) {
|
|
32
|
+
opts = opts || {};
|
|
33
|
+
validateOpts(opts, [
|
|
34
|
+
"document", "pathJson", "pathYaml", "pretty",
|
|
35
|
+
"cacheControl", "accessControl", "audit",
|
|
36
|
+
], "middleware.asyncapiServe");
|
|
37
|
+
if (!opts.document || typeof opts.document.toJson !== "function") {
|
|
38
|
+
throw new AsyncApiError("asyncapi/bad-document",
|
|
39
|
+
"asyncapiServe: document must be a builder created via b.asyncapi.create()");
|
|
40
|
+
}
|
|
41
|
+
var pathJson = opts.pathJson || "/asyncapi.json";
|
|
42
|
+
var pathYaml = opts.pathYaml || "/asyncapi.yaml";
|
|
43
|
+
var pretty = opts.pretty === true ? 2 : 0;
|
|
44
|
+
var cacheControl = (typeof opts.cacheControl === "string" && opts.cacheControl.length > 0)
|
|
45
|
+
? opts.cacheControl : "public, max-age=300";
|
|
46
|
+
var accessControl = opts.accessControl || "public";
|
|
47
|
+
var auditOn = opts.audit !== false;
|
|
48
|
+
|
|
49
|
+
if (typeof pathJson !== "string" || pathJson.charAt(0) !== "/") {
|
|
50
|
+
throw new AsyncApiError("asyncapi/bad-path",
|
|
51
|
+
"asyncapiServe: pathJson must start with '/'");
|
|
52
|
+
}
|
|
53
|
+
if (typeof pathYaml !== "string" || pathYaml.charAt(0) !== "/") {
|
|
54
|
+
throw new AsyncApiError("asyncapi/bad-path",
|
|
55
|
+
"asyncapiServe: pathYaml must start with '/'");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var cachedJsonStr = null;
|
|
59
|
+
var cachedYamlStr = null;
|
|
60
|
+
var cachedJsonEtag = null;
|
|
61
|
+
var cachedYamlEtag = null;
|
|
62
|
+
|
|
63
|
+
function _rebuild() {
|
|
64
|
+
var doc = opts.document.toJson();
|
|
65
|
+
cachedJsonStr = JSON.stringify(doc, null, pretty);
|
|
66
|
+
cachedYamlStr = openapiYaml().toYaml(doc);
|
|
67
|
+
cachedJsonEtag = '"' + nodeCrypto.createHash("sha3-512").update(cachedJsonStr).digest("base64url").slice(0, 24) + '"';
|
|
68
|
+
cachedYamlEtag = '"' + nodeCrypto.createHash("sha3-512").update(cachedYamlStr).digest("base64url").slice(0, 24) + '"';
|
|
69
|
+
}
|
|
70
|
+
_rebuild();
|
|
71
|
+
|
|
72
|
+
function _writeBody(req, res, body, etag, contentType) {
|
|
73
|
+
var requestEtag = (req.headers && req.headers["if-none-match"]) || null;
|
|
74
|
+
if (requestEtag && requestEtag === etag) {
|
|
75
|
+
res.writeHead(304, { "ETag": etag, "Cache-Control": cacheControl }); // allow:raw-byte-literal — HTTP 304
|
|
76
|
+
res.end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
var headers = {
|
|
80
|
+
"Content-Type": contentType,
|
|
81
|
+
"Content-Length": Buffer.byteLength(body),
|
|
82
|
+
"Cache-Control": cacheControl,
|
|
83
|
+
"ETag": etag,
|
|
84
|
+
};
|
|
85
|
+
if (accessControl === "public") {
|
|
86
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
87
|
+
}
|
|
88
|
+
res.writeHead(200, headers); // allow:raw-byte-literal — HTTP 200
|
|
89
|
+
res.end(body);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var mw = function (req, res, next) {
|
|
93
|
+
if (typeof res.writeHead !== "function") return next();
|
|
94
|
+
var method = (req.method || "GET").toUpperCase();
|
|
95
|
+
if (method !== "GET" && method !== "HEAD") return next();
|
|
96
|
+
var pathname = req.pathname;
|
|
97
|
+
if (typeof pathname !== "string") {
|
|
98
|
+
var url = req.url || "";
|
|
99
|
+
var qIdx = url.indexOf("?");
|
|
100
|
+
pathname = qIdx === -1 ? url : url.slice(0, qIdx);
|
|
101
|
+
}
|
|
102
|
+
if (pathname === pathJson) {
|
|
103
|
+
_writeBody(req, res, cachedJsonStr, cachedJsonEtag, "application/json; charset=utf-8");
|
|
104
|
+
if (auditOn) {
|
|
105
|
+
try {
|
|
106
|
+
audit().safeEmit({
|
|
107
|
+
action: "asyncapi.document.served",
|
|
108
|
+
outcome: "success",
|
|
109
|
+
actor: null,
|
|
110
|
+
metadata: { format: "json", path: pathname, bytes: cachedJsonStr.length },
|
|
111
|
+
});
|
|
112
|
+
} catch (_e) { /* drop-silent */ }
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (pathname === pathYaml) {
|
|
117
|
+
_writeBody(req, res, cachedYamlStr, cachedYamlEtag, "application/yaml; charset=utf-8");
|
|
118
|
+
if (auditOn) {
|
|
119
|
+
try {
|
|
120
|
+
audit().safeEmit({
|
|
121
|
+
action: "asyncapi.document.served",
|
|
122
|
+
outcome: "success",
|
|
123
|
+
actor: null,
|
|
124
|
+
metadata: { format: "yaml", path: pathname, bytes: cachedYamlStr.length },
|
|
125
|
+
});
|
|
126
|
+
} catch (_e) { /* drop-silent */ }
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
return next();
|
|
131
|
+
};
|
|
132
|
+
mw.forceRebuild = _rebuild;
|
|
133
|
+
return mw;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { create: create };
|