@blamejs/core 0.16.6 → 0.16.8
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/lib/a2a.js +4 -4
- package/lib/cloud-events.js +2 -2
- package/lib/guard-regex.js +2 -2
- package/lib/metrics.js +219 -60
- package/lib/middleware/asyncapi-serve.js +1 -1
- package/lib/middleware/index.js +5 -1
- package/lib/middleware/openapi-serve.js +1 -1
- package/lib/pqc-software.js +3 -3
- package/lib/retention.js +1 -1
- package/lib/safe-schema.js +4 -1
- package/lib/subject.js +6 -1
- package/lib/tls-exporter.js +3 -3
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +12 -6
- package/lib/vendor/public-suffix-list.data.js +1389 -1387
- 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.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
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- v0.16.6 (2026-07-05) — **Repair the ClusterFuzzLite / OSS-Fuzz build script so its fuzz targets actually install the jazzer.js runtime and pair with their seed corpora.** This patch fixes the OSS-Fuzz / ClusterFuzzLite build script (.clusterfuzzlite/build.sh), which was latently broken: it compiled every fuzz/<name>.fuzz.js harness without first installing @jazzer.js/core, so the generated targets referenced a runtime that was never present and could not start, and it named each seed-corpus archive after the .fuzz.js-stripped base (guard-csv_seed_corpus.zip) rather than the compiled target (guard-csv.fuzz), so the fuzzing engine never associated a corpus with its target and bootstrapped from nothing. Both failures were silent because the compile step still exits 0 and the in-repo CI fuzz workflows invoke jazzer.js directly rather than through this script — the script is the OSS-Fuzz-upstream integration spec. The build script now installs the jazzer.js runtime into the project-root node_modules before compiling and names each corpus after its target, and a codebase-patterns check locks both invariants in. The build image's inline documentation is corrected (jazzer.js is not present in the base image; the CI workflows do not consume this image) and now records why the upstream path is still latent (the base image ships Node 20 / GLIBC 2.31, below the framework's Node 22+ and jazzer.js's GLIBC 2.38 needs). No shipped framework code changes; fuzz and build-image assets are dev-only and are not part of the published package. **Fixed:** *OSS-Fuzz / ClusterFuzzLite build script installs the jazzer.js runtime before compiling* — compile_javascript_fuzzer emits a runnable that executes <project>/node_modules/@jazzer.js/core at fuzz time from a wholesale copy of the source tree, so the runtime must exist in the project-root node_modules at compile time. The build script never ran an install, so @jazzer.js/core (declared in fuzz/package.json) was absent and every compiled target referenced a runtime that wasn't there. The script now installs it before the compile loop. · *Fuzz seed corpora are paired with their compiled target* — compile_javascript_fuzzer names each target after basename -s .js (keeping the .fuzz stem, e.g. guard-csv.fuzz), but the script zipped each seed corpus under the .fuzz.js-stripped base (guard-csv_seed_corpus.zip). The fuzzing engine pairs <target>_seed_corpus.zip, so no corpus was ever associated and targets started from an empty corpus. Each corpus is now named after its target. · *Build-image documentation corrected* — The build image's comments claimed jazzer.js was pre-installed in the base image and that the CI fuzz workflows consume this image; neither is true. The comments now state that build.sh installs jazzer.js and that the CI workflows invoke jazzer.js directly, and record that the OSS-Fuzz-upstream path remains latent until the base image advances to Node 24 / a newer GLIBC. **Detectors:** *Fuzz-build invariants checked in codebase-patterns* — A cross-artifact check asserts that .clusterfuzzlite/build.sh installs a jazzer.js runtime before compile_javascript_fuzzer and names each seed-corpus archive after the compiled target, so neither gap can silently reappear.
|
|
12
16
|
|
|
13
17
|
- v0.16.5 (2026-07-03) — **Correctness fixes across the ReDoS guard, Base32 codec, HTTP multipart builder, SAML encrypted-assertion decryption, and the OAuth device grant — several restore advertised functionality that was silently non-working, plus a multipart header-injection fix.** This patch fixes a batch of correctness and security defects, several of which restore advertised behaviour that never actually worked. The ReDoS guard (b.guardRegex) no longer false-rejects LINEAR patterns that use a quantified non-capturing group (`(?:…)?`, `(?:…)*`) or an optional quantified group (`(X+)?`, `(?:X+)?`) — those repeat the group at most once and are not catastrophic; the detector now correctly requires the OUTER quantifier to be unbounded, so operator regex screening and b.selfUpdate asset patterns that use an optional SemVer suffix work again while genuine `(a+)+` shapes are still refused. The Base32 decoder now rejects NON-CANONICAL input (a final symbol whose unused low bits are non-zero — previously two distinct strings decoded to the same bytes, a malleability problem for the TOTP secrets and identifiers Base32 backs) and impossible symbol counts (1/3/6 mod 8 that silently produced a truncated buffer). The HTTP multipart/form-data builder now refuses CR/LF/NUL in a field name, filename, or content-type, closing a part-header-injection / form-part-forgery path. Two SAML EncryptedAssertion decryption paths that were completely dead — ML-KEM-1024 key transport and XChaCha20-Poly1305 content — are fixed (they referenced crypto entry points that were never exported, so the framework's advertised PQC-first / AEAD SAML encryption always failed and misreported the cause as a wrong-key or tag-mismatch error). The OAuth device grant now works against spec-compliant identity providers (its poll aborted on the first `authorization_pending`, which RFC 8628 delivers as an HTTP 400 the client was rejecting before reading), and a static (non-discovery) OAuth client can now use introspection, dynamic registration, and device authorization by configuring those endpoints. Smaller fixes round it out: the HTTP client's default error carries statusCode/permanent; the CLI dev server's repeatable flags accumulate; DANE and MTA-STS failures are scoped correctly per RFC; and typed errors replace raw TypeErrors on a couple of malformed-input paths. **Fixed:** *HTTP client default error carries statusCode and permanent* — request() fell back to the base FrameworkError when no explicit opts.errorClass was passed, so error.statusCode and error.permanent were undefined on the default path despite the documented default of HttpClientError. The default is now HttpClientError, so those fields are populated for every consumer. · *Static OAuth clients can configure introspection / registration / device-authorization endpoints* — create() never read opts.introspectionEndpoint / registrationEndpoint / deviceAuthorizationEndpoint into its static-endpoint set, so introspectToken / registerClient / deviceAuthorization resolved those endpoints only via OIDC discovery — a static (non-discovery) client could not use them, even though introspectToken's own error told operators to set the endpoint on create(). Those three endpoints are now honored from static config. · *CLI dev server repeatable flags accumulate* — `blamejs dev --arg / --watch / --ignore` are documented repeatable, but the argument parser overwrote each on repeat, so only the last occurrence survived — only the last --watch directory was monitored, only the last --ignore applied, and the child received only the last --arg. Repeated occurrences of these flags now accumulate as documented. · *DANE per-recipient failure isolation and MTA-STS testing-mode handling* — A DANE-enforce TLSA-lookup failure threw out of the entire deliver() batch instead of failing just the one recipient; it now fails that recipient and lets the rest proceed. And an MTA-STS policy published in `testing` mode was hard-bounced under the default enforce posture, violating RFC 8461 §5.2 (testing mode is report-only); a testing-mode policy no longer blocks delivery. · *external-db validates defaultBackend and enforces the pool min floor* — init() did not validate that opts.defaultBackend named a registered backend, so a typo surfaced as an opaque TypeError at the first query instead of a typed config-time error; it is now validated at init. The pool `min` (documented as a floor on idle clients) was never enforced; the reaper now respects it. · *mail-bounce returns a typed error for a null SES SNS message* — An SES SNS notification whose Message field is JSON literal `null` dereferenced null and threw a raw TypeError (also risking an internal-message leak); it now throws the typed MailBounceError like the other malformed-input paths. **Security:** *b.guardRegex no longer false-rejects linear quantified-group patterns (#432, #429)* — The nested-quantifier ReDoS detector treated the `?` in a `(?:` group prefix as an inner quantifier and treated a bounded outer `?` / `{0,1}` as a dangerous outer quantifier, so it wrongly refused LINEAR patterns like `^(?:/page/\d+)?$`, `^foo(?:bar)*$`, `^(a+)?$`, and `(?:[-+][0-9A-Za-z.-]+)?`. The catastrophic class requires the OUTER quantifier to be unbounded (`*`/`+`/`{n,}`). The detector now relies solely on the paren-aware structural scanner, which requires an unbounded outer quantifier and does not miscount a group prefix — so genuine `(a+)+` / `((a)+)+` shapes stay refused while these linear ones are accepted. Consumers screening operator-supplied regexes (route rules, and b.selfUpdate asset patterns using an optional SemVer prerelease/build group) are no longer forced to rewrite valid input. · *Base32 decoder rejects non-canonical encodings and impossible lengths* — b.base32.decode discarded the final symbol's unused low bits without checking they were zero (RFC 4648 §3.5), so two distinct strings — e.g. `MY======` and `MZ======` — decoded to the same bytes (decoder malleability), and it silently accepted impossible symbol counts (1/3/6 mod 8) that can't represent whole bytes, returning a truncated buffer. Both are now refused (`base32/non-canonical`, `base32/bad-length`), giving a one-to-one mapping between a byte sequence and its Base32 string — important where the string is a key / secret / dedup handle (TOTP, identifiers). Valid input, including unpadded and loose-mode input, is unaffected. · *HTTP multipart/form-data builder refuses CR/LF/NUL in part-header values* — The multipart body builder interpolated the field name, file field, filename, and content-type onto `Content-Disposition:` / `Content-Type:` part-header lines without a control-character check, so an attacker controlling one of those values could smuggle a CRLF and inject additional part headers or forge form parts. CR, LF, and NUL in any of those values are now refused, matching the header-safety the mail-header sweep already applies to RFC 822 lines. · *SAML EncryptedAssertion ML-KEM-1024 and XChaCha20-Poly1305 decryption now work* — verifyResponse's post-quantum key-transport branch and its XChaCha20-Poly1305 content branch called crypto entry points that the crypto module never exported, so every ML-KEM-1024-wrapped or XChaCha20-Poly1305-encrypted SAML assertion failed with an internal TypeError that was re-reported as a key-unwrap or tag-mismatch error — the advertised PQC-first / AEAD SAML encryption was dead. Both paths now route through the exported envelope-open and packed-AEAD primitives, verified with a full encrypt→decrypt round trip. Fails closed (no auth bypass) either way. · *OAuth device grant polls correctly against spec-compliant providers* — pollDeviceCode issued its token request in the default buffering mode, so the HTTP client rejected the HTTP 400 that RFC 8628 §3.5 / RFC 6749 §5.2 use to carry `authorization_pending` / `slow_down` before the poll loop could read the OAuth error body — the grant aborted on the first poll (which is almost always `authorization_pending`, since the user hasn't approved yet). The request now resolves 4xx OAuth error responses so the pending / slow-down / terminal handling runs and the device grant completes.
|
package/lib/a2a.js
CHANGED
|
@@ -117,7 +117,7 @@ function _validateCardShape(card, errorClass) {
|
|
|
117
117
|
* `verifyCard` use the same canonicalizer internally.
|
|
118
118
|
*
|
|
119
119
|
* @example
|
|
120
|
-
* var b = require("blamejs")
|
|
120
|
+
* var b = require("blamejs");
|
|
121
121
|
* var bytes = b.a2a.canonicalize({
|
|
122
122
|
* issuer: "agent.example.com",
|
|
123
123
|
* agentId: "ops-bot-1",
|
|
@@ -159,7 +159,7 @@ function canonicalize(card) {
|
|
|
159
159
|
* }
|
|
160
160
|
*
|
|
161
161
|
* @example
|
|
162
|
-
* var b = require("blamejs")
|
|
162
|
+
* var b = require("blamejs");
|
|
163
163
|
* var card = b.a2a.createCard({
|
|
164
164
|
* issuer: "agent.example.com",
|
|
165
165
|
* agentId: "ops-bot-1",
|
|
@@ -209,7 +209,7 @@ function createCard(opts) {
|
|
|
209
209
|
* }
|
|
210
210
|
*
|
|
211
211
|
* @example
|
|
212
|
-
* var b = require("blamejs")
|
|
212
|
+
* var b = require("blamejs");
|
|
213
213
|
* var card = b.a2a.createCard({
|
|
214
214
|
* issuer: "agent.example.com",
|
|
215
215
|
* agentId: "ops-bot-1",
|
|
@@ -292,7 +292,7 @@ function signCard(card, privateKeyPem, opts) {
|
|
|
292
292
|
* }
|
|
293
293
|
*
|
|
294
294
|
* @example
|
|
295
|
-
* var b = require("blamejs")
|
|
295
|
+
* var b = require("blamejs");
|
|
296
296
|
* var result = b.a2a.verifyCard(envelope, peerPublicKeyPem, {
|
|
297
297
|
* expectedIssuer: "agent.example.com"
|
|
298
298
|
* });
|
package/lib/cloud-events.js
CHANGED
|
@@ -110,7 +110,7 @@ function _genId() {
|
|
|
110
110
|
* }
|
|
111
111
|
*
|
|
112
112
|
* @example
|
|
113
|
-
* var b = require("blamejs")
|
|
113
|
+
* var b = require("blamejs");
|
|
114
114
|
* var ce = b.cloudEvents.wrap({
|
|
115
115
|
* source: "/services/orders",
|
|
116
116
|
* type: "com.example.order.created",
|
|
@@ -214,7 +214,7 @@ function wrap(opts) {
|
|
|
214
214
|
* envelope.
|
|
215
215
|
*
|
|
216
216
|
* @example
|
|
217
|
-
* var b = require("blamejs")
|
|
217
|
+
* var b = require("blamejs");
|
|
218
218
|
* var record = b.cloudEvents.parse({
|
|
219
219
|
* specversion: "1.0",
|
|
220
220
|
* id: "evt-1",
|
package/lib/guard-regex.js
CHANGED
|
@@ -484,12 +484,12 @@ function _sanitizeTransform(input) {
|
|
|
484
484
|
* @example
|
|
485
485
|
* var gate = b.guardRegex.gate({ profile: "strict" });
|
|
486
486
|
*
|
|
487
|
-
* gate({ identifier: "(a+)+b" }).then(function (rv) {
|
|
487
|
+
* gate.check({ identifier: "(a+)+b" }).then(function (rv) {
|
|
488
488
|
* rv.ok; // → false
|
|
489
489
|
* rv.action; // → "refuse"
|
|
490
490
|
* });
|
|
491
491
|
*
|
|
492
|
-
* gate({ identifier: "^[a-z]+$" }).then(function (rv) {
|
|
492
|
+
* gate.check({ identifier: "^[a-z]+$" }).then(function (rv) {
|
|
493
493
|
* rv.action; // → "serve"
|
|
494
494
|
* });
|
|
495
495
|
*/
|
package/lib/metrics.js
CHANGED
|
@@ -256,6 +256,84 @@ function _sortedLabelKeys(labelObj) {
|
|
|
256
256
|
return keys;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
// Shared metric-family encoder for the Prometheus / OpenMetrics text
|
|
260
|
+
// formats. The live registry's exposition(), the shadow registry's
|
|
261
|
+
// prometheus render, and snapshot.render's labeled-registry path all
|
|
262
|
+
// route through here, so there is exactly one encoder for labeled /
|
|
263
|
+
// bucketed sample lines.
|
|
264
|
+
//
|
|
265
|
+
// family: { name, type, help, unit, buckets, entries }
|
|
266
|
+
// counter / gauge entries: [{ labels, value }]
|
|
267
|
+
// histogram entries: [{ labels, counts, sum, count, exemplars }]
|
|
268
|
+
function _renderFamilyLines(family, openMetrics, lines) {
|
|
269
|
+
// OpenMetrics §5.1.2 — counter sample lines MUST suffix with
|
|
270
|
+
// `_total`. The metadata `# HELP / # TYPE / # UNIT` lines MUST
|
|
271
|
+
// name the SAME family identifier the samples use, otherwise
|
|
272
|
+
// strict OpenMetrics parsers reject the family. Derive the
|
|
273
|
+
// exposition name once so metadata and sample lines agree.
|
|
274
|
+
var exposedName = family.name;
|
|
275
|
+
if (openMetrics && family.type === "counter" && !/_total$/.test(family.name)) { // allow:regex-no-length-cap — name-suffix check
|
|
276
|
+
exposedName = family.name + "_total";
|
|
277
|
+
}
|
|
278
|
+
if (family.help) lines.push("# HELP " + exposedName + " " + family.help);
|
|
279
|
+
lines.push("# TYPE " + exposedName + " " + family.type);
|
|
280
|
+
if (openMetrics && family.unit) lines.push("# UNIT " + exposedName + " " + family.unit);
|
|
281
|
+
var entries = family.entries;
|
|
282
|
+
if (family.type === "histogram") {
|
|
283
|
+
for (var k = 0; k < entries.length; k++) {
|
|
284
|
+
var entry = entries[k];
|
|
285
|
+
for (var bi = 0; bi < family.buckets.length; bi++) {
|
|
286
|
+
var bLabels = Object.assign(Object.create(null), entry.labels, { le: String(family.buckets[bi]) });
|
|
287
|
+
var bucketLine = family.name + "_bucket" + _renderLabels(bLabels) + " " + entry.counts[bi];
|
|
288
|
+
// OpenMetrics 1.0 §6.2 — exemplar trace + span IDs appended
|
|
289
|
+
// as `# {trace_id="...",span_id="..."} <value> <timestamp>`.
|
|
290
|
+
if (openMetrics && entry.exemplars && entry.exemplars[bi]) {
|
|
291
|
+
var ex = entry.exemplars[bi];
|
|
292
|
+
bucketLine += " # " + _renderLabels(ex.labels || {}) + " " + ex.value;
|
|
293
|
+
if (ex.timestamp) bucketLine += " " + ex.timestamp;
|
|
294
|
+
}
|
|
295
|
+
lines.push(bucketLine);
|
|
296
|
+
}
|
|
297
|
+
var infLabels = Object.assign(Object.create(null), entry.labels, { le: "+Inf" });
|
|
298
|
+
lines.push(family.name + "_bucket" + _renderLabels(infLabels) + " " + entry.counts[family.buckets.length]);
|
|
299
|
+
lines.push(family.name + "_sum" + _renderLabels(entry.labels) + " " + entry.sum);
|
|
300
|
+
lines.push(family.name + "_count" + _renderLabels(entry.labels) + " " + entry.count);
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
// exposedName only diverges from family.name for OpenMetrics
|
|
304
|
+
// counters (the `_total` suffix rule above), so using it
|
|
305
|
+
// unconditionally keeps Prometheus output byte-identical.
|
|
306
|
+
for (var v = 0; v < entries.length; v++) {
|
|
307
|
+
lines.push(exposedName + _renderLabels(entries[v].labels) + " " + entries[v].value);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Flatten one family into `{ key → value }` sample pairs (key = sample
|
|
313
|
+
// name + rendered labels). The text-format snapshot renderer consumes
|
|
314
|
+
// these as synthetic field rows; _renderFamilyLines renders the same
|
|
315
|
+
// samples as wire-format lines (plus metadata + exemplars, which have
|
|
316
|
+
// no key/value representation).
|
|
317
|
+
function _familySamples(family) {
|
|
318
|
+
var out = Object.create(null);
|
|
319
|
+
for (var i = 0; i < family.entries.length; i++) {
|
|
320
|
+
var entry = family.entries[i];
|
|
321
|
+
if (family.type === "histogram") {
|
|
322
|
+
for (var bi = 0; bi < family.buckets.length; bi++) {
|
|
323
|
+
var bLabels = Object.assign(Object.create(null), entry.labels, { le: String(family.buckets[bi]) });
|
|
324
|
+
out[family.name + "_bucket" + _renderLabels(bLabels)] = entry.counts[bi];
|
|
325
|
+
}
|
|
326
|
+
var infLabels = Object.assign(Object.create(null), entry.labels, { le: "+Inf" });
|
|
327
|
+
out[family.name + "_bucket" + _renderLabels(infLabels)] = entry.counts[family.buckets.length];
|
|
328
|
+
out[family.name + "_sum" + _renderLabels(entry.labels)] = entry.sum;
|
|
329
|
+
out[family.name + "_count" + _renderLabels(entry.labels)] = entry.count;
|
|
330
|
+
} else {
|
|
331
|
+
out[family.name + _renderLabels(entry.labels)] = entry.value;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
|
|
259
337
|
// Combine default + per-call labels, validating against the metric's
|
|
260
338
|
// declared labelNames. Throws if a label name isn't declared.
|
|
261
339
|
function _resolveLabels(defaultLabels, declaredNames, callLabels) {
|
|
@@ -596,53 +674,17 @@ function create(opts) {
|
|
|
596
674
|
var sortedNames = Array.from(metrics.keys()).sort();
|
|
597
675
|
for (var i = 0; i < sortedNames.length; i++) {
|
|
598
676
|
var m = metrics.get(sortedNames[i]);
|
|
599
|
-
// OpenMetrics §5.1.2 — counter sample lines MUST suffix with
|
|
600
|
-
// `_total`. The metadata `# HELP / # TYPE / # UNIT` lines MUST
|
|
601
|
-
// name the SAME family identifier the samples use, otherwise
|
|
602
|
-
// strict OpenMetrics parsers reject the family. Derive the
|
|
603
|
-
// exposition name once at the top of the loop so both the
|
|
604
|
-
// metadata lines and the sample lines agree.
|
|
605
|
-
var exposedName = m.name;
|
|
606
|
-
if (openMetrics && m.type === "counter" && !/_total$/.test(m.name)) {
|
|
607
|
-
exposedName = m.name + "_total";
|
|
608
|
-
}
|
|
609
|
-
if (m.help) lines.push("# HELP " + exposedName + " " + m.help);
|
|
610
|
-
lines.push("# TYPE " + exposedName + " " + m.type);
|
|
611
|
-
if (openMetrics && m.unit) lines.push("# UNIT " + exposedName + " " + m.unit);
|
|
612
677
|
var keys = Array.from(m.values.keys()).sort();
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
bucketLine += " # " + _renderLabels(ex.labels || {}) + " " + ex.value;
|
|
624
|
-
if (ex.timestamp) bucketLine += " " + ex.timestamp;
|
|
625
|
-
}
|
|
626
|
-
lines.push(bucketLine);
|
|
627
|
-
}
|
|
628
|
-
var infLabels = Object.assign({}, entry.labels, { le: "+Inf" });
|
|
629
|
-
lines.push(m.name + "_bucket" + _renderLabels(infLabels) + " " + entry.counts[m.buckets.length]);
|
|
630
|
-
lines.push(m.name + "_sum" + _renderLabels(entry.labels) + " " + entry.sum);
|
|
631
|
-
lines.push(m.name + "_count" + _renderLabels(entry.labels) + " " + entry.count);
|
|
632
|
-
}
|
|
633
|
-
} else if (m.type === "counter" && openMetrics) {
|
|
634
|
-
// exposedName already carries the `_total` suffix when needed
|
|
635
|
-
// (derived at the top of the loop so metadata + samples agree).
|
|
636
|
-
for (var v = 0; v < keys.length; v++) {
|
|
637
|
-
var ent = m.values.get(keys[v]);
|
|
638
|
-
lines.push(exposedName + _renderLabels(ent.labels) + " " + ent.value);
|
|
639
|
-
}
|
|
640
|
-
} else {
|
|
641
|
-
for (var v2 = 0; v2 < keys.length; v2++) {
|
|
642
|
-
var ent2 = m.values.get(keys[v2]);
|
|
643
|
-
lines.push(m.name + _renderLabels(ent2.labels) + " " + ent2.value);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
678
|
+
var entries = [];
|
|
679
|
+
for (var k = 0; k < keys.length; k++) entries.push(m.values.get(keys[k]));
|
|
680
|
+
_renderFamilyLines({
|
|
681
|
+
name: m.name,
|
|
682
|
+
type: m.type,
|
|
683
|
+
help: m.help,
|
|
684
|
+
unit: m.unit,
|
|
685
|
+
buckets: m.buckets,
|
|
686
|
+
entries: entries,
|
|
687
|
+
}, openMetrics, lines);
|
|
646
688
|
lines.push("");
|
|
647
689
|
}
|
|
648
690
|
if (openMetrics) lines.push("# EOF");
|
|
@@ -1157,6 +1199,19 @@ function snapshotRead(p) {
|
|
|
1157
1199
|
* queries start returning the right answer once the new types reach
|
|
1158
1200
|
* the scrape target.
|
|
1159
1201
|
*
|
|
1202
|
+
* ## Labeled registry series
|
|
1203
|
+
*
|
|
1204
|
+
* A snapshot written with `startWriter`'s `registry` option carries the
|
|
1205
|
+
* registry's counters / gauges / histograms — label sets and histogram
|
|
1206
|
+
* bucket counts — in a structured `metrics` field. Both formats render
|
|
1207
|
+
* them: `prometheus` emits the same labeled / bucketed sample lines the
|
|
1208
|
+
* live `exposition()` endpoint serves, family names verbatim (NOT
|
|
1209
|
+
* `prefix`-qualified) so dashboards see one series name regardless of
|
|
1210
|
+
* scrape source; `text` lists each labeled sample as a
|
|
1211
|
+
* `name{label="value"}` row. A malformed family, metric / label name,
|
|
1212
|
+
* or non-numeric sample in a hand-edited snapshot file is dropped,
|
|
1213
|
+
* never rendered.
|
|
1214
|
+
*
|
|
1160
1215
|
* @opts
|
|
1161
1216
|
* format: "text" | "prometheus", // default: "text"
|
|
1162
1217
|
* prefix: string, // prometheus-only; default: "blamejs"
|
|
@@ -1233,6 +1288,84 @@ function _renderText(fields, snap, opts) {
|
|
|
1233
1288
|
return lines.join("\n") + "\n";
|
|
1234
1289
|
}
|
|
1235
1290
|
|
|
1291
|
+
// Validate + normalize one serialized registry family from a snapshot's
|
|
1292
|
+
// `metrics` field (written by startWriter's `registry` option) into the
|
|
1293
|
+
// _renderFamilyLines shape. Snapshot files are read back from disk, so
|
|
1294
|
+
// this is a defensive reader: a malformed family, a non-Prometheus
|
|
1295
|
+
// metric / label name, or a non-numeric sample is dropped rather than
|
|
1296
|
+
// rendered — a hand-edited snapshot file must not be able to forge
|
|
1297
|
+
// exposition lines. Returns null when the whole family is unusable.
|
|
1298
|
+
function _normalizeSnapshotFamily(name, fam) {
|
|
1299
|
+
if (!fam || typeof fam !== "object" || Array.isArray(fam)) return null;
|
|
1300
|
+
// Same name contracts as the live registry (METRIC_NAME_RE allows the
|
|
1301
|
+
// colon forms _validateMetricName accepts, LABEL_NAME_RE does not) —
|
|
1302
|
+
// a family the live exposition emits must never be dropped here. The
|
|
1303
|
+
// length cap bounds the anchored test on untrusted snapshot-file bytes.
|
|
1304
|
+
if (name.length > 1024 || !METRIC_NAME_RE.test(name)) return null;
|
|
1305
|
+
var type = fam.type;
|
|
1306
|
+
if (type !== "counter" && type !== "gauge" && type !== "histogram") return null;
|
|
1307
|
+
var buckets = null;
|
|
1308
|
+
if (type === "histogram") {
|
|
1309
|
+
if (!Array.isArray(fam.buckets)) return null;
|
|
1310
|
+
buckets = [];
|
|
1311
|
+
for (var bi = 0; bi < fam.buckets.length; bi += 1) {
|
|
1312
|
+
if (typeof fam.buckets[bi] !== "number" || !isFinite(fam.buckets[bi])) return null;
|
|
1313
|
+
buckets.push(fam.buckets[bi]);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
var observations = Array.isArray(fam.observations) ? fam.observations : [];
|
|
1317
|
+
var entries = [];
|
|
1318
|
+
for (var i = 0; i < observations.length; i += 1) {
|
|
1319
|
+
var obs = observations[i];
|
|
1320
|
+
if (!obs || typeof obs !== "object") continue;
|
|
1321
|
+
// null-proto so a label literally named `__proto__` / `constructor`
|
|
1322
|
+
// lands as an own property instead of being swallowed by the
|
|
1323
|
+
// plain-object prototype setter.
|
|
1324
|
+
var labels = Object.create(null);
|
|
1325
|
+
var rawLabels = obs.labels && typeof obs.labels === "object" ? obs.labels : {};
|
|
1326
|
+
var lnames = Object.keys(rawLabels);
|
|
1327
|
+
for (var li = 0; li < lnames.length; li += 1) {
|
|
1328
|
+
if (lnames[li].length > 1024 || !LABEL_NAME_RE.test(lnames[li])) continue; // drop forged / oversized label names
|
|
1329
|
+
labels[lnames[li]] = String(rawLabels[lnames[li]]);
|
|
1330
|
+
}
|
|
1331
|
+
if (type === "histogram") {
|
|
1332
|
+
if (!Array.isArray(obs.counts) || obs.counts.length !== buckets.length + 1) continue;
|
|
1333
|
+
var countsOk = true;
|
|
1334
|
+
for (var ci = 0; ci < obs.counts.length; ci += 1) {
|
|
1335
|
+
if (typeof obs.counts[ci] !== "number" || !isFinite(obs.counts[ci])) { countsOk = false; break; }
|
|
1336
|
+
}
|
|
1337
|
+
if (!countsOk) continue;
|
|
1338
|
+
if (typeof obs.sum !== "number" || !isFinite(obs.sum)) continue;
|
|
1339
|
+
if (typeof obs.count !== "number" || !isFinite(obs.count)) continue;
|
|
1340
|
+
entries.push({ labels: labels, counts: obs.counts, sum: obs.sum, count: obs.count });
|
|
1341
|
+
} else {
|
|
1342
|
+
if (typeof obs.value !== "number" || !isFinite(obs.value)) continue; // numeric samples only
|
|
1343
|
+
entries.push({ labels: labels, value: obs.value });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
// Escape HELP text per the exposition format so a forged help string
|
|
1347
|
+
// can't inject metric lines (live registries carry operator-authored
|
|
1348
|
+
// help; snapshot files are untrusted bytes).
|
|
1349
|
+
var help = typeof fam.help === "string"
|
|
1350
|
+
? fam.help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "") // allow:regex-no-length-cap — fixed-char-set escape // allow:duplicate-regex — Prometheus help-text escape shape
|
|
1351
|
+
: "";
|
|
1352
|
+
return { name: name, type: type, help: help, buckets: buckets, entries: entries };
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Walk a snapshot's serialized `metrics` object into validated families,
|
|
1356
|
+
// sorted by name for stable exposition output.
|
|
1357
|
+
function _normalizeSnapshotFamilies(metricsObj) {
|
|
1358
|
+
if (!metricsObj || typeof metricsObj !== "object" || Array.isArray(metricsObj)) return [];
|
|
1359
|
+
var families = [];
|
|
1360
|
+
// allow:bare-canonicalize-walk — stable exposition output ordering
|
|
1361
|
+
var names = Object.keys(metricsObj).sort();
|
|
1362
|
+
for (var i = 0; i < names.length; i += 1) {
|
|
1363
|
+
var family = _normalizeSnapshotFamily(names[i], metricsObj[names[i]]);
|
|
1364
|
+
if (family) families.push(family);
|
|
1365
|
+
}
|
|
1366
|
+
return families;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1236
1369
|
function snapshotRender(snap, opts) {
|
|
1237
1370
|
opts = opts || {};
|
|
1238
1371
|
var format = opts.format || "text";
|
|
@@ -1241,8 +1374,25 @@ function snapshotRender(snap, opts) {
|
|
|
1241
1374
|
"metrics.snapshot.render: snap must be a startWriter-produced object (got " + typeof snap + ")");
|
|
1242
1375
|
}
|
|
1243
1376
|
var fields = snap.fields;
|
|
1377
|
+
// Labeled registry families (issue #430) — a snapshot written with
|
|
1378
|
+
// startWriter's `registry` option carries every registered counter /
|
|
1379
|
+
// gauge / histogram under `metrics`. Both formats render them so a
|
|
1380
|
+
// sidecar consuming a snapshot written by another process gets the
|
|
1381
|
+
// full labeled series, not just the flat numeric fields.
|
|
1382
|
+
var families = _normalizeSnapshotFamilies(snap.metrics);
|
|
1244
1383
|
if (format === "text") {
|
|
1245
|
-
|
|
1384
|
+
var textFields = fields;
|
|
1385
|
+
if (families.length > 0) {
|
|
1386
|
+
// Labeled series become synthetic `name{label="value"}` rows; an
|
|
1387
|
+
// operator-supplied flat field wins on a (pathological) name
|
|
1388
|
+
// collision.
|
|
1389
|
+
var synth = Object.create(null);
|
|
1390
|
+
for (var tf = 0; tf < families.length; tf += 1) {
|
|
1391
|
+
Object.assign(synth, _familySamples(families[tf]));
|
|
1392
|
+
}
|
|
1393
|
+
textFields = Object.assign(synth, fields);
|
|
1394
|
+
}
|
|
1395
|
+
return _renderText(textFields, snap, opts);
|
|
1246
1396
|
}
|
|
1247
1397
|
if (format === "prometheus") {
|
|
1248
1398
|
var prefix = opts.prefix || "blamejs";
|
|
@@ -1299,6 +1449,14 @@ function snapshotRender(snap, opts) {
|
|
|
1299
1449
|
out.push("# TYPE " + emName + " gauge");
|
|
1300
1450
|
out.push(emName + " " + ms);
|
|
1301
1451
|
}
|
|
1452
|
+
// Registry families render through the same encoder the live
|
|
1453
|
+
// exposition() endpoint uses, names verbatim (NOT prefix-qualified),
|
|
1454
|
+
// so a scraper switching between the live /metrics route and the
|
|
1455
|
+
// snapshot sidecar sees identical series names.
|
|
1456
|
+
for (var fj = 0; fj < families.length; fj += 1) {
|
|
1457
|
+
if (out.length > 0) out.push(""); // blank-line family separator, mirrors exposition()
|
|
1458
|
+
_renderFamilyLines(families[fj], false, out);
|
|
1459
|
+
}
|
|
1302
1460
|
return out.join("\n") + "\n";
|
|
1303
1461
|
}
|
|
1304
1462
|
throw new MetricsError("metrics-snapshot/bad-format",
|
|
@@ -1477,30 +1635,31 @@ function shadowRegistry(opts) {
|
|
|
1477
1635
|
function _emitLabeled(name, labelMap, kind) {
|
|
1478
1636
|
var metric = prefix + "_" + name;
|
|
1479
1637
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(metric)) return; // allow:regex-no-length-cap — Prometheus name-shape; metric length bounded by namespace + name caps
|
|
1480
|
-
|
|
1638
|
+
var entries = [];
|
|
1481
1639
|
var lks = Object.keys(labelMap);
|
|
1482
1640
|
for (var li = 0; li < lks.length; li += 1) {
|
|
1483
1641
|
var lk = lks[li];
|
|
1484
|
-
if (lk === "") { out.push(metric + " " + labelMap[lk]); continue; }
|
|
1485
1642
|
// Read the structured label set kept alongside the canonical key (see
|
|
1486
1643
|
// _coerceLabels) rather than re-parsing the key. No serialize-then-
|
|
1487
1644
|
// split round-trip, so a `,` or `=` in a label value stays inside the
|
|
1488
1645
|
// value and can never forge extra label pairs; and a label NAMED
|
|
1489
1646
|
// `constructor` / `prototype` / `__proto__` survives instead of being
|
|
1490
1647
|
// stripped by a prototype-pollution-hardened parse.
|
|
1491
|
-
var labelObj = labelSets[lk];
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1648
|
+
var labelObj = lk === "" ? null : labelSets[lk];
|
|
1649
|
+
// null-proto so a label literally named `__proto__` /
|
|
1650
|
+
// `constructor` lands as an own property instead of being
|
|
1651
|
+
// swallowed by the plain-object prototype setter.
|
|
1652
|
+
var labels = Object.create(null);
|
|
1653
|
+
if (labelObj && typeof labelObj === "object") {
|
|
1654
|
+
var lnames = Object.keys(labelObj);
|
|
1655
|
+
for (var pi = 0; pi < lnames.length; pi += 1) {
|
|
1656
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(lnames[pi])) continue; // allow:regex-no-length-cap — Prometheus label-name shape
|
|
1657
|
+
labels[lnames[pi]] = labelObj[lnames[pi]];
|
|
1658
|
+
}
|
|
1501
1659
|
}
|
|
1502
|
-
|
|
1660
|
+
entries.push({ labels: labels, value: labelMap[lk] });
|
|
1503
1661
|
}
|
|
1662
|
+
_renderFamilyLines({ name: metric, type: kind, help: "", entries: entries }, false, out);
|
|
1504
1663
|
}
|
|
1505
1664
|
var cn2 = Object.keys(counters);
|
|
1506
1665
|
for (var ci = 0; ci < cn2.length; ci += 1) {
|
|
@@ -94,7 +94,7 @@ var audit = lazyRequire(function () { return require("../audit"); });
|
|
|
94
94
|
* @example
|
|
95
95
|
* var b = require("@blamejs/core");
|
|
96
96
|
* var app = b.router.create();
|
|
97
|
-
* var aapi = b.asyncapi.create({ title: "events", version: "1.0.0" });
|
|
97
|
+
* var aapi = b.asyncapi.create({ info: { title: "events", version: "1.0.0" } });
|
|
98
98
|
* app.use(b.middleware.asyncapiServe({
|
|
99
99
|
* document: aapi,
|
|
100
100
|
* pathJson: "/asyncapi.json",
|
package/lib/middleware/index.js
CHANGED
|
@@ -120,7 +120,11 @@ module.exports = {
|
|
|
120
120
|
tracePropagate: tracePropagate.create,
|
|
121
121
|
tusUpload: tusUpload.create,
|
|
122
122
|
webAppManifest: webAppManifest.create,
|
|
123
|
-
clearSiteData: clearSiteData.create,
|
|
123
|
+
clearSiteData: Object.assign(clearSiteData.create, {
|
|
124
|
+
headerValue: clearSiteData.headerValue,
|
|
125
|
+
KNOWN_TYPES: clearSiteData.KNOWN_TYPES,
|
|
126
|
+
DEFAULT_TYPES: clearSiteData.DEFAULT_TYPES,
|
|
127
|
+
}),
|
|
124
128
|
nel: nel.create,
|
|
125
129
|
speculationRules: speculationRules.create,
|
|
126
130
|
protectedResourceMetadata: protectedResourceMetadata.create,
|
|
@@ -100,7 +100,7 @@ var audit = lazyRequire(function () { return require("../audit"); });
|
|
|
100
100
|
* @example
|
|
101
101
|
* var b = require("@blamejs/core");
|
|
102
102
|
* var app = b.router.create();
|
|
103
|
-
* var doc = b.openapi.create({ title: "api", version: "1.0.0" });
|
|
103
|
+
* var doc = b.openapi.create({ info: { title: "api", version: "1.0.0" } });
|
|
104
104
|
* app.use(b.middleware.openapiServe({
|
|
105
105
|
* document: doc,
|
|
106
106
|
* pretty: true,
|
package/lib/pqc-software.js
CHANGED
|
@@ -103,7 +103,7 @@ function _accessor(name) {
|
|
|
103
103
|
* posture returns a stub whose primitive calls throw `PqcError`.
|
|
104
104
|
*
|
|
105
105
|
* @example
|
|
106
|
-
* var b = require("blamejs")
|
|
106
|
+
* var b = require("blamejs");
|
|
107
107
|
* if (b.pqcSoftware.isAvailable()) {
|
|
108
108
|
* var ss = b.pqcSoftware.DEFAULT_KEM.keygen();
|
|
109
109
|
* ss.publicKey.length;
|
|
@@ -128,7 +128,7 @@ function isAvailable() {
|
|
|
128
128
|
* bundle is unavailable.
|
|
129
129
|
*
|
|
130
130
|
* @example
|
|
131
|
-
* var b = require("blamejs")
|
|
131
|
+
* var b = require("blamejs");
|
|
132
132
|
* var names = b.pqcSoftware.listAlgorithms();
|
|
133
133
|
* names.indexOf("ml_kem_1024") >= 0;
|
|
134
134
|
* // → true (when the vendored bundle is present)
|
|
@@ -235,7 +235,7 @@ Object.defineProperty(pqc, "DEFAULT_HASH_SIG", {
|
|
|
235
235
|
* shared secrets are byte-identical (32 bytes per FIPS 203 §1).
|
|
236
236
|
*
|
|
237
237
|
* @example
|
|
238
|
-
* var b = require("blamejs")
|
|
238
|
+
* var b = require("blamejs");
|
|
239
239
|
* var result = b.pqcSoftware.runKnownAnswerTest();
|
|
240
240
|
* result.ok;
|
|
241
241
|
* // → true (or { ok: false, reason: "<diagnostic>" } when broken)
|
package/lib/retention.js
CHANGED
|
@@ -623,7 +623,7 @@ var COMPLIANCE_RETENTION_FLOOR_MS = Object.freeze({
|
|
|
623
623
|
* matched). Throws on an unknown posture so config-time typos surface.
|
|
624
624
|
*
|
|
625
625
|
* @example
|
|
626
|
-
* var ttl = b.retention.complianceFloor("hipaa", b.
|
|
626
|
+
* var ttl = b.retention.complianceFloor("hipaa", b.constants.TIME.days(180));
|
|
627
627
|
* // → 189216000000 (HIPAA's 6-year floor wins over the 180-day candidate)
|
|
628
628
|
*
|
|
629
629
|
* var sox = b.retention.complianceFloor("sox", 0);
|
package/lib/safe-schema.js
CHANGED
|
@@ -996,8 +996,11 @@ function unknown() { return any(); }
|
|
|
996
996
|
* // → "object/unknown-key"
|
|
997
997
|
*
|
|
998
998
|
* // Prototype-pollution attempt rejected even with passthrough.
|
|
999
|
+
* // A hostile __proto__ only becomes an own key through JSON input;
|
|
1000
|
+
* // object-literal `__proto__:` sets the prototype instead.
|
|
999
1001
|
* var loose = user.passthrough();
|
|
1000
|
-
* var
|
|
1002
|
+
* var hostile = JSON.parse('{ "email": "a@b.com", "age": 30, "__proto__": { "admin": true } }');
|
|
1003
|
+
* var report = loose.safeParse(hostile);
|
|
1001
1004
|
* report.ok;
|
|
1002
1005
|
* // → false
|
|
1003
1006
|
* report.errors[0].code;
|
package/lib/subject.js
CHANGED
|
@@ -133,7 +133,12 @@ function exportData(subjectId, opts) {
|
|
|
133
133
|
|
|
134
134
|
var tables = db()._getSubjectTables();
|
|
135
135
|
if (tables.length === 0) {
|
|
136
|
-
|
|
136
|
+
// No subjectField-tagged tables: still audit, but return the documented
|
|
137
|
+
// empty dump ({}) — _writeAudit returns undefined, so returning its value
|
|
138
|
+
// (with a vestigial 5th arg) silently handed callers `undefined`, crashing
|
|
139
|
+
// `Object.keys(dump)` / `dump.<table>` before any table is even declared.
|
|
140
|
+
_writeAudit("subject.export", subjectId, "success", { reason: opts.reason || null });
|
|
141
|
+
return {};
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
var dump = {};
|
package/lib/tls-exporter.js
CHANGED
|
@@ -106,7 +106,7 @@ function _resolveTlsSocket(socketOrReq) {
|
|
|
106
106
|
* }
|
|
107
107
|
*
|
|
108
108
|
* @example
|
|
109
|
-
* var b = require("blamejs")
|
|
109
|
+
* var b = require("blamejs");
|
|
110
110
|
* var server = b.https.createServer({ key: KEY, cert: CERT }, function (req, res) {
|
|
111
111
|
* var exporter = b.tlsExporter.fromSocket(req, { length: 32 });
|
|
112
112
|
* res.end("exporter bytes: " + exporter.length);
|
|
@@ -175,7 +175,7 @@ function fromSocket(socketOrReq, opts) {
|
|
|
175
175
|
* compare via `verifyTokenBinding` on the next request.
|
|
176
176
|
*
|
|
177
177
|
* @example
|
|
178
|
-
* var b = require("blamejs")
|
|
178
|
+
* var b = require("blamejs");
|
|
179
179
|
* b.https.createServer({ key: KEY, cert: CERT }, function (req, res) {
|
|
180
180
|
* var binding = b.tlsExporter.bindToken(req, "session-token-abc123");
|
|
181
181
|
* binding.length;
|
|
@@ -214,7 +214,7 @@ function bindToken(socketOrReq, token) {
|
|
|
214
214
|
* the input shape is wrong.
|
|
215
215
|
*
|
|
216
216
|
* @example
|
|
217
|
-
* var b = require("blamejs")
|
|
217
|
+
* var b = require("blamejs");
|
|
218
218
|
* b.https.createServer({ key: KEY, cert: CERT }, function (req, res) {
|
|
219
219
|
* var stored = b.tlsExporter.bindToken(req, "session-token-abc123");
|
|
220
220
|
* var ok = b.tlsExporter.verifyTokenBinding(req, "session-token-abc123", stored);
|