@blamejs/core 0.16.8 → 0.16.10
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/index.js +4 -0
- package/lib/auth/ciba.js +7 -0
- package/lib/cache-status.js +3 -7
- package/lib/compliance-ai-act.js +6 -8
- package/lib/crypto.js +2 -2
- package/lib/gate-contract.js +13 -8
- package/lib/guard-archive.js +2 -2
- package/lib/guard-time.js +1 -1
- package/lib/guard-uuid.js +2 -2
- package/lib/html-balance.js +1 -1
- package/lib/http-message-signature.js +1 -1
- package/lib/link-header.js +2 -1
- package/lib/log-stream.js +3 -1
- package/lib/mail-auth.js +1 -1
- package/lib/mail-server-imap.js +3 -4
- package/lib/mail-server-managesieve.js +7 -11
- package/lib/middleware/index.js +1 -0
- package/lib/safe-buffer.js +36 -0
- package/lib/server-timing.js +2 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.16.x
|
|
10
10
|
|
|
11
|
+
- v0.16.10 (2026-07-11) — **Restore five APIs that silently didn't work — certificate-fingerprint hashing from PEM, the tus-upload close handler, the mail-delivery factory, the log-stream local-sink env boot, and the CIBA internal-IdP opt — and make single-base guard profile composition work, all surfaced by writing a test for every documented primitive.** Writing a behavioral test for every documented primitive that previously had none surfaced a batch of APIs that were advertised but silently non-working, now fixed at the root. b.crypto.hashCertFingerprint and b.crypto.isCertRevoked threw a TypeError on the PEM-string input their own examples pass (a helper was called on its lazy-require getter instead of the module), so only the raw-DER path worked; b.middleware.tusUpload.close and b.mail.send.deliver.create were documented but never attached to their export, so calling the documented form threw 'not a function'; b.logStream.bootFromEnv advertised a 'local' protocol that threw at init because it wired the sink with the wrong option; and the CIBA client dropped the allowInternal opt on its own backchannel and token POSTs, so an operator who explicitly opted into an internal or loopback identity provider was still SSRF-blocked. Separately, gate-contract profile composition now honors its documented extends: string | string[] contract — a single base profile name passed as a bare string was silently ignored, which made the buildProfile example every guard shares a no-op. The EU AI Act fundamental-rights-impact-assessment and GPAI training-data-summary builders now raise typed ComplianceError refusals with descriptive messages instead of a bare Error whose message was only the error code. Five documentation examples are corrected to their real emitted values (the SRI integrity attribute, two guard rule-ids, a guard issue kind, and the guard-archive profile-composition call). The static gate that requires every documented primitive to be referenced by a test now has an empty backlog register: every documented primitive is exercised. **Added:** *A test for every documented primitive* — The primitive-without-test static gate's backlog register, which listed documented primitives that no test referenced, is now empty: every documented primitive is exercised by a behavioral test through its real consumer path. A new primitive must land with its own test reference; the register accepts an entry only with a specific documented reason coverage is indirect. **Changed:** *Guard profile composition accepts a single base profile as a bare string* — gate-contract profile composition documents extends: string | string[], but a bare-string extends was silently ignored — only the array form composed — which made the buildProfile example every guard shares (buildProfile({ extends: "strict" })) a no-op that returned an empty caps object. A string extends is now normalized to a one-element list, so single-base composition works across every b.guard*.buildProfile. The array form is unchanged. **Fixed:** *b.crypto.hashCertFingerprint / isCertRevoked accept PEM input again* — Both threw 'TypeError: safeBuffer.byteLengthOf is not a function' on a PEM-string certificate — the internal PEM-to-DER step called the byte-length helper on its lazy-require getter rather than the resolved module, so only the pre-parsed DER/Buffer path (which returns before that line) worked. Their documented examples pass a PEM string, so the advertised behavior was silently broken. Fixed at the call site. · *b.middleware.tusUpload.close is reachable* — The middleware export wired tusUpload.create and .memoryStore but never attached the documented .close handler, so b.middleware.tusUpload.close(middleware) threw 'not a function'. It is now attached to the export. · *b.mail.send.deliver.create is reachable* — b.mail.send.deliver was bound directly to the factory function but never carried the documented .create property, so the documented b.mail.send.deliver.create(opts) form resolved to undefined. The .create factory is now attached; the existing collapsed b.mail.send.deliver(opts) callable is unchanged. · *b.logStream.bootFromEnv local protocol works* — BLAMEJS_LOG_STREAM_PROTOCOL=local threw 'log-stream local requires { dir }' at init: bootFromEnv wired the local sink with a path option, but the directory-based local sink requires dir. BLAMEJS_LOG_STREAM_PATH is now mapped to the sink directory, so the advertised local protocol boots and writes end-to-end. · *CIBA client honors allowInternal on its own requests* — The backchannel-authentication and token POSTs of the CIBA client dropped the allowInternal option that create() accepts and threads to the OAuth client for discovery/JWKS, so an operator who explicitly set allowInternal:true for an internal or loopback identity provider was still refused with ssrf-guard/blocked-loopback on exactly those two endpoints. The option is now threaded through per request. The SSRF guard stays on by default; nothing changes unless the operator opts in. · *EU AI Act FRIA / training-data-summary raise typed refusals with real messages* — b.compliance.aiAct.fundamentalRightsImpactAssessment and b.compliance.aiAct.gpai.trainingDataSummary validated required fields by passing the built-in Error as the error class, so the descriptive message was dropped (its slot became the Error options bag) and the thrown message was only the error code. Both now raise a typed ComplianceError with the descriptive message and a stable code, matching the sibling adherenceForm builder. · *Five documentation examples corrected to real values* — The b.crypto.sri example carried a fabricated SHA-384 value (an operator copy-pasting the integrity attribute would get a mismatch); it now shows the correct digest. The b.guardTime.validate example named rule-id time.year-out-of-range (actual time.year-window); b.guardUuid named uuid.nil / uuid.max (actual uuid.nil-uuid / uuid.max-uuid); b.htmlBalance.checkSafe named issue kind event-handler-attribute (actual event-handler). The b.guardArchive.buildProfile example passed { profile: "strict" }, which resolves no caps; it now uses the correct { extends: "strict" } form.
|
|
12
|
+
|
|
13
|
+
- v0.16.9 (2026-07-10) — **`b.safeBuffer.quoteString` — one RFC quoted-string serializer behind eight header and protocol emitters — plus a consolidated per-primitive test suite and a static gate that refuses a documented primitive no test references.** Eight places in the framework serialized an RFC quoted-string by hand — escape backslash and DQUOTE, wrap in DQUOTEs — for Cache-Status and Signature-Input sf-strings, Link header parameters, Authentication-Results reason, IMAP quoted strings and METADATA values, ManageSieve responses, and Server-Timing descriptions. Those copies are consolidated into one primitive, b.safeBuffer.quoteString, so an unescaped quote can never terminate a string early and smuggle extra parameters into a protocol line, and the escaping cannot drift between emitters. Output is byte-identical to before. The test suite is reorganized so every primitive's tests live in a canonical per-primitive file (thirty-eight auxiliary files folded or renamed; every assertion preserved — the full suite passes with the exact same check count). On top of that reorganization, the comment-block validator gains a primitive-without-test check: a documented @primitive that no test references — neither the full dotted form nor its namespace plus a method invocation — now fails the static gate. One hundred six existing primitives currently lack any test reference; they are recorded in an explicit shrink-only register in the validator, each entry an open test-backfill item, and the gate refuses any new primitive shipping untested. **Added:** *b.safeBuffer.quoteString — RFC quoted-string serialization* — Coerces to string, escapes every backslash and DQUOTE with a leading backslash, and wraps the result in DQUOTEs — the quoted-string grammar shared by RFC 8941 §3.3.3 Structured Fields sf-string, RFC 8288 Link header parameters, RFC 8601 §2.2 Authentication-Results reason, RFC 3501 §4.3 IMAP quoted strings, and RFC 5804 §1.2 ManageSieve strings. Escaping only: a grammar with a restricted character range (sf-string is printable-ASCII; IMAP quoted strings cannot carry CR/LF) enforces its range check before calling it. · *Static gate: a documented primitive must be referenced by a test* — The comment-block validator now scans the test corpus for every @primitive: the full dotted form (b.namespace.method) anywhere under test/, or the namespace / owning module referenced together with the method invoked in the same test file — the same-file rule stops a ubiquitous method name like .create( in an unrelated test from counting as coverage. A primitive with no reference fails the pre-push static gate. One hundred six existing primitives currently have no reference; they are listed in a shrink-only register inside the validator — each entry an open test-backfill item that is deleted when its test lands — and a new primitive cannot ship untested. **Changed:** *Eight quoted-string emitters route through the one serializer* — Cache-Status key/detail, Signature-Input sf-strings, Link header parameter values, Authentication-Results reason, IMAP METADATA values and quoted strings, ManageSieve OK/NO/BYE/LISTSCRIPTS responses, and Server-Timing desc now compose b.safeBuffer.quoteString instead of hand-rolling the escape. Emitted bytes are unchanged. · *Per-primitive canonical test files* — Thirty-eight auxiliary test files are folded into (or renamed to) their primitive's canonical test file — one file per primitive domain, concern-named splits kept (e.g. http-client-cache / -stream / -throttle). No assertion was changed, added, or removed by the reorganization; the full suite passes with the exact same check count before and after. The b.*-surface reference gate that previously ran inside the smoke suite is superseded by the stronger validator check above. **Detectors:** *rfc-quoted-string-escape-owned-by-safeBuffer* — Flags an inline backslash-then-DQUOTE escape chain in lib/ outside the primitive's home — the quoted-string serializer re-implemented, whose escaping can drift from the canonical one. While being proven against the pre-consolidation tree it caught an eighth emitter the manual sweep had missed (an IMAP response-literal helper), which is consolidated in this release too.
|
|
14
|
+
|
|
11
15
|
- v0.16.8 (2026-07-10) — **`b.metrics.snapshot.render` now emits the labeled registry — counters, gauges and histogram buckets with their label sets — matching the live exposition endpoint byte-for-byte, plus a Public Suffix List refresh and supply-chain pin maintenance.** A snapshot written with b.metrics.snapshot.startWriter's registry option always carried the full labeled registry in its metrics field, but b.metrics.snapshot.render ignored that field: the prometheus format emitted only the flat numeric fields, so a sidecar wanting a complete /metrics exposition of labeled series had to re-implement label-value escaping and bucketed line formatting itself. Both render formats now emit the labeled series. The prometheus output is produced by the same family encoder the live exposition() endpoint uses — one encoder now backs the live exposition, the shadow registry and snapshot rendering — so series names and escaping are identical whichever source a scraper reads. Snapshot files are parsed defensively: a malformed family, a non-Prometheus metric or label name, or a non-numeric sample in a hand-edited snapshot file is dropped rather than rendered, so a tampered file cannot forge exposition lines. Separately, twelve documentation examples opened with require("blamejs").create(), a factory the export does not have; the examples are corrected and the @example-execution test no longer supplies a substitute create(), so any future example calling a method the shipped export lacks fails the suite. The vendored Public Suffix List is refreshed to the 2026-07-09 upstream revision, and the CI toolchain pins (github/codeql-action, the ClusterFuzzLite base-builder digest) are brought current. **Changed:** *One shared Prometheus family encoder behind every exposition surface* — The live registry exposition(), the shadow registry's prometheus render and the new snapshot rendering now share a single labeled-sample encoder (label ordering, value escaping, histogram bucket lines). Output of the existing surfaces is unchanged, including the guarantee that a label literally named constructor / prototype / __proto__ survives rendering. · *Public Suffix List refreshed to the 2026-07-09 upstream revision* — The vendored, signature-verified Public Suffix List bundle is rebuilt from the current upstream publication (previously 2026-07-02). This keeps DMARC / BIMI organizational-domain alignment and cookie-scope decisions current with registry changes. · *CI toolchain pins brought current* — github/codeql-action is pinned to v4.37.0 across the CodeQL and Scorecard workflows in one change (its init / analyze / upload-sarif sub-actions must move together — mixed versions break the CodeQL analysis run), and Dependabot now groups codeql-action updates into a single pull request. The ClusterFuzzLite base-builder image digest is bumped to the current upstream publication, with the oss-fuzz submission mirror kept in sync. **Fixed:** *b.metrics.snapshot.render emits the labeled registry from snap.metrics (#430)* — Snapshots written with startWriter's registry option carry every registered counter / gauge / histogram — label sets and bucket counts — in a structured metrics field, but render() only emitted the flat numeric fields. The prometheus format now renders those families with the same labeled / bucketed sample lines (name{label="value"} n, name_bucket{le=…}, name_sum, name_count) the live exposition() endpoint serves, family names verbatim rather than prefix-qualified, so dashboards see one series name regardless of scrape source; the text format lists each labeled sample as a name{label="value"} row. Malformed families, non-Prometheus metric / label names, and non-numeric samples in a snapshot file are dropped rather than rendered, so a hand-edited file cannot forge exposition lines. · *Twelve documentation examples no longer call a nonexistent create() factory* — @example blocks in b.a2a, b.cloudEvents, b.pqcSoftware and b.tlsExporter opened with var b = require("blamejs").create(), but the top-level export has no create() — copying those examples threw a TypeError on the first line. The examples now open with var b = require("blamejs"). The @example-execution test previously supplied a substitute create() to the examples' require, which masked exactly this drift; the substitute is removed, so an example calling any method the shipped export lacks now fails the suite. **Detectors:** *prometheus-exposition-escape-owned-by-metrics* — Flags a hand-rolled Prometheus escape chain (backslash-doubling plus newline escaping) outside lib/metrics.js — the signature of a second exposition encoder growing whose escaping can drift from the canonical one. The two-step backslash+quote escapes used by RFC quoted-string wire formats (sf-string, IMAP, Sieve, Link header) are out of scope.
|
|
12
16
|
|
|
13
17
|
- v0.16.7 (2026-07-06) — **Restore two APIs that silently didn't work — the Clear-Site-Data header helper and data-subject export — and correct five documentation examples, all surfaced by a new test that executes every JSDoc @example end-to-end.** The comment-block validator only ever PARSE-checked each documentation @example, so an example could compile and still be dead — calling a method that no longer exists, passing an option the API rejects, or misreading a return shape. A new test now EXECUTES every self-contained @example against the real framework, and on its first run it caught two genuine framework defects plus five stale examples. b.middleware.clearSiteData.headerValue (documented @status stable since 0.15.9) was unreachable: the middleware export was a bare factory that never carried the headerValue helper (nor KNOWN_TYPES / DEFAULT_TYPES), so calling it threw — it is now attached to the export, mirroring b.middleware.idempotencyKey. b.subject.export / b.subject.exportData returned undefined instead of the documented empty object when no data-subject tables are declared, so an operator running an export before tagging any subjectField column got undefined and crashed on the first property access — it now returns {}. Five @example blocks are corrected to runnable code (b.guardRegex.gate's return usage; the invalid title option on b.openapi.create / b.asyncapi.create, which belongs under info; b.retention.complianceFloor; b.safeSchema.object). No security surface changes. **Added:** *JSDoc @example execution validation* — A new test executes every self-contained JSDoc @example against the real framework — not just parse-checks it — so an example that references a renamed or removed method, passes an option the API rejects, or misreads a return shape now fails the suite instead of shipping as dead documentation. Examples with side effects (network, database, filesystem, long-lived work) or that abstract external setup are skipped; the executed set runs in a sandbox with a wall-clock ceiling. **Fixed:** *b.middleware.clearSiteData.headerValue is reachable again* — b.middleware.clearSiteData was exported as a bare factory function, so the advertised b.middleware.clearSiteData.headerValue(types, label?) — plus KNOWN_TYPES and DEFAULT_TYPES — were undefined off the export and threw a TypeError, even though the helper (documented stable since 0.15.9) existed on the module. Those members are now attached to the middleware export, so the documented call works. · *b.subject.export / exportData returns {} when no data-subject tables are declared* — On the no-subject-tables path, export returned the value of its internal audit-write helper — which has no return statement, i.e. undefined — instead of the documented empty dump. An operator running a data-subject export before tagging any subjectField column therefore received undefined and crashed on Object.keys(dump) / dump.<table>. It now returns {} as documented (export and exportData are the same function). · *Five documentation examples corrected to runnable code* — b.guardRegex.gate's example now uses the gate return value correctly; b.openapi.create and b.asyncapi.create examples move title/version under info (title is not a top-level option); and the b.retention.complianceFloor and b.safeSchema.object examples are made self-contained. These are documentation-only corrections.
|
package/index.js
CHANGED
|
@@ -301,6 +301,10 @@ mail.rbl = require("./lib/mail-rbl");
|
|
|
301
301
|
mail.serverRegistry = require("./lib/mail-server-registry");
|
|
302
302
|
mail.send = mail.send || {};
|
|
303
303
|
mail.send.deliver = require("./lib/mail-send-deliver").create;
|
|
304
|
+
// Expose the documented `b.mail.send.deliver.create(opts)` factory form
|
|
305
|
+
// (its @signature / @example) alongside the collapsed callable so an
|
|
306
|
+
// operator following the docs reaches the same builder.
|
|
307
|
+
mail.send.deliver.create = require("./lib/mail-send-deliver").create;
|
|
304
308
|
mail.send.deliver.DeliverError = require("./lib/mail-send-deliver").DeliverError;
|
|
305
309
|
mail.greylist = require("./lib/mail-greylist");
|
|
306
310
|
mail.helo = require("./lib/mail-helo");
|
package/lib/auth/ciba.js
CHANGED
|
@@ -278,6 +278,13 @@ function create(opts) {
|
|
|
278
278
|
};
|
|
279
279
|
Object.assign(req, opts.httpClientOpts || {});
|
|
280
280
|
if (opts.allowHttp === true) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
|
|
281
|
+
// Thread the operator's explicit SSRF waiver onto the backchannel-auth
|
|
282
|
+
// and token POSTs too — create() already hands allowInternal to the
|
|
283
|
+
// inner OAuth client for discovery / JWKS, so an internal-network IdP
|
|
284
|
+
// whose discovery resolves would still have its /bc-auth and /token
|
|
285
|
+
// requests blocked here without this. Matches oauth's per-request
|
|
286
|
+
// threading; the SSRF guard stays on unless allowInternal is set.
|
|
287
|
+
if (opts.allowInternal != null) req.allowInternal = opts.allowInternal;
|
|
281
288
|
var res = await hc.request(req);
|
|
282
289
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
283
290
|
var bodyText = res.body ? res.body.toString("utf8") : "";
|
package/lib/cache-status.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* RFC 9211 Cache-Status header — documents which intermediate caches handled a request with structured `hit` / `fwd` / `ttl` parameters so operators diagnose cache-decision chains.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
var safeBuffer = require("./safe-buffer");
|
|
37
38
|
var validateOpts = require("./validate-opts");
|
|
38
39
|
var { defineClass } = require("./framework-error");
|
|
39
40
|
|
|
@@ -58,11 +59,6 @@ var BOOLEAN_PARAMS = Object.freeze(["hit", "stored", "collapsed"]);
|
|
|
58
59
|
// passing other keys get passed-through verbatim as token=value.
|
|
59
60
|
var KNOWN_PARAMS = Object.freeze(["hit", "fwd", "fwd-status", "ttl", "stored", "collapsed", "key", "detail"]);
|
|
60
61
|
|
|
61
|
-
function _sfStringQuote(s) {
|
|
62
|
-
// RFC 8941 sf-string — quoted-string with escaping for " and \.
|
|
63
|
-
// Operator-supplied detail/key strings get the full quote-escape.
|
|
64
|
-
return "\"" + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
|
|
65
|
-
}
|
|
66
62
|
|
|
67
63
|
/**
|
|
68
64
|
* @primitive b.cacheStatus.append
|
|
@@ -178,14 +174,14 @@ function entryString(entry) {
|
|
|
178
174
|
throw new CacheStatusError("cache-status/bad-key",
|
|
179
175
|
"entry.key must be a string when provided");
|
|
180
176
|
}
|
|
181
|
-
parts.push("key=" +
|
|
177
|
+
parts.push("key=" + safeBuffer.quoteString(entry.key));
|
|
182
178
|
}
|
|
183
179
|
if (entry.detail !== undefined && entry.detail !== null) {
|
|
184
180
|
if (typeof entry.detail !== "string") {
|
|
185
181
|
throw new CacheStatusError("cache-status/bad-detail",
|
|
186
182
|
"entry.detail must be a string when provided");
|
|
187
183
|
}
|
|
188
|
-
parts.push("detail=" +
|
|
184
|
+
parts.push("detail=" + safeBuffer.quoteString(entry.detail));
|
|
189
185
|
}
|
|
190
186
|
return parts.join("; ");
|
|
191
187
|
}
|
package/lib/compliance-ai-act.js
CHANGED
|
@@ -591,11 +591,10 @@ function deployerChecklist(assessment) {
|
|
|
591
591
|
* });
|
|
592
592
|
*/
|
|
593
593
|
function fundamentalRightsImpactAssessment(opts) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
594
|
+
validateOpts.requireObject(opts, "fundamentalRightsImpactAssessment",
|
|
595
|
+
ComplianceError, "compliance-ai-act/bad-input");
|
|
597
596
|
validateOpts.requireNonEmptyString(opts.systemId, "systemId",
|
|
598
|
-
|
|
597
|
+
ComplianceError, "compliance-ai-act/no-system-id");
|
|
599
598
|
return {
|
|
600
599
|
"$schema": "https://blamejs.com/schema/ai-act-fria-v1.json",
|
|
601
600
|
regulation: "EU Regulation 2024/1689 — AI Act",
|
|
@@ -657,11 +656,10 @@ function fundamentalRightsImpactAssessment(opts) {
|
|
|
657
656
|
* });
|
|
658
657
|
*/
|
|
659
658
|
function trainingDataSummary(opts) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
659
|
+
validateOpts.requireObject(opts, "trainingDataSummary",
|
|
660
|
+
ComplianceError, "compliance-ai-act/bad-input");
|
|
663
661
|
validateOpts.requireNonEmptyString(opts.modelId, "modelId",
|
|
664
|
-
|
|
662
|
+
ComplianceError, "compliance-ai-act/no-model-id");
|
|
665
663
|
return {
|
|
666
664
|
"$schema": "https://blamejs.com/schema/ai-act-gpai-training-summary-v1.json",
|
|
667
665
|
regulation: "EU Regulation 2024/1689 — AI Act",
|
package/lib/crypto.js
CHANGED
|
@@ -957,7 +957,7 @@ var SRI_ALGORITHMS = { "sha256": "sha256", "sha384": "sha384", "sha512": "sha512
|
|
|
957
957
|
*
|
|
958
958
|
* @example
|
|
959
959
|
* var attr = b.crypto.sri(Buffer.from("alert(1);", "utf8"), { algorithm: "sha384" });
|
|
960
|
-
* // → "sha384-
|
|
960
|
+
* // → "sha384-dnux3uAPxaf+IhCrFG1D/XVNzP1XLDNcn3Pe3jyxouEAoot5kfwC5u8rMwNhE5oi"
|
|
961
961
|
*
|
|
962
962
|
* var multi = b.crypto.sri(["payload-a", "payload-b"], { algorithm: "sha512" });
|
|
963
963
|
* // → "sha512-... sha512-..." (two tokens, space-separated)
|
|
@@ -1899,7 +1899,7 @@ function _pemToDer(pemOrDer) {
|
|
|
1899
1899
|
// for mTLS bootstrap / webhook verification / peer-cert pinning.
|
|
1900
1900
|
// 64 KiB caps the largest plausible PEM (a P-384 cert + chain) at
|
|
1901
1901
|
// ~3× margin while refusing pathological inputs outright.
|
|
1902
|
-
if (safeBuffer.byteLengthOf(pemOrDer) > C.BYTES.kib(64)) {
|
|
1902
|
+
if (safeBuffer().byteLengthOf(pemOrDer) > C.BYTES.kib(64)) {
|
|
1903
1903
|
throw new TypeError(
|
|
1904
1904
|
"crypto.hashCertFingerprint: PEM input exceeds 64 KiB (" +
|
|
1905
1905
|
pemOrDer.length + " bytes); refuse oversized input to avoid " +
|
package/lib/gate-contract.js
CHANGED
|
@@ -2313,11 +2313,11 @@ function _resetForTest() {
|
|
|
2313
2313
|
* `makeProfileBuilder` and never call `buildProfile` directly.
|
|
2314
2314
|
*
|
|
2315
2315
|
* @opts
|
|
2316
|
-
* baseProfile: string,
|
|
2317
|
-
* extends: string[],
|
|
2318
|
-
* overrides: object,
|
|
2319
|
-
* removes: object,
|
|
2320
|
-
* resolveProfile: function,
|
|
2316
|
+
* baseProfile: string, // start from this profile name
|
|
2317
|
+
* extends: string|string[], // additional base(s) (later-wins)
|
|
2318
|
+
* overrides: object, // inline merge after extends
|
|
2319
|
+
* removes: object, // drop array entries or keys
|
|
2320
|
+
* resolveProfile: function, // (name) → profile|null (required)
|
|
2321
2321
|
*
|
|
2322
2322
|
* @example
|
|
2323
2323
|
* var PROFILES = {
|
|
@@ -2363,9 +2363,14 @@ function buildProfile(opts) {
|
|
|
2363
2363
|
return merged;
|
|
2364
2364
|
}
|
|
2365
2365
|
var base = opts.baseProfile ? _walk(opts.baseProfile) : {};
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2366
|
+
// @opts documents `extends: string|string[]`; normalize a bare string
|
|
2367
|
+
// to a one-element list so the single-base form composes instead of
|
|
2368
|
+
// being silently dropped (the ABI-template @example every guard shares
|
|
2369
|
+
// is `buildProfile({ extends: "strict" })`).
|
|
2370
|
+
var extendsList = typeof opts.extends === "string" ? [opts.extends] : opts.extends;
|
|
2371
|
+
if (Array.isArray(extendsList)) {
|
|
2372
|
+
for (var i = 0; i < extendsList.length; i++) {
|
|
2373
|
+
base = _mergeProfile(base, _walk(extendsList[i]));
|
|
2369
2374
|
}
|
|
2370
2375
|
}
|
|
2371
2376
|
if (opts.overrides) base = _mergeProfile(base, opts.overrides);
|
package/lib/guard-archive.js
CHANGED
|
@@ -781,10 +781,10 @@ function gate(opts) {
|
|
|
781
781
|
* `GuardArchiveError("archive.bad-profile")` on unknown name.
|
|
782
782
|
*
|
|
783
783
|
* @opts
|
|
784
|
-
*
|
|
784
|
+
* extends: "strict"|"balanced"|"permissive", // base profile name(s)
|
|
785
785
|
*
|
|
786
786
|
* @example
|
|
787
|
-
* var resolved = b.guardArchive.buildProfile({
|
|
787
|
+
* var resolved = b.guardArchive.buildProfile({ extends: "strict" });
|
|
788
788
|
* resolved.maxEntries; // → 100
|
|
789
789
|
* resolved.symlinkPolicy; // → "reject"
|
|
790
790
|
* resolved.maxCompressionRatio; // → 100
|
package/lib/guard-time.js
CHANGED
|
@@ -319,7 +319,7 @@ function _detectIssues(input, opts) {
|
|
|
319
319
|
*
|
|
320
320
|
* var bad = b.guardTime.validate("1969-12-31T23:59:59Z", { profile: "strict" });
|
|
321
321
|
* bad.ok; // → false
|
|
322
|
-
* bad.issues[0].ruleId; // → "time.year-
|
|
322
|
+
* bad.issues[0].ruleId; // → "time.year-window"
|
|
323
323
|
*/
|
|
324
324
|
// validate is assembled by gateContract.defineGuard from `detect`
|
|
325
325
|
// (_detectIssues) below — `validate(input, opts) = aggregateIssues(detect(
|
package/lib/guard-uuid.js
CHANGED
|
@@ -277,7 +277,7 @@ function _detectIssues(input, opts) {
|
|
|
277
277
|
* var bad = b.guardUuid.validate("00000000-0000-0000-0000-000000000000",
|
|
278
278
|
* { profile: "strict" });
|
|
279
279
|
* bad.ok; // → false
|
|
280
|
-
* bad.issues[0].ruleId; // → "uuid.nil"
|
|
280
|
+
* bad.issues[0].ruleId; // → "uuid.nil-uuid"
|
|
281
281
|
*/
|
|
282
282
|
// validate is assembled by gateContract.defineGuard from `detect`
|
|
283
283
|
// (_detectIssues) below — `validate(input, opts) = aggregateIssues(detect(
|
|
@@ -311,7 +311,7 @@ function _detectIssues(input, opts) {
|
|
|
311
311
|
* b.guardUuid.sanitize("ffffffff-ffff-ffff-ffff-ffffffffffff",
|
|
312
312
|
* { profile: "strict" });
|
|
313
313
|
* } catch (e) {
|
|
314
|
-
* e.code; // → "uuid.max"
|
|
314
|
+
* e.code; // → "uuid.max-uuid"
|
|
315
315
|
* }
|
|
316
316
|
*/
|
|
317
317
|
// _sanitizeTransform — the guard-specific normalize applied by defineGuard's
|
package/lib/html-balance.js
CHANGED
|
@@ -326,7 +326,7 @@ var markupTokenizer = require("./markup-tokenizer");
|
|
|
326
326
|
*
|
|
327
327
|
* @example
|
|
328
328
|
* var rv = b.htmlBalance.checkSafe("<div onclick=\"x()\">hi</div>", { profile: "strict" });
|
|
329
|
-
* // → { balanceIssue: null, guardIssues: [{ kind: "event-handler
|
|
329
|
+
* // → { balanceIssue: null, guardIssues: [{ kind: "event-handler", ... }], ok: false }
|
|
330
330
|
*
|
|
331
331
|
* b.htmlBalance.checkSafe("<p>hello</p>", { profile: "strict" });
|
|
332
332
|
* // → { balanceIssue: null, guardIssues: [], ok: true }
|
|
@@ -104,7 +104,7 @@ function _sfQuotedString(s) {
|
|
|
104
104
|
"httpSig: parameter string contains non-printable byte at offset " + i);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
return
|
|
107
|
+
return safeBuffer.quoteString(s);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// _serializeCovered — RFC 9421 §2.5 covered-components list.
|
package/lib/link-header.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* parameter never splits the list.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
var safeBuffer = require("./safe-buffer");
|
|
32
33
|
var structuredFields = require("./structured-fields");
|
|
33
34
|
var { defineClass } = require("./framework-error");
|
|
34
35
|
|
|
@@ -114,7 +115,7 @@ function parse(headerValue) {
|
|
|
114
115
|
// common convention (RFC 8288 examples, REST pagination) quotes too.
|
|
115
116
|
function _serParam(name, value) {
|
|
116
117
|
if (value === "" || value === true) return name; // valueless parameter
|
|
117
|
-
return name + "
|
|
118
|
+
return name + "=" + safeBuffer.quoteString(value);
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
/**
|
package/lib/log-stream.js
CHANGED
|
@@ -539,7 +539,9 @@ function bootFromEnv(opts) {
|
|
|
539
539
|
sink.logGroupName = env.BLAMEJS_LOG_STREAM_CLOUDWATCH_LOG_GROUP;
|
|
540
540
|
sink.logStreamName = env.BLAMEJS_LOG_STREAM_CLOUDWATCH_LOG_STREAM;
|
|
541
541
|
} else if (proto === "local") {
|
|
542
|
-
sink
|
|
542
|
+
// The local sink is directory-based (writes <dir>/<prefix>.log);
|
|
543
|
+
// BLAMEJS_LOG_STREAM_PATH names that directory and maps to `dir`.
|
|
544
|
+
sink.dir = env.BLAMEJS_LOG_STREAM_PATH;
|
|
543
545
|
} else {
|
|
544
546
|
throw _err("BAD_OPT",
|
|
545
547
|
"BLAMEJS_LOG_STREAM_PROTOCOL='" + proto + "' is not one of " +
|
package/lib/mail-auth.js
CHANGED
|
@@ -2158,7 +2158,7 @@ function authResultsEmit(opts) {
|
|
|
2158
2158
|
// (`\"`). Pre-v0.8.32 the framework collapsed `"` to `'` which
|
|
2159
2159
|
// is lossy. Use the spec-correct escape so the receiver can
|
|
2160
2160
|
// round-trip the original reason.
|
|
2161
|
-
clause +=
|
|
2161
|
+
clause += " reason=" + safeBuffer.quoteString(r.reason);
|
|
2162
2162
|
}
|
|
2163
2163
|
// Method-specific properties (ptype.property=value triples per
|
|
2164
2164
|
// RFC 8601 §2.3). Operators pass them as flat object keys.
|
package/lib/mail-server-imap.js
CHANGED
|
@@ -789,7 +789,7 @@ function create(opts) {
|
|
|
789
789
|
.then(function (rows) {
|
|
790
790
|
if (Array.isArray(rows) && rows.length > 0) {
|
|
791
791
|
var pairs = rows.map(function (r) {
|
|
792
|
-
var v = r.value === null || r.value === undefined ? "NIL" :
|
|
792
|
+
var v = r.value === null || r.value === undefined ? "NIL" : safeBuffer.quoteString(r.value);
|
|
793
793
|
return r.entry + " " + v;
|
|
794
794
|
}).join(" ");
|
|
795
795
|
_writeUntagged(socket, "METADATA " + (mailbox === "" ? '""' : mailbox) + " (" + pairs + ")");
|
|
@@ -1247,7 +1247,7 @@ function create(opts) {
|
|
|
1247
1247
|
for (var i = 0; i < folders.length; i += 1) {
|
|
1248
1248
|
var f = folders[i];
|
|
1249
1249
|
var attrs = (f.attributes || []).map(function (a) { return "\\" + a; }).join(" ");
|
|
1250
|
-
_writeUntagged(socket, "LIST (" + attrs + ") \"/\" " +
|
|
1250
|
+
_writeUntagged(socket, "LIST (" + attrs + ") \"/\" " + safeBuffer.quoteString(f.name));
|
|
1251
1251
|
}
|
|
1252
1252
|
_writeTagged(socket, tag, "OK LIST completed");
|
|
1253
1253
|
})
|
|
@@ -1282,7 +1282,7 @@ function create(opts) {
|
|
|
1282
1282
|
var key = items[k].toUpperCase();
|
|
1283
1283
|
if (info[key] !== undefined) parts.push(key + " " + info[key]);
|
|
1284
1284
|
}
|
|
1285
|
-
_writeUntagged(socket, "STATUS " +
|
|
1285
|
+
_writeUntagged(socket, "STATUS " + safeBuffer.quoteString(name) + " (" + parts.join(" ") + ")");
|
|
1286
1286
|
_writeTagged(socket, tag, "OK STATUS completed");
|
|
1287
1287
|
})
|
|
1288
1288
|
.catch(function (err) {
|
|
@@ -1780,7 +1780,6 @@ function create(opts) {
|
|
|
1780
1780
|
try { socket.destroy(); } catch (_e2) { /* idempotent */ }
|
|
1781
1781
|
connections.delete(socket);
|
|
1782
1782
|
}
|
|
1783
|
-
function _quote(s) { return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'; }
|
|
1784
1783
|
function _unquote(s) {
|
|
1785
1784
|
if (typeof s !== "string") return "";
|
|
1786
1785
|
if (s[0] === "\"" && s[s.length - 1] === "\"") return s.slice(1, -1);
|
|
@@ -730,7 +730,7 @@ function create(opts) {
|
|
|
730
730
|
var s = list[i];
|
|
731
731
|
var nm = String(s.name || "");
|
|
732
732
|
var active = s.active === true ? " ACTIVE" : "";
|
|
733
|
-
socket.write(
|
|
733
|
+
socket.write(safeBuffer.quoteString(nm) + active + "\r\n");
|
|
734
734
|
}
|
|
735
735
|
_emit("mail.server.managesieve.listscripts",
|
|
736
736
|
{ connectionId: state.id, count: list.length });
|
|
@@ -813,24 +813,20 @@ function create(opts) {
|
|
|
813
813
|
});
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
-
// RFC 5804 §1.2 quoted
|
|
817
|
-
//
|
|
818
|
-
function _quoteEscape(s) {
|
|
819
|
-
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); // allow:regex-no-length-cap — backslash + DQUOTE escape on bounded-input
|
|
820
|
-
}
|
|
821
|
-
|
|
816
|
+
// RFC 5804 §1.2 quoted strings — serialization routes through
|
|
817
|
+
// safeBuffer.quoteString (one quoted-string escaper framework-wide).
|
|
822
818
|
function _writeOk(socket, msg) {
|
|
823
|
-
try { socket.write(
|
|
819
|
+
try { socket.write("OK " + safeBuffer.quoteString(msg) + "\r\n"); } catch (_e) { /* socket down */ }
|
|
824
820
|
}
|
|
825
821
|
function _writeOkWithTag(socket, tag, msg) {
|
|
826
|
-
try { socket.write(
|
|
822
|
+
try { socket.write("OK (TAG " + safeBuffer.quoteString(tag) + ") " + safeBuffer.quoteString(msg) + "\r\n"); }
|
|
827
823
|
catch (_e) { /* socket down */ }
|
|
828
824
|
}
|
|
829
825
|
function _writeNo(socket, msg) {
|
|
830
|
-
try { socket.write(
|
|
826
|
+
try { socket.write("NO " + safeBuffer.quoteString(msg) + "\r\n"); } catch (_e) { /* socket down */ }
|
|
831
827
|
}
|
|
832
828
|
function _writeBye(socket, msg) {
|
|
833
|
-
try { socket.write(
|
|
829
|
+
try { socket.write("BYE " + safeBuffer.quoteString(msg) + "\r\n"); } catch (_e) { /* socket down */ }
|
|
834
830
|
}
|
|
835
831
|
function _close(socket) {
|
|
836
832
|
try { socket.end(); } catch (_e) { /* idempotent */ }
|
package/lib/middleware/index.js
CHANGED
package/lib/safe-buffer.js
CHANGED
|
@@ -872,6 +872,41 @@ function assertHeaderSafe(value, label, ErrorClass, code) {
|
|
|
872
872
|
return value;
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
+
/**
|
|
876
|
+
* @primitive b.safeBuffer.quoteString
|
|
877
|
+
* @signature b.safeBuffer.quoteString(s)
|
|
878
|
+
* @since 0.16.9
|
|
879
|
+
* @status stable
|
|
880
|
+
* @related b.safeBuffer.assertHeaderSafe, b.safeBuffer.foldHeaderText
|
|
881
|
+
*
|
|
882
|
+
* Serialize a value as an RFC quoted-string: coerce to string, escape
|
|
883
|
+
* every backslash and DQUOTE with a leading backslash, and wrap the
|
|
884
|
+
* result in DQUOTEs. One serializer for the quoted-string grammars the
|
|
885
|
+
* framework emits — RFC 8941 §3.3.3 Structured Fields sf-string
|
|
886
|
+
* (Cache-Status, Signature-Input, Server-Timing desc), RFC 8288 Link
|
|
887
|
+
* header parameters, RFC 8601 §2.2 Authentication-Results reason,
|
|
888
|
+
* RFC 3501 §4.3 IMAP quoted strings, and RFC 5804 §1.2 ManageSieve
|
|
889
|
+
* strings — so an unescaped quote can never terminate the string early
|
|
890
|
+
* and smuggle extra parameters into the protocol line.
|
|
891
|
+
*
|
|
892
|
+
* Escaping only — it does not validate a grammar's character range. A
|
|
893
|
+
* grammar that forbids bytes a quoted-string cannot carry (sf-string is
|
|
894
|
+
* printable-ASCII only; IMAP quoted strings cannot carry CR / LF)
|
|
895
|
+
* enforces its range check before calling this.
|
|
896
|
+
*
|
|
897
|
+
* @example
|
|
898
|
+
* var b = require("blamejs");
|
|
899
|
+
* b.safeBuffer.quoteString("cache miss");
|
|
900
|
+
* // → "\"cache miss\""
|
|
901
|
+
*
|
|
902
|
+
* // A quote or backslash in the value cannot break out of the string.
|
|
903
|
+
* b.safeBuffer.quoteString('say "hi"');
|
|
904
|
+
* // → "\"say \\\"hi\\\"\""
|
|
905
|
+
*/
|
|
906
|
+
function quoteString(s) {
|
|
907
|
+
return "\"" + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\""; // allow:regex-no-length-cap — fixed-char-set escape on caller-bounded input
|
|
908
|
+
}
|
|
909
|
+
|
|
875
910
|
module.exports = {
|
|
876
911
|
normalizeText: normalizeText,
|
|
877
912
|
toBuffer: toBuffer,
|
|
@@ -885,6 +920,7 @@ module.exports = {
|
|
|
885
920
|
stripCrlf: stripCrlf,
|
|
886
921
|
foldHeaderText: foldHeaderText,
|
|
887
922
|
assertHeaderSafe: assertHeaderSafe,
|
|
923
|
+
quoteString: quoteString,
|
|
888
924
|
stripTrailingHspace: stripTrailingHspace,
|
|
889
925
|
indexAfterOpenTag: indexAfterOpenTag,
|
|
890
926
|
HEX_RE: HEX_RE,
|
package/lib/server-timing.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* W3C Server-Timing response header builder — per-request timing-metric collector that surfaces server-side latency in the browser's Performance API.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
var safeBuffer = require("./safe-buffer");
|
|
40
41
|
var validateOpts = require("./validate-opts");
|
|
41
42
|
var { defineClass } = require("./framework-error");
|
|
42
43
|
|
|
@@ -46,9 +47,6 @@ var ServerTimingError = defineClass("ServerTimingError", { alwaysPermanent: true
|
|
|
46
47
|
// at 128 chars for sanity; operator-supplied desc is sf-string.
|
|
47
48
|
var METRIC_NAME_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,128}$/; // RFC 7230 token shape + length cap
|
|
48
49
|
|
|
49
|
-
function _quoteDesc(s) {
|
|
50
|
-
return "\"" + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
|
|
51
|
-
}
|
|
52
50
|
|
|
53
51
|
/**
|
|
54
52
|
* @primitive b.serverTiming.create
|
|
@@ -120,7 +118,7 @@ function create() {
|
|
|
120
118
|
return entries.map(function (e) {
|
|
121
119
|
var parts = [e.name];
|
|
122
120
|
if (e.dur !== null) parts.push("dur=" + _formatDur(e.dur));
|
|
123
|
-
if (e.desc !== null) parts.push("desc=" +
|
|
121
|
+
if (e.desc !== null) parts.push("desc=" + safeBuffer.quoteString(e.desc));
|
|
124
122
|
return parts.join("; ");
|
|
125
123
|
}).join(", ");
|
|
126
124
|
}
|
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:1c234c55-1297-48c3-bbd2-7b7b1f134fa5",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-11T03:08:14.929Z",
|
|
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.16.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.16.10",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.16.
|
|
25
|
+
"version": "0.16.10",
|
|
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.16.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.16.10",
|
|
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.16.
|
|
57
|
+
"ref": "@blamejs/core@0.16.10",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|