@blamejs/blamejs-shop 0.3.55 → 0.3.57
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 +4 -0
- package/README.md +39 -2
- package/SECURITY.md +14 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/email.js +169 -0
- package/lib/storefront.js +368 -0
- package/lib/vendor/MANIFEST.json +84 -72
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/README.md +3 -3
- package/lib/vendor/blamejs/SECURITY.md +5 -0
- package/lib/vendor/blamejs/api-snapshot.json +15 -3
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +10 -4
- package/lib/vendor/blamejs/lib/ai-prompt.js +1 -1
- package/lib/vendor/blamejs/lib/app-shutdown.js +28 -0
- package/lib/vendor/blamejs/lib/archive-read.js +215 -16
- package/lib/vendor/blamejs/lib/archive.js +206 -52
- package/lib/vendor/blamejs/lib/auth/oauth.js +58 -0
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +84 -27
- package/lib/vendor/blamejs/lib/breach-deadline.js +166 -1
- package/lib/vendor/blamejs/lib/cloud-events.js +3 -1
- package/lib/vendor/blamejs/lib/codepoint-class.js +21 -0
- package/lib/vendor/blamejs/lib/db-schema.js +120 -3
- package/lib/vendor/blamejs/lib/db.js +10 -3
- package/lib/vendor/blamejs/lib/error-page.js +93 -9
- package/lib/vendor/blamejs/lib/external-db.js +164 -13
- package/lib/vendor/blamejs/lib/guard-email.js +36 -3
- package/lib/vendor/blamejs/lib/http-client.js +37 -7
- package/lib/vendor/blamejs/lib/mail-auth.js +554 -55
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +15 -5
- package/lib/vendor/blamejs/lib/mail-sieve.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +88 -19
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +58 -11
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +56 -4
- package/lib/vendor/blamejs/lib/middleware/attach-user.js +45 -10
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +70 -14
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +30 -2
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +29 -9
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +56 -4
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +301 -14
- package/lib/vendor/blamejs/lib/openapi-paths-builder.js +105 -29
- package/lib/vendor/blamejs/lib/openapi.js +225 -100
- package/lib/vendor/blamejs/lib/problem-details.js +15 -3
- package/lib/vendor/blamejs/lib/queue-local.js +148 -38
- package/lib/vendor/blamejs/lib/queue.js +41 -11
- package/lib/vendor/blamejs/lib/render.js +21 -3
- package/lib/vendor/blamejs/lib/router.js +13 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +55 -0
- package/lib/vendor/blamejs/lib/sse.js +7 -5
- package/lib/vendor/blamejs/lib/static.js +46 -17
- package/lib/vendor/blamejs/lib/uri-template.js +3 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +57 -0
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +127 -0
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +61 -0
- package/lib/vendor/blamejs/test/00-primitives.js +151 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +201 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +179 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/attach-user-bearer-scheme.test.js +154 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +10 -8
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +99 -20
- package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +50 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +63 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +107 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +145 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +119 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +121 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +14 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +53 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +177 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +312 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +165 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +33 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +59 -0
- package/lib/wishlist-alerts.js +22 -0
- package/lib/wishlist-digest.js +18 -0
- package/package.json +1 -1
|
@@ -256,10 +256,10 @@ async function _applyMtaStsPolicy(domain, mxs, policyMode, auditEmit) {
|
|
|
256
256
|
// The primitive composes the lookup; per-cert chain verification is
|
|
257
257
|
// the operator's responsibility (or future b.network.smtp.policy.dane.
|
|
258
258
|
// verifyChain extension).
|
|
259
|
-
async function _fetchDaneTlsa(mxHost, daneMode, auditEmit) {
|
|
259
|
+
async function _fetchDaneTlsa(mxHost, port, daneMode, auditEmit) {
|
|
260
260
|
if (daneMode === "off") return null;
|
|
261
261
|
try {
|
|
262
|
-
var tlsa = await smtpPolicy().dane.tlsa(mxHost, DEFAULT_PORT_SMTP);
|
|
262
|
+
var tlsa = await smtpPolicy().dane.tlsa(mxHost, port || DEFAULT_PORT_SMTP);
|
|
263
263
|
return tlsa && tlsa.length > 0 ? tlsa : null;
|
|
264
264
|
} catch (e) {
|
|
265
265
|
auditEmit("mail.send.deliver.dane.skip", "warn",
|
|
@@ -281,7 +281,7 @@ async function _tryHost(envelope, mxHost, hostnameLocal, opts) {
|
|
|
281
281
|
var factory = opts.transportFactory || mailModule().smtpTransport;
|
|
282
282
|
var transport = factory({
|
|
283
283
|
host: mxHost,
|
|
284
|
-
port: DEFAULT_PORT_SMTP,
|
|
284
|
+
port: opts.port || DEFAULT_PORT_SMTP,
|
|
285
285
|
ehloName: hostnameLocal,
|
|
286
286
|
timeoutMs: opts.perHostTimeoutMs || DEFAULT_PER_HOST_TIMEOUT_MS,
|
|
287
287
|
requireTls: envelope.requireTls === true,
|
|
@@ -327,7 +327,7 @@ async function _deliverOne(envelope, recipient, ctx) {
|
|
|
327
327
|
// composes directly into smtpTransport.dane); this branch carries
|
|
328
328
|
// the discovery so the audit chain records the policy posture
|
|
329
329
|
// applied to each delivery attempt.
|
|
330
|
-
await _fetchDaneTlsa(mx.exchange, ctx.policy.dane, ctx.auditEmit);
|
|
330
|
+
await _fetchDaneTlsa(mx.exchange, ctx.port, ctx.policy.dane, ctx.auditEmit);
|
|
331
331
|
try {
|
|
332
332
|
var rv = await _tryHost({
|
|
333
333
|
from: envelope.from,
|
|
@@ -396,6 +396,7 @@ async function _deliverOne(envelope, recipient, ctx) {
|
|
|
396
396
|
*
|
|
397
397
|
* @opts
|
|
398
398
|
* hostname: string, // required — local hostname for HELO/EHLO + DSN Reporting-MTA
|
|
399
|
+
* port: number, // default 25 (IANA SMTP, RFC 5321) — set 587 (RFC 6409 submission) or 465 (RFC 8314 implicit-TLS) for a smarthost relay
|
|
399
400
|
* resolver: object | null, // optional — b.network.dns.resolver handle; falls back to node:dns when omitted
|
|
400
401
|
* policy: {
|
|
401
402
|
* mtaSts: "enforce" | "testing" | "off", // default "enforce" — RFC 8461 posture
|
|
@@ -438,11 +439,19 @@ function create(opts) {
|
|
|
438
439
|
throw new DeliverError("deliver/bad-opts", "mail.send.deliver.create: opts is required");
|
|
439
440
|
}
|
|
440
441
|
validateOpts(opts,
|
|
441
|
-
["hostname", "resolver", "policy", "retry", "dsn", "timeouts", "audit", "transportFactory"],
|
|
442
|
+
["hostname", "resolver", "policy", "retry", "dsn", "timeouts", "audit", "transportFactory", "port"],
|
|
442
443
|
"mail.send.deliver.create");
|
|
443
444
|
validateOpts.requireNonEmptyString(opts.hostname,
|
|
444
445
|
"mail.send.deliver.create: hostname (local HELO/EHLO + DSN Reporting-MTA)",
|
|
445
446
|
DeliverError, "deliver/bad-hostname");
|
|
447
|
+
// Submission/smarthost relays listen on 587 (RFC 6409) or implicit-TLS
|
|
448
|
+
// 465 (RFC 8314) rather than the IANA SMTP port 25 (RFC 5321 §2.3.4)
|
|
449
|
+
// that direct MX delivery uses. Operators routing through such a relay
|
|
450
|
+
// set opts.port; the value is range-checked here (RFC 6335 §6) so a
|
|
451
|
+
// typo fails at config time, not on the first connect attempt.
|
|
452
|
+
validateOpts.optionalPort(opts.port,
|
|
453
|
+
"mail.send.deliver.create: port", DeliverError, "deliver/bad-port");
|
|
454
|
+
var port = opts.port || DEFAULT_PORT_SMTP;
|
|
446
455
|
|
|
447
456
|
var policy = opts.policy || {};
|
|
448
457
|
validateOpts(policy, ["mtaSts", "dane"], "mail.send.deliver.create.policy");
|
|
@@ -516,6 +525,7 @@ function create(opts) {
|
|
|
516
525
|
resolver: opts.resolver || null,
|
|
517
526
|
policy: { mtaSts: policyMtaSts, dane: policyDane },
|
|
518
527
|
hostname: opts.hostname,
|
|
528
|
+
port: port,
|
|
519
529
|
mxLookupTimeoutMs: mxLookupTimeoutMs,
|
|
520
530
|
perHostTimeoutMs: perHostTimeoutMs,
|
|
521
531
|
transportFactory: opts.transportFactory || null,
|
|
@@ -61,6 +61,7 @@ var safeSieve = require("./safe-sieve");
|
|
|
61
61
|
var { defineClass } = require("./framework-error");
|
|
62
62
|
var numericBounds = require("./numeric-bounds");
|
|
63
63
|
var validateOpts = require("./validate-opts");
|
|
64
|
+
var codepointClass = require("./codepoint-class");
|
|
64
65
|
|
|
65
66
|
var MailSieveError = defineClass("MailSieveError", { alwaysPermanent: true });
|
|
66
67
|
|
|
@@ -122,7 +123,7 @@ function _envelopeAddresses(env, key) {
|
|
|
122
123
|
// ---- match-type ---------------------------------------------------------
|
|
123
124
|
|
|
124
125
|
function _escapeRe(s) {
|
|
125
|
-
return
|
|
126
|
+
return codepointClass.escapeRegExp(s);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
function _wildcardToRe(pattern, caseInsensitive) {
|
|
@@ -19,9 +19,14 @@
|
|
|
19
19
|
* - "html" — when the response Content-Type is HTML,
|
|
20
20
|
* injects a <div role="status" ...> banner
|
|
21
21
|
* immediately after the <body> tag plus a
|
|
22
|
-
* <meta> tag inside <head>.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* <meta> tag inside <head>. Handles both a
|
|
23
|
+
* string and a Buffer body (the common server-
|
|
24
|
+
* render path); a Buffer is decoded under the
|
|
25
|
+
* response charset, injected, and re-encoded
|
|
26
|
+
* for utf-8 / ascii / latin1. Other charsets
|
|
27
|
+
* warn once and serve the original bytes (the
|
|
28
|
+
* disclosure headers still carry the notice).
|
|
29
|
+
* Skipped when the response is not text/html.
|
|
25
30
|
*
|
|
26
31
|
* The middleware does NOT alter the response when:
|
|
27
32
|
* - response status >= 400 (operator's error pages stay clean)
|
|
@@ -41,6 +46,24 @@ var requestHelpers = require("../request-helpers");
|
|
|
41
46
|
|
|
42
47
|
var aiActMod = lazyRequire(function () { return require("../compliance-ai-act"); });
|
|
43
48
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
49
|
+
var logger = lazyRequire(function () { return require("../log").boot("ai-act-disclosure"); });
|
|
50
|
+
|
|
51
|
+
// Charsets whose byte<->string round-trip is lossless for the inject
|
|
52
|
+
// operation: utf-8 (and its ascii / latin1 subsets, which Node decodes
|
|
53
|
+
// byte-for-byte). Other charsets (utf-16le, big5, gb18030, …) are not
|
|
54
|
+
// safe to decode→inject→re-encode without a transcoder we don't vendor,
|
|
55
|
+
// so the Buffer path warns once and serves the original bytes untouched
|
|
56
|
+
// rather than risk corrupting the page.
|
|
57
|
+
var SAFE_INJECT_ENCODINGS = { "utf-8": "utf8", "utf8": "utf8", "us-ascii": "utf8", "ascii": "utf8", "latin1": "latin1", "iso-8859-1": "latin1" };
|
|
58
|
+
|
|
59
|
+
// Read the charset token out of a Content-Type header, lowercased and
|
|
60
|
+
// stripped of surrounding quotes. Returns "" when absent (the caller
|
|
61
|
+
// treats a missing charset as the HTML default, utf-8).
|
|
62
|
+
function _charsetOf(contentType) {
|
|
63
|
+
if (typeof contentType !== "string") return "";
|
|
64
|
+
var m = /;\s*charset\s*=\s*"?([^";]+)"?/i.exec(contentType);
|
|
65
|
+
return m ? m[1].trim().toLowerCase() : "";
|
|
66
|
+
}
|
|
44
67
|
|
|
45
68
|
/**
|
|
46
69
|
* @primitive b.middleware.aiActDisclosure
|
|
@@ -53,7 +76,10 @@ var audit = lazyRequire(function () { return require("../audit"); });
|
|
|
53
76
|
* responses. In `mode: "header"` (default) it sets `AI-Act-Notice` and
|
|
54
77
|
* `AI-Act-Article` response headers — cheapest, works for both JSON
|
|
55
78
|
* and HTML. In `mode: "html"` it additionally inserts a status banner
|
|
56
|
-
* after `<body>`
|
|
79
|
+
* after `<body>` for HTML responses, handling both a string and a
|
|
80
|
+
* Buffer body (a Buffer is decoded under the response charset, injected,
|
|
81
|
+
* and re-encoded for utf-8 / ascii / latin1; other charsets warn once
|
|
82
|
+
* and serve the original bytes with the disclosure headers still set).
|
|
57
83
|
* Skips error pages, redirects, requests bearing the configured
|
|
58
84
|
* skip-header, and responses opted out via `res.locals.aiActSkip`.
|
|
59
85
|
* Emits `compliance.aiact.disclosed` audits on success.
|
|
@@ -138,22 +164,29 @@ function create(opts) {
|
|
|
138
164
|
res.end = function (chunk, encoding) {
|
|
139
165
|
try {
|
|
140
166
|
var ctype = (res.getHeader && res.getHeader("Content-Type")) || "";
|
|
141
|
-
if (typeof ctype === "string" && ctype.indexOf("text/html") !== -1 &&
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
var
|
|
152
|
-
if (
|
|
153
|
-
|
|
167
|
+
if (typeof ctype === "string" && ctype.indexOf("text/html") !== -1 && chunk) {
|
|
168
|
+
if (typeof chunk === "string") {
|
|
169
|
+
chunk = _injectBanner(chunk, opts);
|
|
170
|
+
} else if (Buffer.isBuffer(chunk)) {
|
|
171
|
+
// res.end(Buffer.from(html)) is the common server-render path
|
|
172
|
+
// (b.render serves a Buffer). Decode under the response charset,
|
|
173
|
+
// inject the Art. 50 banner, re-encode — but only for charsets
|
|
174
|
+
// whose round-trip is lossless. Unknown charsets warn once and
|
|
175
|
+
// serve the original bytes (no transcoder is vendored).
|
|
176
|
+
var charset = _charsetOf(ctype) || "utf-8";
|
|
177
|
+
var nodeEnc = SAFE_INJECT_ENCODINGS[charset];
|
|
178
|
+
if (nodeEnc) {
|
|
179
|
+
var injected = _injectBanner(chunk.toString(nodeEnc), opts);
|
|
180
|
+
chunk = Buffer.from(injected, nodeEnc);
|
|
181
|
+
// Content-Length, if the operator pre-set it, now understates
|
|
182
|
+
// the body — clear it so the runtime recomputes / chunks.
|
|
183
|
+
if (res.getHeader && res.getHeader("Content-Length") != null &&
|
|
184
|
+
typeof res.removeHeader === "function") {
|
|
185
|
+
res.removeHeader("Content-Length");
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
_warnUnsafeCharset(charset);
|
|
154
189
|
}
|
|
155
|
-
} else {
|
|
156
|
-
chunk = bannerHtml + chunk;
|
|
157
190
|
}
|
|
158
191
|
}
|
|
159
192
|
} catch (_e) { /* injection best-effort */ }
|
|
@@ -186,6 +219,42 @@ function create(opts) {
|
|
|
186
219
|
};
|
|
187
220
|
}
|
|
188
221
|
|
|
222
|
+
// Insert the EU AI Act Art. 50 status banner into an HTML string. The
|
|
223
|
+
// banner goes immediately after the opening <body> tag when present, else
|
|
224
|
+
// it is prepended. Returns the original string unchanged on any builder
|
|
225
|
+
// error (best-effort injection — the disclosure header still carries the
|
|
226
|
+
// machine-readable notice).
|
|
227
|
+
function _injectBanner(html, opts) {
|
|
228
|
+
var bannerHtml = aiActMod().transparency.htmlBanner({
|
|
229
|
+
kind: opts.kind || "ai-interaction",
|
|
230
|
+
lang: opts.lang || "en",
|
|
231
|
+
});
|
|
232
|
+
var bodyOpen = html.indexOf("<body");
|
|
233
|
+
if (bodyOpen !== -1) {
|
|
234
|
+
var afterTag = html.indexOf(">", bodyOpen);
|
|
235
|
+
if (afterTag !== -1) {
|
|
236
|
+
return html.slice(0, afterTag + 1) + bannerHtml + html.slice(afterTag + 1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return bannerHtml + html;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Warn once per process per charset that a Buffer HTML body in an
|
|
243
|
+
// unsupported charset was served without the banner injected, so an
|
|
244
|
+
// operator can switch the response to utf-8 (or accept the header-only
|
|
245
|
+
// disclosure). Drop-silent if the logger is unavailable.
|
|
246
|
+
var _warnedCharsets = Object.create(null);
|
|
247
|
+
function _warnUnsafeCharset(charset) {
|
|
248
|
+
if (_warnedCharsets[charset]) return;
|
|
249
|
+
_warnedCharsets[charset] = true;
|
|
250
|
+
try {
|
|
251
|
+
logger().warn("ai-act-disclosure: HTML response body is a Buffer in charset '" +
|
|
252
|
+
charset + "'; the Art. 50 banner was not injected (no transcoder for that " +
|
|
253
|
+
"charset). The disclosure headers are still set. Serve text/html as utf-8 to " +
|
|
254
|
+
"get the in-page banner.");
|
|
255
|
+
} catch (_e) { /* drop-silent — logger optional */ }
|
|
256
|
+
}
|
|
257
|
+
|
|
189
258
|
function _articleFor(kind) {
|
|
190
259
|
switch (kind) {
|
|
191
260
|
case "ai-interaction": return "Art. 50(1)";
|
|
@@ -440,6 +440,21 @@ function create(opts) {
|
|
|
440
440
|
});
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// _encodeEnvelope — build the wire envelope for a single response
|
|
444
|
+
// body. In per-request mode it is `{ _ct }`; in per-session mode it
|
|
445
|
+
// carries `{ _ct, _sid, _ctr }` so the client can detect tampered /
|
|
446
|
+
// replayed responses with a monotonic counter check.
|
|
447
|
+
function _encodeEnvelope(data, sessionKey, sessionCtx) {
|
|
448
|
+
var ptBuf = Buffer.from(JSON.stringify(data), "utf8");
|
|
449
|
+
var ctBuf = bCrypto.encryptPacked(ptBuf, sessionKey);
|
|
450
|
+
var encrypted = { _ct: ctBuf.toString("base64") };
|
|
451
|
+
if (sessionCtx) {
|
|
452
|
+
encrypted._sid = sessionCtx.sid;
|
|
453
|
+
encrypted._ctr = sessionCtx.responseCtr;
|
|
454
|
+
}
|
|
455
|
+
return encrypted;
|
|
456
|
+
}
|
|
457
|
+
|
|
443
458
|
// _wrapResJson — install res.json that encrypts the response with the
|
|
444
459
|
// session key. In per-request mode the response is `{ _ct }`; in
|
|
445
460
|
// per-session mode it carries `{ _ct, _sid, _ctr }` so the client can
|
|
@@ -448,13 +463,7 @@ function create(opts) {
|
|
|
448
463
|
var origJson = res.json;
|
|
449
464
|
res.json = function (data) {
|
|
450
465
|
try {
|
|
451
|
-
var
|
|
452
|
-
var ctBuf = bCrypto.encryptPacked(ptBuf, sessionKey);
|
|
453
|
-
var encrypted = { _ct: ctBuf.toString("base64") };
|
|
454
|
-
if (sessionCtx) {
|
|
455
|
-
encrypted._sid = sessionCtx.sid;
|
|
456
|
-
encrypted._ctr = sessionCtx.responseCtr;
|
|
457
|
-
}
|
|
466
|
+
var encrypted = _encodeEnvelope(data, sessionKey, sessionCtx);
|
|
458
467
|
if (typeof origJson === "function") {
|
|
459
468
|
return origJson.call(res, encrypted);
|
|
460
469
|
}
|
|
@@ -669,6 +678,10 @@ function create(opts) {
|
|
|
669
678
|
req.apiEncryptSessionKey = sessionKey;
|
|
670
679
|
if (sessionCtx) req.apiEncryptSession = { sid: sessionCtx.sid };
|
|
671
680
|
|
|
681
|
+
// Error-path encoder for terminal writers that bypass res.json; set
|
|
682
|
+
// only here (after a valid decrypt) so pre-session errors stay plaintext.
|
|
683
|
+
req.apiEncryptEncode = function (data) { return _encodeEnvelope(data, sessionKey, sessionCtx); };
|
|
684
|
+
|
|
672
685
|
_wrapResJson(res, sessionKey, sessionCtx);
|
|
673
686
|
_maybePrune();
|
|
674
687
|
|
|
@@ -895,6 +908,7 @@ function httpClientEncrypted(opts) {
|
|
|
895
908
|
opts = opts || {};
|
|
896
909
|
validateOpts(opts, [
|
|
897
910
|
"pubkey", "baseUrl", "headers", "method", "maxDecryptedBytes", "keying",
|
|
911
|
+
"responseMode",
|
|
898
912
|
], "middleware.apiEncrypt.httpClient");
|
|
899
913
|
if (!opts.pubkey) {
|
|
900
914
|
throw _err("CLIENT_INVALID_PUBKEY",
|
|
@@ -904,6 +918,16 @@ function httpClientEncrypted(opts) {
|
|
|
904
918
|
? opts.maxDecryptedBytes
|
|
905
919
|
: C.BYTES.mib(4);
|
|
906
920
|
var keying = opts.keying != null ? opts.keying : "per-request";
|
|
921
|
+
// responseMode controls how a non-2xx / non-encrypted response is
|
|
922
|
+
// handled: "reject" (default) keeps the core throwing on non-2xx and
|
|
923
|
+
// requires an encrypted `_ct` body; "passthrough" resolves any status
|
|
924
|
+
// and returns a plaintext body verbatim when it carries no `_ct`.
|
|
925
|
+
var responseModeDefault = opts.responseMode != null ? opts.responseMode : "reject";
|
|
926
|
+
if (responseModeDefault !== "reject" && responseModeDefault !== "passthrough") {
|
|
927
|
+
throw _err("CLIENT_BAD_OPT",
|
|
928
|
+
"httpClient.encrypted: responseMode must be 'reject' (default) or 'passthrough', got " +
|
|
929
|
+
JSON.stringify(opts.responseMode), 500);
|
|
930
|
+
}
|
|
907
931
|
var clientCtx = client({
|
|
908
932
|
pubkey: opts.pubkey,
|
|
909
933
|
maxDecryptedBytes: maxDecryptedBytes,
|
|
@@ -929,6 +953,12 @@ function httpClientEncrypted(opts) {
|
|
|
929
953
|
async function request(reqOpts) {
|
|
930
954
|
reqOpts = reqOpts || {};
|
|
931
955
|
var url = _resolveUrl(reqOpts);
|
|
956
|
+
var mode = (reqOpts && reqOpts.responseMode != null) ? reqOpts.responseMode : responseModeDefault;
|
|
957
|
+
if (mode !== "reject" && mode !== "passthrough") {
|
|
958
|
+
throw _err("CLIENT_BAD_OPT",
|
|
959
|
+
"httpClient.encrypted.request: responseMode must be 'reject' (default) or 'passthrough', got " +
|
|
960
|
+
JSON.stringify(reqOpts.responseMode), 500);
|
|
961
|
+
}
|
|
932
962
|
var encrypted = clientCtx.encryptRequest(
|
|
933
963
|
reqOpts.body !== undefined ? reqOpts.body : null
|
|
934
964
|
);
|
|
@@ -947,16 +977,24 @@ function httpClientEncrypted(opts) {
|
|
|
947
977
|
}
|
|
948
978
|
|
|
949
979
|
var rawBody = Buffer.from(JSON.stringify(encrypted.body), "utf8");
|
|
950
|
-
var
|
|
980
|
+
var requestArgs = Object.assign({
|
|
951
981
|
url: url,
|
|
952
982
|
method: reqOpts.method || defaultMethod,
|
|
953
983
|
headers: headers,
|
|
954
984
|
body: rawBody,
|
|
955
|
-
}, passThrough)
|
|
985
|
+
}, passThrough);
|
|
986
|
+
// In passthrough mode a non-2xx status resolves instead of throwing,
|
|
987
|
+
// so the caller can inspect the (possibly plaintext) error body.
|
|
988
|
+
if (mode === "passthrough") {
|
|
989
|
+
requestArgs.responseMode = "always-resolve";
|
|
990
|
+
}
|
|
991
|
+
var resp = await httpClient().request(requestArgs);
|
|
992
|
+
|
|
993
|
+
var ok = resp.statusCode >= 200 && resp.statusCode < 300;
|
|
956
994
|
|
|
957
995
|
// Empty body → no decryption (e.g. 204 No Content).
|
|
958
996
|
if (!resp.body || resp.body.length === 0) {
|
|
959
|
-
return { statusCode: resp.statusCode, headers: resp.headers, body: null };
|
|
997
|
+
return { statusCode: resp.statusCode, headers: resp.headers, body: null, ok: ok };
|
|
960
998
|
}
|
|
961
999
|
var parsed;
|
|
962
1000
|
try { parsed = safeJson.parse(resp.body.toString("utf8"), { maxBytes: maxDecryptedBytes }); }
|
|
@@ -964,10 +1002,19 @@ function httpClientEncrypted(opts) {
|
|
|
964
1002
|
throw _err("CLIENT_RESPONSE_NOT_JSON",
|
|
965
1003
|
"httpClient.encrypted: response body is not valid JSON: " + e.message);
|
|
966
1004
|
}
|
|
1005
|
+
// passthrough: decrypt only an encrypted envelope; return a plaintext
|
|
1006
|
+
// body verbatim. reject: always decrypt (throws on a missing _ct).
|
|
1007
|
+
var body;
|
|
1008
|
+
if (mode === "passthrough") {
|
|
1009
|
+
body = (parsed && typeof parsed._ct === "string") ? encrypted.decryptResponse(parsed) : parsed;
|
|
1010
|
+
} else {
|
|
1011
|
+
body = encrypted.decryptResponse(parsed);
|
|
1012
|
+
}
|
|
967
1013
|
return {
|
|
968
1014
|
statusCode: resp.statusCode,
|
|
969
1015
|
headers: resp.headers,
|
|
970
|
-
body:
|
|
1016
|
+
body: body,
|
|
1017
|
+
ok: ok,
|
|
971
1018
|
};
|
|
972
1019
|
}
|
|
973
1020
|
|
|
@@ -22,9 +22,42 @@
|
|
|
22
22
|
var nodeCrypto = require("node:crypto");
|
|
23
23
|
var validateOpts = require("../validate-opts");
|
|
24
24
|
var lazyRequire = require("../lazy-require");
|
|
25
|
+
var safeUrl = require("../safe-url");
|
|
25
26
|
var { defineClass } = require("../framework-error");
|
|
26
27
|
var AsyncApiError = defineClass("AsyncApiError", { alwaysPermanent: true });
|
|
27
28
|
|
|
29
|
+
// Validate an operator-supplied accessControl.allowOrigin and return the
|
|
30
|
+
// canonical `scheme://host[:port]` string for the Access-Control-Allow-
|
|
31
|
+
// Origin response header. CORS (Fetch Standard §3.2.1) requires a single
|
|
32
|
+
// concrete origin with no path / query / fragment; the empty-string and
|
|
33
|
+
// "*" wildcard forms are spelled separately ("same-origin" / "public").
|
|
34
|
+
// Parsing through safeUrl rejects header-injection bytes (CR/LF) and
|
|
35
|
+
// userinfo, and confirms the value is a real http(s) origin. Throws so
|
|
36
|
+
// the operator catches a typo'd allowOrigin at boot.
|
|
37
|
+
function _canonicalAllowOrigin(value, label) {
|
|
38
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
39
|
+
throw new AsyncApiError("asyncapi/bad-access-control",
|
|
40
|
+
label + ": accessControl.allowOrigin must be a non-empty origin string " +
|
|
41
|
+
"(e.g. \"https://docs.example.com\")");
|
|
42
|
+
}
|
|
43
|
+
var parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = safeUrl.parse(value, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
throw new AsyncApiError("asyncapi/bad-access-control",
|
|
48
|
+
label + ": accessControl.allowOrigin '" + value + "' is not a valid " +
|
|
49
|
+
"http(s) origin: " + ((e && e.message) || String(e)));
|
|
50
|
+
}
|
|
51
|
+
var path = parsed.pathname || "";
|
|
52
|
+
if ((path !== "" && path !== "/") || parsed.search || parsed.hash) {
|
|
53
|
+
throw new AsyncApiError("asyncapi/bad-access-control",
|
|
54
|
+
label + ": accessControl.allowOrigin must be a bare origin " +
|
|
55
|
+
"(scheme://host[:port]) with no path / query / fragment; got '" + value + "'");
|
|
56
|
+
}
|
|
57
|
+
var port = parsed.port;
|
|
58
|
+
return parsed.protocol + "//" + parsed.hostname.toLowerCase() + (port ? ":" + port : "");
|
|
59
|
+
}
|
|
60
|
+
|
|
28
61
|
var openapiYaml = lazyRequire(function () { return require("../openapi-yaml"); });
|
|
29
62
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
30
63
|
|
|
@@ -38,8 +71,12 @@ var audit = lazyRequire(function () { return require("../audit"); });
|
|
|
38
71
|
* configurable JSON + YAML mount points. Matches `openapiServe`
|
|
39
72
|
* behaviour: GET/HEAD only, SHA3-512 ETag with conditional 304,
|
|
40
73
|
* operator-controlled CORS gate, falls through on unmatched paths
|
|
41
|
-
* or methods.
|
|
42
|
-
*
|
|
74
|
+
* or methods. `accessControl: "public"` (default) emits
|
|
75
|
+
* `Access-Control-Allow-Origin: *`; `same-origin` omits the header;
|
|
76
|
+
* `{ allowOrigin: "https://docs.example.com" }` echoes one validated
|
|
77
|
+
* origin with `Vary: Origin`. Use to publish channel + operation +
|
|
78
|
+
* message + schema specs for event-driven APIs (Kafka, AMQP, MQTT,
|
|
79
|
+
* WebSocket).
|
|
43
80
|
*
|
|
44
81
|
* @opts
|
|
45
82
|
* {
|
|
@@ -79,6 +116,17 @@ function create(opts) {
|
|
|
79
116
|
var cacheControl = (typeof opts.cacheControl === "string" && opts.cacheControl.length > 0)
|
|
80
117
|
? opts.cacheControl : "public, max-age=300";
|
|
81
118
|
var accessControl = opts.accessControl || "public";
|
|
119
|
+
// Resolve the Access-Control-Allow-Origin value once at config time.
|
|
120
|
+
// "public" → "*"; an { allowOrigin } object → the canonical origin
|
|
121
|
+
// (validated, throws on a bad value); "same-origin" / anything else →
|
|
122
|
+
// null (no CORS header emitted).
|
|
123
|
+
var allowOriginHeader = null;
|
|
124
|
+
if (accessControl === "public") {
|
|
125
|
+
allowOriginHeader = "*";
|
|
126
|
+
} else if (accessControl && typeof accessControl === "object" &&
|
|
127
|
+
typeof accessControl.allowOrigin === "string") {
|
|
128
|
+
allowOriginHeader = _canonicalAllowOrigin(accessControl.allowOrigin, "asyncapiServe");
|
|
129
|
+
}
|
|
82
130
|
var auditOn = opts.audit !== false;
|
|
83
131
|
|
|
84
132
|
if (typeof pathJson !== "string" || pathJson.charAt(0) !== "/") {
|
|
@@ -117,8 +165,12 @@ function create(opts) {
|
|
|
117
165
|
"Cache-Control": cacheControl,
|
|
118
166
|
"ETag": etag,
|
|
119
167
|
};
|
|
120
|
-
if (
|
|
121
|
-
headers["Access-Control-Allow-Origin"] =
|
|
168
|
+
if (allowOriginHeader !== null) {
|
|
169
|
+
headers["Access-Control-Allow-Origin"] = allowOriginHeader;
|
|
170
|
+
// A specific (non-"*") origin makes the response vary by Origin;
|
|
171
|
+
// advertise it so shared caches don't serve one operator's allowed
|
|
172
|
+
// origin to another's request (Fetch Standard §3.2.1).
|
|
173
|
+
if (allowOriginHeader !== "*") headers["Vary"] = "Origin";
|
|
122
174
|
}
|
|
123
175
|
res.writeHead(200, headers); // HTTP 200
|
|
124
176
|
res.end(body);
|
|
@@ -33,18 +33,27 @@
|
|
|
33
33
|
*
|
|
34
34
|
* Options:
|
|
35
35
|
* {
|
|
36
|
-
* cookieName:
|
|
37
|
-
* tokenFrom:
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
36
|
+
* cookieName: 'blamejs_session' (cookie name to read)
|
|
37
|
+
* tokenFrom: 'both' | 'cookie' | 'header' (default 'both')
|
|
38
|
+
* bearerScheme: 'Bearer' (Authorization scheme token; RFC 6750 §2.1)
|
|
39
|
+
* tokenExtractor: (req) => token | null (overrides header extraction entirely)
|
|
40
|
+
* sealed: false (use cookies.readSealed)
|
|
41
|
+
* vault: b.vault (required when sealed)
|
|
42
|
+
* userLoader: async (verifiedSession) => user (REQUIRED)
|
|
43
|
+
* audit: true (emit user-load audits)
|
|
42
44
|
* }
|
|
45
|
+
*
|
|
46
|
+
* The Authorization-header path matches the `Bearer` scheme by default
|
|
47
|
+
* (RFC 6750 §2.1). Operators behind a gateway that issues a different
|
|
48
|
+
* scheme (e.g. `Token`, `DPoP` per RFC 9449) set bearerScheme, or pass
|
|
49
|
+
* tokenExtractor(req) to read the credential from anywhere on the
|
|
50
|
+
* request. The scheme match is case-insensitive (RFC 9110 §11.1).
|
|
43
51
|
*/
|
|
44
52
|
var lazyRequire = require("../lazy-require");
|
|
45
53
|
var cookies = require("../cookies");
|
|
46
54
|
var requestHelpers = require("../request-helpers");
|
|
47
55
|
var validateOpts = require("../validate-opts");
|
|
56
|
+
var codepointClass = require("../codepoint-class");
|
|
48
57
|
var session = lazyRequire(function () { return require("../session"); });
|
|
49
58
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
50
59
|
|
|
@@ -56,10 +65,17 @@ function _readCookie(cookieHeader, name) {
|
|
|
56
65
|
return Object.prototype.hasOwnProperty.call(jar, name) ? jar[name] : null;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
// Read the credential after an Authorization scheme token. Default scheme
|
|
69
|
+
// is "Bearer" (RFC 6750 §2.1); operators fronted by a gateway that mints
|
|
70
|
+
// "Token", "DPoP" (RFC 9449), or a custom scheme pass that name so the
|
|
71
|
+
// header is consumed instead of silently ignored. The scheme match is
|
|
72
|
+
// case-insensitive per RFC 9110 §11.1 (auth-scheme is case-insensitive).
|
|
73
|
+
function _readBearer(authHeader, scheme) {
|
|
60
74
|
if (!authHeader || typeof authHeader !== "string") return null;
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
var schemeTok = (typeof scheme === "string" && scheme.length > 0) ? scheme : "Bearer";
|
|
76
|
+
// allow:dynamic-regex — schemeTok is RegExp-escaped via codepointClass.escapeRegExp,
|
|
77
|
+
// so the operator-supplied scheme matches literally and cannot inject a pattern
|
|
78
|
+
var m = authHeader.match(new RegExp("^" + codepointClass.escapeRegExp(schemeTok) + "\\s+(.+)$", "i"));
|
|
63
79
|
return m ? m[1].trim() : null;
|
|
64
80
|
}
|
|
65
81
|
|
|
@@ -86,6 +102,8 @@ function _readBearer(authHeader) {
|
|
|
86
102
|
* userLoader: async function(session): user|null, // required
|
|
87
103
|
* cookieName: string, // default "blamejs_session"
|
|
88
104
|
* tokenFrom: "both"|"cookie"|"header", // default "both"
|
|
105
|
+
* bearerScheme: string, // default "Bearer" (RFC 6750); set "Token"/"DPoP"/etc. for a gateway scheme
|
|
106
|
+
* tokenExtractor: function, // (req) → token|null; fully owns header extraction when supplied
|
|
89
107
|
* sealed: boolean,
|
|
90
108
|
* vault: object, // required when sealed
|
|
91
109
|
* requireFingerprintMatch: boolean,
|
|
@@ -108,15 +126,28 @@ function create(opts) {
|
|
|
108
126
|
validateOpts(opts, [
|
|
109
127
|
"cookieName", "tokenFrom", "sealed", "vault", "userLoader", "audit",
|
|
110
128
|
"requireFingerprintMatch", "maxAnomalyScore", "scorer",
|
|
129
|
+
"bearerScheme", "tokenExtractor",
|
|
111
130
|
], "middleware.attachUser");
|
|
112
131
|
if (typeof opts.userLoader !== "function") {
|
|
113
132
|
throw new Error("middleware.attachUser: opts.userLoader is required " +
|
|
114
133
|
"(async function (verifiedSession) → user | null)");
|
|
115
134
|
}
|
|
135
|
+
validateOpts.optionalNonEmptyString(opts.bearerScheme,
|
|
136
|
+
"middleware.attachUser: opts.bearerScheme (the Authorization scheme token, " +
|
|
137
|
+
"e.g. \"Bearer\", \"Token\", \"DPoP\")");
|
|
138
|
+
validateOpts.optionalFunction(opts.tokenExtractor,
|
|
139
|
+
"middleware.attachUser: opts.tokenExtractor (req) → token | null");
|
|
116
140
|
var cookieName = opts.cookieName || "blamejs_session";
|
|
117
141
|
var tokenFrom = opts.tokenFrom || "both";
|
|
118
142
|
var auditOn = opts.audit !== false;
|
|
119
143
|
var sealed = !!opts.sealed;
|
|
144
|
+
// Authorization-header scheme token (default "Bearer", RFC 6750 §2.1).
|
|
145
|
+
// tokenExtractor, when supplied, fully owns header-token extraction so
|
|
146
|
+
// gateway-specific schemes (a forwarded JWT in a non-standard header,
|
|
147
|
+
// DPoP-bound tokens, etc.) work without the framework assuming the
|
|
148
|
+
// RFC 6750 shape.
|
|
149
|
+
var bearerScheme = opts.bearerScheme || "Bearer";
|
|
150
|
+
var tokenExtractor = typeof opts.tokenExtractor === "function" ? opts.tokenExtractor : null;
|
|
120
151
|
// Fingerprint-drift / IP-UA pin / anomaly-score opts thread through
|
|
121
152
|
// session.verify so the documented session.create({ req,
|
|
122
153
|
// fingerprintFields }) defenses actually engage on every verify
|
|
@@ -153,7 +184,11 @@ function create(opts) {
|
|
|
153
184
|
// the header re-read here avoids the duplicate verify and the
|
|
154
185
|
// confusing "session.verify failed" audit row that would land
|
|
155
186
|
// when the bearer token is a JWT or API key, not a session ID.
|
|
156
|
-
|
|
187
|
+
if (tokenExtractor) {
|
|
188
|
+
token = tokenExtractor(req) || null;
|
|
189
|
+
} else {
|
|
190
|
+
token = _readBearer(req.headers && req.headers.authorization, bearerScheme);
|
|
191
|
+
}
|
|
157
192
|
}
|
|
158
193
|
if (!token) return next();
|
|
159
194
|
|