@blamejs/core 0.13.46 → 0.14.1
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 +6 -0
- package/lib/asn1-der.js +11 -0
- package/lib/auth/jar.js +11 -0
- package/lib/auth/oauth.js +5 -7
- package/lib/auth/oid4vci.js +1 -1
- package/lib/auth/saml.js +6 -2
- package/lib/graphql-federation.js +8 -1
- package/lib/http-client-cache.js +17 -0
- package/lib/http-client.js +12 -9
- package/lib/log.js +7 -2
- package/lib/mail-crypto-pgp.js +22 -14
- package/lib/middleware/age-gate.js +14 -2
- package/lib/middleware/ai-act-disclosure.js +11 -4
- package/lib/middleware/rate-limit.js +14 -5
- package/lib/middleware/sse.js +14 -8
- package/lib/network-tls.js +12 -7
- package/lib/protobuf-encoder.js +8 -0
- package/lib/queue.js +4 -2
- package/lib/sse.js +12 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ Pre-1.0 the surface is intentionally evolving — every release may
|
|
|
6
6
|
change something operators depend on. Read each entry before
|
|
7
7
|
upgrading across more than a few patches at a time.
|
|
8
8
|
|
|
9
|
+
## v0.14.x
|
|
10
|
+
|
|
11
|
+
- v0.14.1 (2026-05-29) — **Correctness fixes: JAR request-object typ enforcement, byte-faithful PGP multipart/signed, and SAML InResponseTo binding.** A set of correctness fixes across the auth, mail-crypto, TLS, and encoder surfaces. The most important: JWT-secured authorization requests (RFC 9101) now require the request object to carry the registered `oauth-authz-req+jwt` typ, closing a cross-JWT-confusion vector; the PGP multipart/signed wrapper is now assembled byte-faithfully so non-ASCII signed content can't be corrupted; and SAML response verification now returns the InResponseTo of the SubjectConfirmation that actually validated. Two of these change behavior — see Changed — and are bug fixes rather than new features. **Changed:** *JAR parsing rejects untyped request objects (breaking)* — Following the typ enforcement above, a request object whose header omits `typ` is now refused. An authorization server whose clients sign request objects without the `oauth-authz-req+jwt` typ must update those clients to set it. · *PGP sign() returns multipartSigned as a Buffer (breaking)* — `b.mail.crypto.pgp.sign(...).multipartSigned` is now a Buffer instead of a string. The OpenPGP signature covers the signed-part bytes exactly, and a JS-string round trip through latin1/utf8 could corrupt non-ASCII signed content and break verification, so the RFC 3156 multipart/signed wrapper is now assembled as bytes. Code that wrote the previous string to the wire works unchanged when it writes the Buffer; code that did string operations on the value should treat it as a Buffer (its `indexOf` / `toString` still work). **Fixed:** *SAML response verification binds InResponseTo to the validated confirmation* — `verifyResponse` returned the InResponseTo of the first SubjectConfirmationData in the assertion rather than of the SubjectConfirmation that actually passed bearer validation. When a response carried more than one SubjectConfirmation, the returned value could come from a non-validated confirmation. It is now the InResponseTo of the confirmation that validated. · *checkServerIdentity9525 emits the documented CN-fallback audit code* — A CN-only legacy certificate (a Common Name present, no subjectAltName) is now refused by the exported `b.network.tls.checkServerIdentity9525` with the distinct `tls/pkix-cn-fallback-refused` code its documentation promised, so audit logs can tell a CN-only certificate apart from one carrying neither a SAN nor a CN (which still yields `tls/pkix-san-required`). The accept/refuse outcome is unchanged — both are refused; only the audit granularity improved. · *OIDC back-channel logout / JARM no longer accept dead override parameters* — `verifyBackchannelLogoutToken` and `parseJarmResponse` passed `acceptedAlgs` / `jwksUri` / `maxClockSkewMs` through to the ID-token verifier, which ignored them and applied the configured (create()-time) values. The pass-throughs are removed so the code no longer reads as if those can be overridden per call; the configured trust anchor was — and remains — what applies. · *Queue lease failures are logged* — The consumer loop's lease-acquisition error path swallowed the backend error silently; it now logs at debug so a flapping backend that has not yet tripped the circuit breaker is visible. · *Protobuf and ASN.1 encoders reject out-of-range tags* — The protobuf tag encoder rejected nothing and would have emitted a wrong tag for field numbers at or above 2^28 (where the 32-bit shift overflows); the ASN.1 context-tag writers silently truncated tag numbers above 30 (which require the multi-byte high-tag-number form). Both now throw a RangeError rather than encode silently-wrong output. The values these encoders serve are well within range, so no current caller is affected. · *oid4vci proofAlgorithms default documented accurately* — The documented default proof-algorithm list now matches the code (`["ES256", "ES384", "EdDSA"]`); the doc previously omitted EdDSA, which the runtime has always accepted by default. **Security:** *JAR request objects must carry the registered typ (RFC 9101)* — `b.auth.jar.parse` now requires the request-object JWS header to carry `typ: "oauth-authz-req+jwt"` (with or without the `application/` prefix); a request object with an absent or different typ is refused with `auth-jar/bad-typ`. RFC 9101 §10.8 specifies this media type precisely to stop a JWT minted for another purpose (an ID token, an access token, a logout token) and signed by the same client key from being replayed as a request object. The existing `iss` / `aud` / `client_id` bindings already constrained that; this restores the explicit type check as well. This is stricter than before — see Changed.
|
|
12
|
+
|
|
13
|
+
- v0.14.0 (2026-05-29) — **Operator-configurable header and field names across SSE, request-id, rate-limit, age-gate, AI-Act disclosure, GraphQL federation, and the HTTP cache.** This release makes operator-facing identifiers that were hardcoded configurable. The framework already let operators rename most names (CSRF cookie/field, cookie parser, i18n header/query/cookie, mTLS CA name, and so on); this closes the remaining gaps so a custom or framework-specific name is never frozen. Every new option defaults to the value emitted today, so upgrading changes no behavior — these are additive knobs. It also fixes a request-id asymmetry (the response header is now written on the same name the inbound id is read from) and wires an SSE proxy-buffering option whose escape hatch was documented but never implemented. **Added:** *Configurable cache-status header on the HTTP client* — The outbound HTTP client annotated every cached response with a hardcoded `x-blamejs-cache: HIT|MISS|STALE|REVALIDATED` header. `b.httpClient.cache.create` now takes `statusHeader` (default "x-blamejs-cache") — pass a custom name (e.g. "x-cache") to rename it, or null/false to suppress it entirely. The decision remains available programmatically on `res.cacheStatus`. · *Configurable rate-limit header names* — `b.middleware.rateLimit` emitted the de-facto `X-RateLimit-Limit` / `X-RateLimit-Remaining` headers (which are not RFC-pinned). It now accepts `headerPrefix` (default "X-RateLimit-") so operators can match the unprefixed IETF-draft `RateLimit-*` names or an upstream gateway's convention; the limit/remaining pair is always built from the same prefix. · *Configurable age-gate and AI-Act disclosure header names* — `b.middleware.ageGate` now takes `privacyPostureHeader` (default "X-Privacy-Posture"; null/false to suppress), and `b.middleware.aiActDisclosure` takes `headerPrefix` (default "AI-Act-") that prefixes the emitted Notice / Article / Policy headers. The EU AI Act mandates the disclosure, not the HTTP spelling, so operators matching a downstream convention can rename these. · *Configurable GraphQL-federation replay-nonce header* — `b.graphqlFederation.guardSdl` read the replay nonce from the Apollo-vendor `x-apollographql-router-nonce` header with no override. It now accepts `nonceHeader` (default unchanged) so an operator fronting the gateway with a non-Apollo router can point the replay check at their own header. · *SSE proxy-buffering opt-out* — `b.sse.create` and `b.middleware.sse` set `X-Accel-Buffering: no` (the nginx hint that disables proxy buffering). They now accept `proxyBuffer` (default true) — pass false when not behind nginx, or when buffering is controlled at the load balancer, to suppress the nginx-specific header. The opt-out was previously referenced in the documentation but not implemented. **Fixed:** *Request-id middleware reflects the configured header name* — `b.log.middleware` read the inbound request id from a configurable `headerName` but always wrote the response on the literal `X-Request-Id`. An operator who set a custom `headerName` (e.g. `X-Correlation-Id`) therefore read from one header and emitted another. The response is now written on the same configured name; the default remains `X-Request-Id`, so deployments that did not set `headerName` are unaffected.
|
|
14
|
+
|
|
9
15
|
## v0.13.x
|
|
10
16
|
|
|
11
17
|
- v0.13.46 (2026-05-29) — **`createApp` now wires the documented security middleware ON by default — CSRF, CSP nonce, cookie parser, fetch-metadata, and body parser.** The README has long described a security middleware stack as "wired by createApp", but createApp only mounted request-ID, security-headers, and bot-guard by default — CSRF protection, the CSP nonce, the threat-aware cookie parser, the fetch-metadata guard, and the body parser were documented but not actually wired. This release closes that gap: createApp now mounts all of them by default, in dependency order (cookies, CSP nonce, fetch-metadata, then body parser, then CSRF last so it can read a body-field token). This is a behavior change — apps built with createApp now enforce CSRF on state-changing requests by default. Each layer is configurable via opts.middleware.<name> (operator cookie and field names flow straight through — nothing is hardcoded) or can be turned off with false, and disabling a security default now emits an app.middleware.disabled audit event. Every layer is idempotent: an operator who also mounts one of these inside opts.routes gets a no-op second mount rather than a double-apply. The default CSRF is a double-submit cookie that auto-skips requests carrying an Authorization header or no cookies at all, which are not CSRF-able, so token-authenticated API clients are not rejected. The README middleware list is now an accurate description of what createApp wires. **Added:** *Idempotent security middleware* — The cookie parser, CSP nonce, fetch-metadata, and CSRF middleware are now idempotent within a request: if one has already run (because createApp wired it and an operator also mounted it), the second instance is a no-op rather than re-parsing, re-generating a nonce, or issuing a second CSRF cookie. This lets an application compose its own middleware order on top of createApp's defaults without double-applying. The body parser already had this behavior. **Changed:** *createApp wires CSRF, CSP nonce, cookie parser, fetch-metadata, and body parser by default (breaking)* — Applications constructed with b.createApp now mount, in order: the threat-aware cookie parser, the CSP nonce generator, the fetch-metadata resource-isolation guard, the body parser (JSON / urlencoded / text / multipart), and CSRF protection — in addition to the request-ID, security-headers, and bot-guard layers already wired. The ordering guarantees CSRF runs after the body parser so a body-field token is available. This is a behavior change: state-changing requests (POST / PUT / DELETE / PATCH) that carry a session cookie are now CSRF-validated by default. Each layer is configured through opts.middleware.<name> (an object passes operator options straight through; cookie and field names are not hardcoded) or disabled with false. Operators who were mounting these middleware themselves inside opts.routes do not need to change anything — the second mount is now a no-op (see idempotency below). · *Default CSRF auto-skips token-authenticated and cookieless requests* — The CSRF middleware gains a skipStateless option (default false; createApp's default wiring sets it true). When on, token validation is skipped for requests that carry an Authorization header or no Cookie header at all — such requests are not CSRF-able, because CSRF abuses a victim's ambient cookie credential and these have none. The token is still issued on safe methods so a later cookie-authenticated browser flow works. Cross-site form CSRF is unaffected: the browser auto-sends the victim's cookies, so an attack request always carries a Cookie header and is validated. · *Disabling a default security middleware is audited* — Passing false for one of the security-on-by-default middleware (for example middleware: { csrf: false }) now emits an app.middleware.disabled audit event naming the middleware, so a weakened posture leaves a trace in the audit chain rather than being silent.
|
package/lib/asn1-der.js
CHANGED
|
@@ -321,6 +321,13 @@ function writeOid(dotted) {
|
|
|
321
321
|
|
|
322
322
|
function writeContextExplicit(tagNumber, child) {
|
|
323
323
|
// [N] EXPLICIT — context-specific class (0xA0 | tag) + constructed.
|
|
324
|
+
// Tag numbers > 30 need the multi-byte high-tag-number form, which this
|
|
325
|
+
// single-byte encoder does not emit — refuse rather than silently
|
|
326
|
+
// truncate via `& 0x1f`.
|
|
327
|
+
if (tagNumber < 0 || tagNumber > 30) {
|
|
328
|
+
throw new RangeError("asn1: context tag number " + tagNumber +
|
|
329
|
+
" out of range (0..30); high-tag-number form is not supported");
|
|
330
|
+
}
|
|
324
331
|
var tagByte = 0xa0 | (tagNumber & 0x1f); // allow:raw-byte-literal — context-specific constructed mask
|
|
325
332
|
return writeNode(tagByte, child);
|
|
326
333
|
}
|
|
@@ -331,6 +338,10 @@ function writeContextImplicit(tagNumber, value, opts) {
|
|
|
331
338
|
// wrapping a structured value (e.g. IMPLICIT [0] OCTET STRING vs
|
|
332
339
|
// IMPLICIT [0] SEQUENCE OF). Value is the raw inner bytes (already
|
|
333
340
|
// encoded for constructed cases).
|
|
341
|
+
if (tagNumber < 0 || tagNumber > 30) {
|
|
342
|
+
throw new RangeError("asn1: context tag number " + tagNumber +
|
|
343
|
+
" out of range (0..30); high-tag-number form is not supported");
|
|
344
|
+
}
|
|
334
345
|
var tagByte = 0x80 | (tagNumber & 0x1f); // allow:raw-byte-literal — context-specific primitive mask
|
|
335
346
|
if (opts && opts.constructed) tagByte |= 0x20; // allow:raw-byte-literal — constructed bit
|
|
336
347
|
return writeNode(tagByte, value);
|
package/lib/auth/jar.js
CHANGED
|
@@ -130,6 +130,17 @@ async function parse(jar, opts) {
|
|
|
130
130
|
audience: opts.audience,
|
|
131
131
|
clockSkewMs: opts.clockSkewMs,
|
|
132
132
|
});
|
|
133
|
+
// RFC 9101 §10.8 — the request object MUST be explicitly typed so a JWT
|
|
134
|
+
// minted for another purpose (id_token / access-token / logout-token)
|
|
135
|
+
// and signed by the same client key cannot be replayed here as a request
|
|
136
|
+
// object (cross-JWT confusion). Require the registered media type, with or
|
|
137
|
+
// without the "application/" prefix; an absent or mismatched typ is refused.
|
|
138
|
+
var jarTyp = verified.header && verified.header.typ;
|
|
139
|
+
if (jarTyp !== JAR_TYP && jarTyp !== "application/" + JAR_TYP) {
|
|
140
|
+
throw new AuthJarError("auth-jar/bad-typ",
|
|
141
|
+
"jar.parse: request object header.typ must be \"" + JAR_TYP +
|
|
142
|
+
"\" (RFC 9101 §10.8 — cross-JWT-confusion defense)");
|
|
143
|
+
}
|
|
133
144
|
var payload = verified.claims;
|
|
134
145
|
|
|
135
146
|
// RFC 9101 §5.2 — the request object MUST carry a client_id claim,
|
package/lib/auth/oauth.js
CHANGED
|
@@ -836,10 +836,10 @@ function create(opts) {
|
|
|
836
836
|
// only in claim validation (no nonce, audience = clientId, no
|
|
837
837
|
// ID-token-specific claims). We wrap verifyIdToken with the
|
|
838
838
|
// skip-nonce flag and apply JARM-specific claim checks below.
|
|
839
|
+
// verifyIdToken applies the create()-level accepted algorithms / JWKS /
|
|
840
|
+
// clock-skew; only the JARM-specific skip-nonce flag is passed here.
|
|
839
841
|
var verified = await verifyIdToken(responseJwt, {
|
|
840
842
|
skipNonceCheck: true,
|
|
841
|
-
acceptedAlgs: jopts.acceptedAlgs,
|
|
842
|
-
maxClockSkewMs: jopts.maxClockSkewMs,
|
|
843
843
|
});
|
|
844
844
|
var c = verified.claims;
|
|
845
845
|
// Per JARM §4: `iss` MUST match the OP issuer; `aud` MUST contain
|
|
@@ -1419,12 +1419,10 @@ function create(opts) {
|
|
|
1419
1419
|
}
|
|
1420
1420
|
// Reuse verifyIdToken's signature-verification path. It looks up
|
|
1421
1421
|
// the IdP JWKS and checks the JWS — same trust anchor.
|
|
1422
|
+
// verifyIdToken applies the create()-level issuer / clientId / accepted
|
|
1423
|
+
// algorithms / JWKS / clock-skew — the same trust anchor as id_tokens.
|
|
1424
|
+
// Only the per-call logout-token semantics are passed here.
|
|
1422
1425
|
var verified = await verifyIdToken(logoutToken, {
|
|
1423
|
-
issuer: issuer,
|
|
1424
|
-
clientId: clientId,
|
|
1425
|
-
acceptedAlgs: vopts.acceptedAlgs,
|
|
1426
|
-
jwksUri: vopts.jwksUri,
|
|
1427
|
-
maxClockSkewMs: vopts.maxClockSkewMs,
|
|
1428
1426
|
// Logout tokens have no nonce — disable the nonce check that
|
|
1429
1427
|
// verifyIdToken would otherwise enforce on id_tokens.
|
|
1430
1428
|
skipNonceCheck: true,
|
package/lib/auth/oid4vci.js
CHANGED
|
@@ -240,7 +240,7 @@ function _verifyProofJwt(proofJwt, expectedAud, expectedCNonce, expectedClientId
|
|
|
240
240
|
* tokenEndpoint: string, // public URL for /token (re-used by the pre-auth flow)
|
|
241
241
|
* sdJwtIssuer: <b.auth.sdJwtVc.issuer instance>, // mints the SD-JWT VC
|
|
242
242
|
* supportedCredentials: { [id]: { format, vct, claims, ... } },
|
|
243
|
-
* proofAlgorithms: string[], // default ["ES256", "ES384"]
|
|
243
|
+
* proofAlgorithms: string[], // default ["ES256", "ES384", "EdDSA"]
|
|
244
244
|
* preAuthCodeTtlMs?: number, // default 5m
|
|
245
245
|
* accessTokenTtlMs?: number, // default 15m
|
|
246
246
|
* cNonceTtlMs?: number, // default 5m
|
package/lib/auth/saml.js
CHANGED
|
@@ -596,6 +596,10 @@ function create(opts) {
|
|
|
596
596
|
var confirmations = _findAllChildren(subject, "SubjectConfirmation", SAML_NS.assertion);
|
|
597
597
|
var bearerOk = false;
|
|
598
598
|
var hokOk = false;
|
|
599
|
+
// InResponseTo of the SubjectConfirmation that actually passed bearer
|
|
600
|
+
// validation — captured so the returned value can't be sourced from a
|
|
601
|
+
// different (non-validated) confirmation when several are present.
|
|
602
|
+
var matchedInResponseTo = null;
|
|
599
603
|
var hokFingerprint = null;
|
|
600
604
|
// Holder-of-Key SubjectConfirmation per SAML 2.0 Profile §3.1
|
|
601
605
|
// (urn:oasis:names:tc:SAML:2.0:cm:holder-of-key). The IdP binds
|
|
@@ -721,6 +725,7 @@ function create(opts) {
|
|
|
721
725
|
"AuthnRequest ID (replay defense)");
|
|
722
726
|
}
|
|
723
727
|
}
|
|
728
|
+
matchedInResponseTo = inResponseTo;
|
|
724
729
|
bearerOk = true;
|
|
725
730
|
break;
|
|
726
731
|
}
|
|
@@ -787,8 +792,7 @@ function create(opts) {
|
|
|
787
792
|
sessionIndex: sessionIndex,
|
|
788
793
|
attributes: attributes,
|
|
789
794
|
audience: audience,
|
|
790
|
-
inResponseTo: bearerOk ?
|
|
791
|
-
"SubjectConfirmationData", SAML_NS.assertion), "InResponseTo") : null,
|
|
795
|
+
inResponseTo: bearerOk ? matchedInResponseTo : null,
|
|
792
796
|
issuer: issuer,
|
|
793
797
|
};
|
|
794
798
|
}
|
|
@@ -116,6 +116,7 @@ function _readBody(req, errorClass) {
|
|
|
116
116
|
* publicSchemaOk: boolean, // default false — explicit override to publish the SDL
|
|
117
117
|
* routerToken: string, // required unless publicSchemaOk; 32+ chars
|
|
118
118
|
* nonceStore: { has(nonce): bool, remember(nonce, ttlMs) }, // optional — replay protection
|
|
119
|
+
* nonceHeader: string, // default "x-apollographql-router-nonce" — request header carrying the replay nonce
|
|
119
120
|
* nonceTtlMs: number, // default 5 minutes
|
|
120
121
|
* errorClass: Function, // default GraphqlFederationError
|
|
121
122
|
* audit: boolean, // default true
|
|
@@ -141,6 +142,12 @@ function guardSdl(opts) {
|
|
|
141
142
|
numericBounds.requirePositiveFiniteIntIfPresent(opts.nonceTtlMs, "graphqlFederation.guardSdl: opts.nonceTtlMs", errorClass, "BAD_TTL");
|
|
142
143
|
var nonceTtlMs = opts.nonceTtlMs || C.TIME.minutes(5);
|
|
143
144
|
var auditOn = opts.audit !== false;
|
|
145
|
+
// The replay nonce is read from the Apollo-router convention header by
|
|
146
|
+
// default, but `x-apollographql-router-nonce` is a vendor name, not a
|
|
147
|
+
// spec one — operators fronting the gateway with a different router send
|
|
148
|
+
// the nonce under their own header. Lowercased to match Node's header keys.
|
|
149
|
+
var nonceHeader = (typeof opts.nonceHeader === "string" && opts.nonceHeader.length > 0)
|
|
150
|
+
? opts.nonceHeader.toLowerCase() : "x-apollographql-router-nonce";
|
|
144
151
|
|
|
145
152
|
function _emitDenied(req, reason, metadata) {
|
|
146
153
|
if (!auditOn) return;
|
|
@@ -190,7 +197,7 @@ function guardSdl(opts) {
|
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
if (nonceStore) {
|
|
193
|
-
var nonce = req.headers && req.headers[
|
|
200
|
+
var nonce = req.headers && req.headers[nonceHeader];
|
|
194
201
|
if (typeof nonce !== "string" || nonce.length < NONCE_MIN_LEN || nonce.length > NONCE_MAX_LEN) {
|
|
195
202
|
_emitDenied(req, "missing nonce", {});
|
|
196
203
|
return _refuse(res, 401, "graphql federation: nonce required");
|
package/lib/http-client-cache.js
CHANGED
|
@@ -545,6 +545,7 @@ function memoryStore(opts) {
|
|
|
545
545
|
* revalidateInBackground: true, // s-w-r kicks off background revalidation
|
|
546
546
|
* audit: undefined, // audit sink with safeEmit({...})
|
|
547
547
|
* observability: undefined, // optional { event, safeEvent }
|
|
548
|
+
* statusHeader: "x-blamejs-cache", // response header carrying the cache decision; null/false to suppress, or a custom name (e.g. "x-cache")
|
|
548
549
|
*
|
|
549
550
|
* @example
|
|
550
551
|
* var cache = b.httpClient.cache.create({
|
|
@@ -585,6 +586,21 @@ function create(opts) {
|
|
|
585
586
|
var revalidateBackground = opts.revalidateInBackground !== false; // default true
|
|
586
587
|
var audit = opts.audit || null;
|
|
587
588
|
var obs = opts.observability || null;
|
|
589
|
+
// statusHeader (default "x-blamejs-cache") names the response header that
|
|
590
|
+
// carries the cache decision (MISS/HIT/STALE/REVALIDATED). The decision is
|
|
591
|
+
// also on res.cacheStatus programmatically. Pass null/false to suppress the
|
|
592
|
+
// header, or a string to rename it (e.g. "x-cache"). Lowercased for the wire.
|
|
593
|
+
var statusHeader;
|
|
594
|
+
if (opts.statusHeader === null || opts.statusHeader === false) {
|
|
595
|
+
statusHeader = null;
|
|
596
|
+
} else if (opts.statusHeader === undefined) {
|
|
597
|
+
statusHeader = "x-blamejs-cache";
|
|
598
|
+
} else if (typeof opts.statusHeader === "string" && opts.statusHeader.length > 0) {
|
|
599
|
+
statusHeader = opts.statusHeader.toLowerCase();
|
|
600
|
+
} else {
|
|
601
|
+
throw _hcErr("httpclient/cache-bad-opts",
|
|
602
|
+
"cache.create: statusHeader must be a non-empty string, or null/false to suppress");
|
|
603
|
+
}
|
|
588
604
|
|
|
589
605
|
function _emit(action, outcome, metadata) {
|
|
590
606
|
if (!audit || typeof audit.safeEmit !== "function") return;
|
|
@@ -825,6 +841,7 @@ function create(opts) {
|
|
|
825
841
|
store: store,
|
|
826
842
|
audit: audit,
|
|
827
843
|
observability: obs,
|
|
844
|
+
statusHeader: statusHeader,
|
|
828
845
|
|
|
829
846
|
// ---- Lookup / store / revalidation flow ----
|
|
830
847
|
//
|
package/lib/http-client.js
CHANGED
|
@@ -849,9 +849,12 @@ function _cacheEligibleMethod(method) {
|
|
|
849
849
|
|
|
850
850
|
// Wrap an outbound headers object with the framework's cache-decision
|
|
851
851
|
// markers. Mutates a copy; never the original.
|
|
852
|
-
function _withCacheHeaders(res, status, ageSeconds) {
|
|
852
|
+
function _withCacheHeaders(res, status, ageSeconds, statusHeader) {
|
|
853
853
|
var headers = Object.assign({}, res.headers || {});
|
|
854
|
-
|
|
854
|
+
// statusHeader defaults to "x-blamejs-cache"; the cache instance can rename
|
|
855
|
+
// it or set it null to suppress (the decision is also on res.cacheStatus).
|
|
856
|
+
var name = (statusHeader === undefined) ? "x-blamejs-cache" : statusHeader;
|
|
857
|
+
if (name) headers[name] = status;
|
|
855
858
|
if (typeof ageSeconds === "number" && ageSeconds >= 0) {
|
|
856
859
|
headers["age"] = String(Math.floor(ageSeconds));
|
|
857
860
|
}
|
|
@@ -893,7 +896,7 @@ function _runWithCache(opts, maxRedirects, runAfter) {
|
|
|
893
896
|
catch (_e) { /* drop-silent */ }
|
|
894
897
|
return _doNetwork(null).then(function (boxed) {
|
|
895
898
|
_maybeStore(cache, method, opts.url, requestHeaders, boxed.res);
|
|
896
|
-
return runAfter(boxed.finalOpts, _withCacheHeaders(boxed.res, "MISS"));
|
|
899
|
+
return runAfter(boxed.finalOpts, _withCacheHeaders(boxed.res, "MISS", undefined, cache.statusHeader));
|
|
897
900
|
});
|
|
898
901
|
}
|
|
899
902
|
|
|
@@ -907,7 +910,7 @@ function _runWithCache(opts, maxRedirects, runAfter) {
|
|
|
907
910
|
catch (_e2) { /* drop-silent */ }
|
|
908
911
|
return _doNetwork(null).then(function (boxed) {
|
|
909
912
|
_maybeStore(cache, method, opts.url, requestHeaders, boxed.res);
|
|
910
|
-
return runAfter(boxed.finalOpts, _withCacheHeaders(boxed.res, "MISS"));
|
|
913
|
+
return runAfter(boxed.finalOpts, _withCacheHeaders(boxed.res, "MISS", undefined, cache.statusHeader));
|
|
911
914
|
});
|
|
912
915
|
}
|
|
913
916
|
|
|
@@ -923,7 +926,7 @@ function _runWithCache(opts, maxRedirects, runAfter) {
|
|
|
923
926
|
body: Buffer.isBuffer(entry.body) ? Buffer.from(entry.body) : entry.body,
|
|
924
927
|
cacheStatus: "HIT",
|
|
925
928
|
};
|
|
926
|
-
return Promise.resolve(runAfter(opts, _withCacheHeaders(hitRes, "HIT", age)));
|
|
929
|
+
return Promise.resolve(runAfter(opts, _withCacheHeaders(hitRes, "HIT", age, cache.statusHeader)));
|
|
927
930
|
}
|
|
928
931
|
|
|
929
932
|
// 4. Stale or must-revalidate. Within stale-while-revalidate or
|
|
@@ -957,7 +960,7 @@ function _runWithCache(opts, maxRedirects, runAfter) {
|
|
|
957
960
|
/* background revalidation best-effort; swallow */
|
|
958
961
|
});
|
|
959
962
|
});
|
|
960
|
-
return Promise.resolve(runAfter(opts, _withCacheHeaders(staleRes, "STALE", ageStale)));
|
|
963
|
+
return Promise.resolve(runAfter(opts, _withCacheHeaders(staleRes, "STALE", ageStale, cache.statusHeader)));
|
|
961
964
|
}
|
|
962
965
|
|
|
963
966
|
// 5. Inline conditional revalidation. Build If-None-Match /
|
|
@@ -974,11 +977,11 @@ function _runWithCache(opts, maxRedirects, runAfter) {
|
|
|
974
977
|
: (rev.refreshed || entry).body,
|
|
975
978
|
cacheStatus: "REVALIDATED",
|
|
976
979
|
};
|
|
977
|
-
return runAfter(opts, _withCacheHeaders(revRes, "REVALIDATED", ageRev));
|
|
980
|
+
return runAfter(opts, _withCacheHeaders(revRes, "REVALIDATED", ageRev, cache.statusHeader));
|
|
978
981
|
}
|
|
979
982
|
if (rev.kind === "fresh-response") {
|
|
980
983
|
_maybeStore(cache, method, opts.url, requestHeaders, rev.res);
|
|
981
|
-
return runAfter(rev.finalOpts || opts, _withCacheHeaders(rev.res, "MISS"));
|
|
984
|
+
return runAfter(rev.finalOpts || opts, _withCacheHeaders(rev.res, "MISS", undefined, cache.statusHeader));
|
|
982
985
|
}
|
|
983
986
|
// rev.kind === "error" — try stale-if-error.
|
|
984
987
|
var sieMs = (evaluation.sieWindowMs || 0);
|
|
@@ -994,7 +997,7 @@ function _runWithCache(opts, maxRedirects, runAfter) {
|
|
|
994
997
|
body: Buffer.isBuffer(entry.body) ? Buffer.from(entry.body) : entry.body,
|
|
995
998
|
cacheStatus: "STALE",
|
|
996
999
|
};
|
|
997
|
-
return runAfter(opts, _withCacheHeaders(sieRes, "STALE", ageErr));
|
|
1000
|
+
return runAfter(opts, _withCacheHeaders(sieRes, "STALE", ageErr, cache.statusHeader));
|
|
998
1001
|
}
|
|
999
1002
|
return Promise.reject(rev.error);
|
|
1000
1003
|
});
|
package/lib/log.js
CHANGED
|
@@ -376,7 +376,12 @@ function create(opts) {
|
|
|
376
376
|
|
|
377
377
|
function middleware(mwOpts) {
|
|
378
378
|
mwOpts = mwOpts || {};
|
|
379
|
-
|
|
379
|
+
// Read and write the SAME header. The raw form keeps the operator's
|
|
380
|
+
// casing (or the canonical "X-Request-Id" default) for the response;
|
|
381
|
+
// the lowercased form matches Node's request-header keys for the read.
|
|
382
|
+
var rawHeaderName = (typeof mwOpts.headerName === "string" && mwOpts.headerName.length > 0)
|
|
383
|
+
? mwOpts.headerName : "X-Request-Id";
|
|
384
|
+
var headerName = rawHeaderName.toLowerCase();
|
|
380
385
|
var setOnRes = mwOpts.setHeader !== false;
|
|
381
386
|
var generate = typeof mwOpts.generate === "function"
|
|
382
387
|
? mwOpts.generate
|
|
@@ -395,7 +400,7 @@ function create(opts) {
|
|
|
395
400
|
id = safeBuffer.stripCrlf(String(id));
|
|
396
401
|
req.id = id;
|
|
397
402
|
if (setOnRes && typeof res.setHeader === "function") {
|
|
398
|
-
try { res.setHeader(
|
|
403
|
+
try { res.setHeader(rawHeaderName, id); } catch (_e) { /* header may be locked */ }
|
|
399
404
|
}
|
|
400
405
|
runWithRequestId(id, function () { next(); });
|
|
401
406
|
};
|
package/lib/mail-crypto-pgp.js
CHANGED
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
* audit: opts.audit, // optional b.audit handle
|
|
85
85
|
* });
|
|
86
86
|
* // → { armored: "-----BEGIN PGP SIGNATURE----- ...",
|
|
87
|
-
* // multipartSigned:
|
|
87
|
+
* // multipartSigned: <Buffer ...>, // RFC 3156 wrapper bytes
|
|
88
88
|
* // signedAt: epochSeconds, fingerprint: "abcd..." }
|
|
89
89
|
*
|
|
90
90
|
* var rv = b.mail.crypto.pgp.verify({
|
|
@@ -564,19 +564,27 @@ function sign(opts) {
|
|
|
564
564
|
// key/cert material flows through createSign/verify, not this path.
|
|
565
565
|
// allow:raw-randombytes-token — boundary string, not auth credential
|
|
566
566
|
var boundary = "blamejs-pgp-" + nodeCrypto.randomBytes(12).toString("hex");
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
567
|
+
// The OpenPGP signature covers the signed-part bytes exactly, so the
|
|
568
|
+
// multipart/signed wrapper is assembled as a Buffer — a JS-string round
|
|
569
|
+
// trip through latin1/utf8 could corrupt non-ASCII signed content and
|
|
570
|
+
// break signature verification. `multipartSigned` is therefore a Buffer.
|
|
571
|
+
var messageBytes = Buffer.isBuffer(message) ? message : Buffer.from(message, "utf8");
|
|
572
|
+
var multipartSigned = Buffer.concat([
|
|
573
|
+
Buffer.from(
|
|
574
|
+
'Content-Type: multipart/signed; micalg="pgp-' + hashName + '"; ' +
|
|
575
|
+
'protocol="application/pgp-signature"; boundary="' + boundary + '"\r\n' +
|
|
576
|
+
"\r\n" +
|
|
577
|
+
"--" + boundary + "\r\n", "utf8"),
|
|
578
|
+
messageBytes,
|
|
579
|
+
Buffer.from(
|
|
580
|
+
"\r\n--" + boundary + "\r\n" +
|
|
581
|
+
'Content-Type: application/pgp-signature; name="signature.asc"\r\n' +
|
|
582
|
+
"Content-Description: OpenPGP digital signature\r\n" +
|
|
583
|
+
'Content-Disposition: attachment; filename="signature.asc"\r\n' +
|
|
584
|
+
"\r\n" +
|
|
585
|
+
armored +
|
|
586
|
+
"--" + boundary + "--\r\n", "utf8"),
|
|
587
|
+
]);
|
|
580
588
|
|
|
581
589
|
// Audit (drop-silent — never crash the request that triggered us).
|
|
582
590
|
_audit(opts.audit, "mail.crypto.pgp.sign", "success", {
|
|
@@ -70,6 +70,7 @@ var AgeGateError = defineClass("AgeGateError", { alwaysPermanent: true });
|
|
|
70
70
|
* hasParentalConsent: function(req): boolean,
|
|
71
71
|
* skipPaths: string[],
|
|
72
72
|
* errorMessage: string,
|
|
73
|
+
* privacyPostureHeader: string, // default "X-Privacy-Posture"; null/false to suppress
|
|
73
74
|
* audit: boolean, // default true
|
|
74
75
|
* }
|
|
75
76
|
*
|
|
@@ -87,7 +88,7 @@ function create(opts) {
|
|
|
87
88
|
opts = opts || {};
|
|
88
89
|
validateOpts(opts, [
|
|
89
90
|
"audit", "getAge", "requireAge", "consentRequired",
|
|
90
|
-
"hasParentalConsent", "skipPaths", "errorMessage",
|
|
91
|
+
"hasParentalConsent", "skipPaths", "errorMessage", "privacyPostureHeader",
|
|
91
92
|
], "middleware.ageGate");
|
|
92
93
|
|
|
93
94
|
if (typeof opts.getAge !== "function") {
|
|
@@ -104,6 +105,17 @@ function create(opts) {
|
|
|
104
105
|
var auditOn = opts.audit !== false;
|
|
105
106
|
var errorMessage = typeof opts.errorMessage === "string" && opts.errorMessage.length > 0
|
|
106
107
|
? opts.errorMessage : "service unavailable without parental consent";
|
|
108
|
+
// privacyPostureHeader (default "X-Privacy-Posture") names the response
|
|
109
|
+
// header carrying the below-threshold classification. Pass null/false to
|
|
110
|
+
// suppress it, or a string to rename it for a downstream convention.
|
|
111
|
+
var privacyPostureHeader;
|
|
112
|
+
if (opts.privacyPostureHeader === null || opts.privacyPostureHeader === false) {
|
|
113
|
+
privacyPostureHeader = null;
|
|
114
|
+
} else if (typeof opts.privacyPostureHeader === "string" && opts.privacyPostureHeader.length > 0) {
|
|
115
|
+
privacyPostureHeader = opts.privacyPostureHeader;
|
|
116
|
+
} else {
|
|
117
|
+
privacyPostureHeader = "X-Privacy-Posture";
|
|
118
|
+
}
|
|
107
119
|
|
|
108
120
|
function _shouldSkip(req) {
|
|
109
121
|
if (skipPaths.length === 0) return false;
|
|
@@ -148,7 +160,7 @@ function create(opts) {
|
|
|
148
160
|
if (typeof res.setHeader === "function") {
|
|
149
161
|
res.setHeader("Cache-Control", "private, no-store");
|
|
150
162
|
res.setHeader("Referrer-Policy", "no-referrer");
|
|
151
|
-
res.setHeader(
|
|
163
|
+
if (privacyPostureHeader) res.setHeader(privacyPostureHeader, classification);
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
166
|
|
|
@@ -66,6 +66,7 @@ var audit = lazyRequire(function () { return require("../audit"); });
|
|
|
66
66
|
* mode: "header"|"html", // default "header"
|
|
67
67
|
* lang: string, // default "en"
|
|
68
68
|
* skipHeader: string, // default "x-skip-ai-act"
|
|
69
|
+
* headerPrefix: string, // default "AI-Act-" — prefixes the Notice/Article/Policy disclosure headers
|
|
69
70
|
* audit: boolean, // default true
|
|
70
71
|
* }
|
|
71
72
|
*
|
|
@@ -83,7 +84,7 @@ function create(opts) {
|
|
|
83
84
|
opts = opts || {};
|
|
84
85
|
validateOpts(opts, [
|
|
85
86
|
"kind", "deployerName", "policyUri", "mode",
|
|
86
|
-
"audit", "lang", "skipHeader",
|
|
87
|
+
"audit", "lang", "skipHeader", "headerPrefix",
|
|
87
88
|
], "middleware.aiActDisclosure");
|
|
88
89
|
|
|
89
90
|
var mode = (opts.mode === "html") ? "html" : "header";
|
|
@@ -99,6 +100,12 @@ function create(opts) {
|
|
|
99
100
|
var skipHeader = (typeof opts.skipHeader === "string" && opts.skipHeader.length > 0)
|
|
100
101
|
? opts.skipHeader.toLowerCase()
|
|
101
102
|
: "x-skip-ai-act";
|
|
103
|
+
// headerPrefix (default "AI-Act-") names the emitted disclosure headers as
|
|
104
|
+
// <prefix>Notice / <prefix>Article / <prefix>Policy. The EU AI Act mandates
|
|
105
|
+
// the disclosure, not the HTTP spelling — operators matching a downstream
|
|
106
|
+
// convention pass their own prefix (e.g. "X-AI-").
|
|
107
|
+
var headerPrefix = (typeof opts.headerPrefix === "string" && opts.headerPrefix.length > 0)
|
|
108
|
+
? opts.headerPrefix : "AI-Act-";
|
|
102
109
|
|
|
103
110
|
return function aiActDisclosureMiddleware(req, res, next) {
|
|
104
111
|
var headers = req.headers || {};
|
|
@@ -118,10 +125,10 @@ function create(opts) {
|
|
|
118
125
|
return origWriteHead.apply(res, arguments);
|
|
119
126
|
}
|
|
120
127
|
var article = _articleFor(opts.kind || "ai-interaction");
|
|
121
|
-
_setHeader(res, "
|
|
122
|
-
_setHeader(res, "
|
|
128
|
+
_setHeader(res, headerPrefix + "Notice", opts.kind || "ai-interaction");
|
|
129
|
+
_setHeader(res, headerPrefix + "Article", article);
|
|
123
130
|
if (typeof opts.policyUri === "string" && opts.policyUri.length > 0) {
|
|
124
|
-
_setHeader(res, "
|
|
131
|
+
_setHeader(res, headerPrefix + "Policy", opts.policyUri);
|
|
125
132
|
}
|
|
126
133
|
injected = true;
|
|
127
134
|
return origWriteHead.apply(res, arguments);
|
|
@@ -364,6 +364,7 @@ function _resolveBackend(opts) {
|
|
|
364
364
|
* statusOnLimit: number, // default 429
|
|
365
365
|
* bodyOnLimit: string, // default "Too Many Requests"
|
|
366
366
|
* header: boolean, // default true
|
|
367
|
+
* headerPrefix: string, // default "X-RateLimit-" — builds <prefix>Limit / <prefix>Remaining (e.g. "RateLimit-" for the IETF draft names)
|
|
367
368
|
* skipPaths: Array<string|RegExp>,
|
|
368
369
|
* scope: "global"|"per-route",
|
|
369
370
|
* backend: "memory"|"cluster"|{ take, reset },
|
|
@@ -390,7 +391,7 @@ function _resolveBackend(opts) {
|
|
|
390
391
|
function create(opts) {
|
|
391
392
|
opts = opts || {};
|
|
392
393
|
validateOpts(opts, [
|
|
393
|
-
"keyFn", "statusOnLimit", "bodyOnLimit", "header", "skipPaths", "scope",
|
|
394
|
+
"keyFn", "statusOnLimit", "bodyOnLimit", "header", "headerPrefix", "skipPaths", "scope",
|
|
394
395
|
"backend", "trustProxy", "algorithm",
|
|
395
396
|
// memory backend (token-bucket)
|
|
396
397
|
"burst", "refillPerSecond",
|
|
@@ -404,6 +405,14 @@ function create(opts) {
|
|
|
404
405
|
var statusOnLimit = opts.statusOnLimit || 429;
|
|
405
406
|
var bodyOnLimit = opts.bodyOnLimit !== undefined ? opts.bodyOnLimit : "Too Many Requests";
|
|
406
407
|
var emitHeaders = opts.header !== false;
|
|
408
|
+
// headerPrefix (default "X-RateLimit-") builds the limit/remaining header
|
|
409
|
+
// names as <prefix>Limit / <prefix>Remaining. The X-RateLimit-* family is a
|
|
410
|
+
// de-facto convention, not RFC-pinned — operators matching the IETF draft
|
|
411
|
+
// pass "RateLimit-", or a gateway's own prefix. Kept as a matched pair.
|
|
412
|
+
var headerPrefix = (typeof opts.headerPrefix === "string" && opts.headerPrefix.length > 0)
|
|
413
|
+
? opts.headerPrefix : "X-RateLimit-";
|
|
414
|
+
var limitHeader = headerPrefix + "Limit";
|
|
415
|
+
var remainingHeader = headerPrefix + "Remaining";
|
|
407
416
|
var skipPaths = opts.skipPaths || [];
|
|
408
417
|
// Throw at create(): each entry must be a string prefix or a RegExp.
|
|
409
418
|
// Anything else would crash _shouldSkip with TypeError on the first request.
|
|
@@ -429,8 +438,8 @@ function create(opts) {
|
|
|
429
438
|
|
|
430
439
|
function _writeBlocked(req, res, k, verdict) {
|
|
431
440
|
if (emitHeaders && typeof res.setHeader === "function") {
|
|
432
|
-
res.setHeader(
|
|
433
|
-
res.setHeader(
|
|
441
|
+
res.setHeader(limitHeader, String(verdict.limit));
|
|
442
|
+
res.setHeader(remainingHeader, String(verdict.remaining));
|
|
434
443
|
if (verdict.retryAfter > 0) res.setHeader("Retry-After", String(verdict.retryAfter));
|
|
435
444
|
}
|
|
436
445
|
try {
|
|
@@ -459,8 +468,8 @@ function create(opts) {
|
|
|
459
468
|
|
|
460
469
|
function _handle(verdict) {
|
|
461
470
|
if (emitHeaders && typeof res.setHeader === "function") {
|
|
462
|
-
res.setHeader(
|
|
463
|
-
res.setHeader(
|
|
471
|
+
res.setHeader(limitHeader, String(verdict.limit));
|
|
472
|
+
res.setHeader(remainingHeader, String(verdict.remaining));
|
|
464
473
|
}
|
|
465
474
|
if (!verdict.allowed) return _writeBlocked(req, res, k, verdict);
|
|
466
475
|
next();
|
package/lib/middleware/sse.js
CHANGED
|
@@ -90,6 +90,7 @@ function _formatEvent(msg) {
|
|
|
90
90
|
* {
|
|
91
91
|
* heartbeatMs: number|false, // default 15000
|
|
92
92
|
* headers: object, // extra response headers
|
|
93
|
+
* proxyBuffer: boolean, // default true — sets X-Accel-Buffering: no; false to suppress
|
|
93
94
|
* }
|
|
94
95
|
*
|
|
95
96
|
* @example
|
|
@@ -105,13 +106,17 @@ function create(handler, opts) {
|
|
|
105
106
|
throw new Error("middleware.sse: handler must be a function (channel, req) => ...");
|
|
106
107
|
}
|
|
107
108
|
opts = opts || {};
|
|
108
|
-
validateOpts(opts, ["heartbeatMs", "headers"], "middleware.sse");
|
|
109
|
+
validateOpts(opts, ["heartbeatMs", "headers", "proxyBuffer"], "middleware.sse");
|
|
109
110
|
var heartbeatMs = opts.heartbeatMs === false ? 0
|
|
110
111
|
: (opts.heartbeatMs != null ? opts.heartbeatMs : DEFAULT_HEARTBEAT_MS);
|
|
111
112
|
if (heartbeatMs !== 0 && (typeof heartbeatMs !== "number" || !isFinite(heartbeatMs) || heartbeatMs <= 0)) {
|
|
112
113
|
throw new Error("middleware.sse: heartbeatMs must be a positive finite number or false");
|
|
113
114
|
}
|
|
114
115
|
var extraHeaders = opts.headers || {};
|
|
116
|
+
// proxyBuffer (default true) sets `X-Accel-Buffering: no` (the nginx hint
|
|
117
|
+
// that disables proxy buffering). Pass false when not behind nginx, or
|
|
118
|
+
// when buffering is controlled at the load balancer, to suppress it.
|
|
119
|
+
var proxyBuffer = opts.proxyBuffer !== false;
|
|
115
120
|
|
|
116
121
|
return async function sseMiddleware(req, res) {
|
|
117
122
|
if (typeof res.writeHead !== "function" || typeof res.write !== "function") {
|
|
@@ -119,13 +124,14 @@ function create(handler, opts) {
|
|
|
119
124
|
// unusual. Fail closed rather than silently dropping the handler.
|
|
120
125
|
throw new Error("middleware.sse: res does not support writeHead/write — wire SSE only on HTTP routes");
|
|
121
126
|
}
|
|
122
|
-
var
|
|
123
|
-
"Content-Type":
|
|
124
|
-
"Cache-Control":
|
|
125
|
-
"Connection":
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
var baseHeaders = {
|
|
128
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
129
|
+
"Cache-Control": "no-cache, no-transform",
|
|
130
|
+
"Connection": "keep-alive",
|
|
131
|
+
};
|
|
132
|
+
// Disable nginx response buffering when terminating behind it.
|
|
133
|
+
if (proxyBuffer) baseHeaders["X-Accel-Buffering"] = "no";
|
|
134
|
+
var headers = Object.assign(baseHeaders, extraHeaders);
|
|
129
135
|
// Append Vary: Accept so a proxy doesn't serve a cached non-SSE
|
|
130
136
|
// response on the same URL to a future client.
|
|
131
137
|
res.writeHead(requestHelpers.HTTP_STATUS.OK, headers);
|
package/lib/network-tls.js
CHANGED
|
@@ -3066,9 +3066,13 @@ function checkServerIdentity9525(host, cert) {
|
|
|
3066
3066
|
}
|
|
3067
3067
|
var rawSan = cert.subjectaltname;
|
|
3068
3068
|
if (typeof rawSan !== "string" || rawSan.length === 0) {
|
|
3069
|
-
// RFC 9525 §6.4.4 forbids CN fallback.
|
|
3070
|
-
//
|
|
3071
|
-
//
|
|
3069
|
+
// RFC 9525 §6.4.4 forbids CN fallback. A CN-only legacy cert (CN
|
|
3070
|
+
// present, no SAN) surfaces the distinct `tls/pkix-cn-fallback-refused`
|
|
3071
|
+
// code so audit logs can tell it apart from a cert carrying neither;
|
|
3072
|
+
// a cert with no SAN and no CN falls through to `tls/pkix-san-required`.
|
|
3073
|
+
// Both refuse — we never fall back to matching on the Common Name.
|
|
3074
|
+
var cnRefusal = _refuseCnFallback(host, cert);
|
|
3075
|
+
if (cnRefusal) return cnRefusal;
|
|
3072
3076
|
return new NetworkTlsError("tls/pkix-san-required",
|
|
3073
3077
|
"checkServerIdentity9525: certificate has no subjectAltName " +
|
|
3074
3078
|
"extension (RFC 9525 §6.4.4 forbids Common Name fallback)");
|
|
@@ -3117,10 +3121,11 @@ function _refuseCnFallback(host, cert) {
|
|
|
3117
3121
|
return null;
|
|
3118
3122
|
}
|
|
3119
3123
|
|
|
3120
|
-
//
|
|
3121
|
-
//
|
|
3122
|
-
//
|
|
3123
|
-
//
|
|
3124
|
+
// Explicit combined verifier kept for tests + callers that want the
|
|
3125
|
+
// CN-fallback / SAN-required split spelled out. The exported drop-in
|
|
3126
|
+
// `checkServerIdentity9525` already performs the CN-fallback refusal in
|
|
3127
|
+
// its no-SAN branch, so the `_refuseCnFallback` call here is a redundant
|
|
3128
|
+
// (idempotent) belt-and-suspenders; the more specific code wins either way.
|
|
3124
3129
|
function _checkServerIdentityStrict(host, cert) {
|
|
3125
3130
|
var cnRefusal = _refuseCnFallback(host, cert);
|
|
3126
3131
|
if (cnRefusal) return cnRefusal;
|
package/lib/protobuf-encoder.js
CHANGED
|
@@ -82,6 +82,14 @@ function _writeVarint(value) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function _tag(fieldNumber, wireType) {
|
|
85
|
+
// `fieldNumber << 3` uses JS's 32-bit signed shift, which overflows and
|
|
86
|
+
// emits a wrong tag once fieldNumber reaches 2^28. Reject anything outside
|
|
87
|
+
// the safe single-shift range rather than encode silently wrong — the OTLP
|
|
88
|
+
// schema this serves uses small field numbers well within it.
|
|
89
|
+
if (fieldNumber < 1 || fieldNumber > 268435455) { // 2^28 - 1
|
|
90
|
+
throw new RangeError("protobuf: field number " + fieldNumber +
|
|
91
|
+
" out of range (1..2^28-1)");
|
|
92
|
+
}
|
|
85
93
|
return _writeVarint((fieldNumber << 3) | wireType);
|
|
86
94
|
}
|
|
87
95
|
|
package/lib/queue.js
CHANGED
|
@@ -417,8 +417,10 @@ function consume(queueName, handler, opts) {
|
|
|
417
417
|
}
|
|
418
418
|
var jobs;
|
|
419
419
|
try { jobs = await b.lease(queueName, leaseDurationMs, slots); }
|
|
420
|
-
catch {
|
|
421
|
-
// Backend down (breaker open, etc.) — back off
|
|
420
|
+
catch (e) {
|
|
421
|
+
// Backend down (breaker open, etc.) — log + back off so a flapping
|
|
422
|
+
// backend that hasn't yet tripped the breaker is still visible.
|
|
423
|
+
log.debug("lease-failed", { op: "b.lease", queue: queueName, error: e.message });
|
|
422
424
|
await _pollSleep(pollIntervalMs);
|
|
423
425
|
continue;
|
|
424
426
|
}
|
package/lib/sse.js
CHANGED
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
* input (default SseError)
|
|
28
28
|
* audit — bool, default true. Emit SSE lifecycle audit
|
|
29
29
|
* events.
|
|
30
|
+
* proxyBuffer — bool, default true. Sets `X-Accel-Buffering:
|
|
31
|
+
* no` (the nginx hint that disables proxy
|
|
32
|
+
* buffering of the stream). Pass false when not
|
|
33
|
+
* behind nginx, or when buffering is handled at
|
|
34
|
+
* the load balancer, to suppress the header.
|
|
30
35
|
*
|
|
31
36
|
* channel.send({ event, id, data, retry })
|
|
32
37
|
* Writes a single SSE event. Each field is validated; LF/CR/NUL
|
|
@@ -236,18 +241,20 @@ function create(req, res, opts) {
|
|
|
236
241
|
JSON.stringify(heartbeatMs) + ")");
|
|
237
242
|
}
|
|
238
243
|
var auditOn = opts.audit !== false;
|
|
244
|
+
// proxyBuffer (default true) sets `X-Accel-Buffering: no` — the nginx hint
|
|
245
|
+
// that defeats proxy buffering of the event stream. Operators not behind
|
|
246
|
+
// nginx, or whose buffering is controlled at the load balancer, pass
|
|
247
|
+
// proxyBuffer: false to suppress the nginx-specific header.
|
|
248
|
+
var proxyBuffer = opts.proxyBuffer !== false;
|
|
239
249
|
|
|
240
250
|
var lastEventId = _readLastEventId(req);
|
|
241
251
|
|
|
242
252
|
// Headers. text/event-stream is the contract; Cache-Control: no-cache
|
|
243
|
-
// and Connection: keep-alive (h1) are the operationally required
|
|
244
|
-
// pair. X-Accel-Buffering: no defeats nginx-style proxy buffering;
|
|
245
|
-
// operators behind a proxy that doesn't honor this set proxyBuffer:
|
|
246
|
-
// false on their LB.
|
|
253
|
+
// and Connection: keep-alive (h1) are the operationally required pair.
|
|
247
254
|
if (typeof res.setHeader === "function") {
|
|
248
255
|
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
249
256
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
250
|
-
res.setHeader("X-Accel-Buffering", "no");
|
|
257
|
+
if (proxyBuffer) res.setHeader("X-Accel-Buffering", "no");
|
|
251
258
|
// Connection: keep-alive only meaningful on h1; h2 streams stay
|
|
252
259
|
// open until either side closes. node:http2 surfaces res.stream
|
|
253
260
|
// (h2 ServerHttp2Stream) where setHeader works the same.
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:5ae2d0f7-e30f-4d02-90d0-975129430c5f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-30T05:49:41.774Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.14.1",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.14.1",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.14.1",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.
|
|
57
|
+
"ref": "@blamejs/core@0.14.1",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|