@blamejs/core 0.13.30 → 0.13.32

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.13.x
10
10
 
11
+ - v0.13.32 (2026-05-28) — **`b.auditDailyReview` enforces notify under the sox-404 posture; compliance doc corrections.** b.auditDailyReview documented `sox-404` (SOX §404 ICFR — the internal-controls regime this primitive serves) as one of the postures that make a `notify` callback mandatory at construction, but the enforcement set used only `sox`, so pinning `posture: "sox-404"` without a notify channel was silently accepted. `sox-404` is now in the mandatory-notify set, so the advertised guarantee holds (a regression test pins it). The rest are documentation corrections with no behavior change: b.compliance.posturesByDomain / posturesByJurisdiction examples showed small fixed arrays where the functions return every matching posture (the catalog has grown); b.dataAct's surface list named two methods that do not exist (the real surface is declareProduct / recordUserAccess / shareWithThirdParty / recordSwitchRequest, with gatekeeper refusal folded into shareWithThirdParty); b.secCyber.eightKArtifact's documented return key `audit` is actually `deadlineBusinessDays`; and b.compliance.aiAct.transparency's helper summary named `cspMetaTag` / a `watermark({ kind })` argument that are really `metaTags` / `watermark({ mediaKind })`. **Fixed:** *`b.auditDailyReview` requires a notify channel under the `sox-404` posture* — The docs listed `sox-404` among the postures that make a `notify` callback mandatory at create-time, but the enforcement set contained only `sox` — so `posture: "sox-404"` without `notify` was accepted instead of refused. `sox-404` (SOX §404 ICFR) is now in the mandatory-notify set, matching the documented guarantee; constructing without a notify channel under it throws `auditDailyReview/notify-required-under-posture`. · *`b.compliance` jurisdiction/domain lister examples no longer enumerate a stale fixed set* — `posturesByDomain` and `posturesByJurisdiction` return every posture matching the domain/jurisdiction, but their `@example`s showed small fixed arrays from before the posture catalog grew. The examples now show a representative prefix with `...` and note they return the full matching set. · *`b.dataAct` surface list matches the real methods* — The module surface listed `userAccessible(...)` and `refuseGatekeeper(...)`, neither of which exists. The real surface is `declareProduct` / `recordUserAccess` / `shareWithThirdParty` / `recordSwitchRequest`, and DMA-gatekeeper refusal (Art 32 §1) is enforced inside `shareWithThirdParty`. The doc now reflects that. · *`b.secCyber.eightKArtifact` documented return shape corrected* — The signature line showed `{ artifact, deadline, audit }`; the function returns `{ artifact, deadline, deadlineBusinessDays }` (there is no `audit` key). The doc now matches. · *`b.compliance.aiAct.transparency` helper names corrected* — The helper summary named a `cspMetaTag(...)` function and a `watermark({ kind })` argument; the real names are `metaTags(...)` and `watermark({ mediaKind })`. Calling the documented names threw. Also corrected: a `b.aiAdverseDecision` illustration showed an ECOA `statutoryDeadlines` shape that didn't match the regime's actual deadlines.
12
+
13
+ - v0.13.31 (2026-05-28) — **Circuit-breaker onStateChange callback now fires; mcp / vault-aad doc corrections.** b.circuitBreaker documented an `onStateChange` callback (both an option and an `onStateChange(handler)` registration method) plus a state-change payload, but the callback was never invoked — only an observability event fired. The callback is now implemented: it fires on every transition with `{ name, from, to, at }`, the registration method works, and a non-function handler is rejected at construction. The same primitive's docs are corrected to name the real accessor (`getState()`, not `state()`) and drop a never-read `audit` option. Plus two doc-only corrections: b.mcp.toolResult.sanitize described composing b.guardHtml / b.ai.input.classify (it uses built-in detection) and documented a `classifyInput` option it never read; and b.vault.aad's prose said HKDF-SHAKE256 where the derivation is SHAKE256 (the AEAD AAD-binding itself is unchanged and sound). **Fixed:** *`b.circuitBreaker` onStateChange callback is invoked on every transition* — The `onStateChange` option and the `onStateChange(handler)` registration method are now wired: each registered handler is called with `{ name, from, to, at }` on every state transition (closed→open, open→half, half→closed/open), alongside the existing `breaker.state.change` observability event. A non-function `onStateChange` is rejected at construction. Previously the documented callback never fired. The docs are also corrected to name the real state accessor `getState()` (there is a `state` property, so `state()` was never a method) and to drop a never-read `audit` option. · *`b.mcp.toolResult.sanitize` documents its actual detection and options* — The prose said the sanitizer composes `b.guardHtml`'s strict profile and `b.ai.input.classify`; it uses built-in dangerous-HTML and prompt-injection-marker detection. The `@opts` also listed a `classifyInput` override the function never read. The prose now describes the built-in detection and the unwired `classifyInput` option is removed. The fail-closed refusal behavior (default `posture: "refuse"`) is unchanged. · *`b.vault.aad` derivation named correctly (SHAKE256)* — The module prose described the per-binding key derivation as HKDF-SHAKE256; it is SHAKE256 over the vault root concatenated with the binding inputs (no HKDF extract/expand). The AEAD AAD-binding to (table, row, column, schema version) — the file's actual security guarantee — is unchanged and sound; only the KDF name in the doc was wrong.
14
+
11
15
  - v0.13.30 (2026-05-28) — **Doc corrections in the safe-* parsers (defaults, an error code, an example, a status list).** Four documentation corrections in the safe-* input parsers; no code behavior changed. The parsers' enforced limits and controls are unchanged — these align the docs with what the code already does. b.safeMime.parse's documented default transfer-encoding allowlist listed `binary`, which is excluded by default (opt-in per RFC 3030 BINARYMIME). b.safeDecompress documented a refusal code (`output-too-large`) it never emits — an absolute-size bomb surfaces under `decompress-failed`. b.safeSmtp.findDotTerminator's example output was off by one. b.safeIcap's intro status-code summary omitted 404 / 405 / 408 (the detailed block already listed them). **Fixed:** *`b.safeMime.parse` documents the actual default transfer-encoding allowlist* — The `@opts` default listed `7bit/8bit/binary/qp/base64`, but `binary` is deliberately excluded by default (RFC 3030 BINARYMIME is opt-in); the default is `7bit/8bit/quoted-printable/base64`. The doc now matches, so operators don't expect inbound `Content-Transfer-Encoding: binary` parts to pass without opting in. · *`b.safeDecompress` names the real absolute-size-bomb refusal code* — The refusal-posture list documented `safe-decompress/output-too-large` for a bomb-by-absolute-size, but that code is never emitted — zlib's `maxOutputLength` throws before allocation and the failure surfaces as `safe-decompress/decompress-failed`. The doc now names the code an operator branching on the result will actually see (the ratio, output-byte, and compressed-input caps are unchanged and enforced). · *`b.safeSmtp.findDotTerminator` example output corrected* — The example claimed the `\r\n.\r\n` terminator in `"Hello world.\r\n.\r\n"` is at index 13; it is at index 12. The example now shows 12 (the implementation was already correct). · *`b.safeIcap` intro status-code summary lists 404 / 405 / 408* — The intro summary said only `100 / 200 / 204 / 400 / 403 / 5xx` are honored, but the parser also accepts `404 / 405 / 408` (legitimate RFC 3507 §4.3.3 codes, already listed in the detailed `parse` block). The intro summary now matches.
12
16
 
13
17
  - v0.13.29 (2026-05-28) — **Doc corrections: AI Act disclosure kind values, SQS queue model, age-gate coupling.** Documentation corrections. The most actionable: b.middleware.aiActDisclosure's @opts listed two EU AI Act transparency `kind` values (`deepfake` and `synthetic-content`) that the middleware does not accept, so they threw at construction; the accepted values use the hyphenated Art. 50 spellings (e.g. `deep-fake`) and include a text-public-interest variant the enum omitted — an operator copying the documented values crashed a compliance middleware at boot. The b.queue docs implied the SQS backend is driven by the generic b.queue.consume loop like local/redis; SQS is actually an SQS-native adapter (complete/fail by message receipt handle, server-side redrive) driven directly, and the docs now say so. b.middleware.ageGate's `requireAge` 451 floor is documented as taking effect only alongside `consentRequired` (it was silently inert without it). Plus a compose-pipeline @since and a flag-context @related correction. No code behavior changed. **Fixed:** *`b.middleware.aiActDisclosure` documents the accepted `kind` values* — The `@opts` listed `kind` as `ai-interaction | deepfake | emotion-recognition | biometric-categorisation | synthetic-content`. Two of those — `deepfake` and `synthetic-content` — are not accepted and threw at construction; the EU AI Act Art. 50 values use hyphenated spellings (e.g. `deep-fake`, the generated-content variant) and include `ai-text-public-interest`, which the documented enum omitted. The `@opts` now lists the full set the middleware accepts. · *`b.queue` SQS backend documented as SQS-native, not consume-driven* — The module docs implied the `sqs` backend is interchangeable with `local`/`redis` under the generic `b.queue.consume` loop. SQS is an SQS-native adapter: `complete` / `fail` act on the message's `receiptHandle` (returned by `lease()`, threaded back by the caller), and DLQ + visibility-expiry are handled server-side by the queue's RedrivePolicy. The docs now state that `sqs` is driven directly (lease → handle → complete/fail) rather than by `b.queue.consume`, and does not use the framework DLQ / sweep. · *`b.middleware.ageGate` documents the `requireAge` / `consentRequired` coupling* — `requireAge` (the HTTP 451 legal floor) is evaluated within the consent classification, so it takes effect only when `consentRequired` is also set — `requireAge` alone, with `consentRequired: null`, never classifies a request as below-threshold and the 451 never fires. The `@opts` and prose now state this coupling instead of presenting `requireAge` as a standalone threshold. · *Smaller doc corrections* — `b.middleware.composePipeline`'s `@since` is corrected to 0.9.43 (its actual ship version). `b.middleware.flagContext`'s `@related` pointed at a non-existent `b.flagClient.getBoolean`; it now references `b.flag.create`.
@@ -47,7 +47,7 @@
47
47
  * requestHumanReview: true,
48
48
  * requestAppeal: true,
49
49
  * requestData: true,
50
- * statutoryDeadlines: { explanation: "30d", appeal: "60d" }
50
+ * statutoryDeadlines: { explanation: "30d", humanReview: null, appeal: null }
51
51
  * }
52
52
  * }
53
53
  */
@@ -17,8 +17,8 @@
17
17
  * review of activity records), SOX §302/§404 (quarterly self-
18
18
  * attestation), SOC 2 CC7.2 (anomaly identification and response),
19
19
  * GDPR Art. 32 (ongoing security testing/evaluation). When `posture`
20
- * is one of `pci-dss` / `hipaa` / `sox` / `soc2`, a `notify`
21
- * callback is mandatory at create-time — the regulators all demand
20
+ * is one of `pci-dss` / `hipaa` / `sox` / `sox-404` / `soc2`, a
21
+ * `notify` callback is mandatory at create-time — the regulators all demand
22
22
  * a follow-up channel.
23
23
  *
24
24
  * Severity classification: `denied` / `failure` outcomes default to
@@ -65,7 +65,7 @@ var CRITICAL_PATTERNS = [
65
65
  /^ato\.killSwitch\.tripped/,
66
66
  ];
67
67
 
68
- var POSTURES_REQUIRING_NOTIFY = ["pci-dss", "hipaa", "sox", "soc2"];
68
+ var POSTURES_REQUIRING_NOTIFY = ["pci-dss", "hipaa", "sox", "sox-404", "soc2"];
69
69
 
70
70
  function _defaultClassify(event) {
71
71
  if (!event || typeof event !== "object" || typeof event.action !== "string") {
@@ -36,8 +36,10 @@ var retryHelper = require("./retry");
36
36
  * Build a circuit-breaker. Returns a CircuitBreaker instance with
37
37
  * `wrap(fn)` (executes `fn` if the breaker is closed; throws an
38
38
  * `Error` with `code: "CIRCUIT_OPEN"` + `isObjectStoreError: true` +
39
- * `permanent: false` when open), `state()`, `reset()`, and
40
- * `onStateChange(handler)` listener registration. Pass-through
39
+ * `permanent: false` when open), `getState()`, `reset()`, and
40
+ * `onStateChange(handler)` listener registration (the handler, and the
41
+ * `onStateChange` opt, receive `{ name, from, to, at }` on every
42
+ * transition). Pass-through
41
43
  * factory: identical instance shape to `b.retry.CircuitBreaker`,
42
44
  * with the framework's `create(opts)` vocabulary.
43
45
  *
@@ -53,8 +55,8 @@ var retryHelper = require("./retry");
53
55
  * failureThreshold: number, // failures in the closed state before opening
54
56
  * cooldownMs: number, // milliseconds the breaker stays open before probing
55
57
  * successThreshold: number, // probe successes required to close from half-open
56
- * audit: Object, // optional b.audit instance for state-change emission
57
- * onStateChange: Function, // ({ name, from, to, at }) → void
58
+ * onStateChange: Function, // ({ name, from, to, at }) → void; also emits the
59
+ * // `breaker.state.change` observability event
58
60
  *
59
61
  * @example
60
62
  * var cb = b.circuitBreaker.create({
@@ -31,8 +31,8 @@
31
31
  * The framework provides:
32
32
  *
33
33
  * - banner({ kind }) → builder for the standard disclosure banner
34
- * - watermark({ kind, ... }) → builder for content-marking tags
35
- * - cspMetaTag({ ... }) → meta tag pair for HTML pages
34
+ * - watermark({ mediaKind, ... }) → builder for content-marking tags
35
+ * - metaTags({ ... }) → meta tag pair for HTML pages
36
36
  * - jsonLdDisclosure({ ... }) → JSON-LD <script> for structured-data emit
37
37
  * - C2pa-stub → operator-feeds-claims pattern for
38
38
  * C2PA Content Credentials integration
package/lib/compliance.js CHANGED
@@ -1407,14 +1407,11 @@ function postureDefault(posture, key) {
1407
1407
  *
1408
1408
  * @example
1409
1409
  * b.compliance.posturesByDomain("privacy");
1410
- * // → ["ccpa", "gdpr", "lgpd-br", "pipl-cn", "appi-jp",
1411
- * // "pdpa-sg", "pipeda-ca", "uk-gdpr"]
1410
+ * // → ["ccpa", "gdpr", "lgpd-br", ...] — every posture whose
1411
+ * // domain is "privacy" (the full set grows as regimes are added)
1412
1412
  *
1413
1413
  * b.compliance.posturesByDomain("health");
1414
- * // → ["hipaa", "wmhmda"]
1415
- *
1416
- * b.compliance.posturesByDomain("payment");
1417
- * // → ["pci-dss"]
1414
+ * // → ["hipaa", "wmhmda", ...] — every "health"-domain posture
1418
1415
  *
1419
1416
  * b.compliance.posturesByDomain("not-a-domain");
1420
1417
  * // → []
@@ -1450,13 +1447,14 @@ function posturesByDomain(domain) {
1450
1447
  *
1451
1448
  * @example
1452
1449
  * b.compliance.posturesByJurisdiction("EU");
1453
- * // → ["gdpr", "dora", "nis2", "cra", "ai-act"]
1450
+ * // → ["gdpr", "dora", "nis2", ...] — every EU-jurisdiction posture
1451
+ * // (the full set grows as regimes are added)
1454
1452
  *
1455
1453
  * b.compliance.posturesByJurisdiction("US");
1456
- * // → ["hipaa", "soc2", "sox"]
1454
+ * // → ["hipaa", "soc2", "sox", ...] — every US-jurisdiction posture
1457
1455
  *
1458
1456
  * b.compliance.posturesByJurisdiction("US-CA");
1459
- * // → ["ccpa"]
1457
+ * // → ["ccpa", ...] — every US-CA (California) posture
1460
1458
  *
1461
1459
  * b.compliance.posturesByJurisdiction("XX");
1462
1460
  * // → []
package/lib/data-act.js CHANGED
@@ -20,8 +20,9 @@
20
20
  *
21
21
  * - Art 4 §1 — let the user access "readily available product
22
22
  * data" generated by their use of the connected product.
23
- * `b.dataAct.userAccessible(productId, userId)` returns the
24
- * operator-supplied data slice.
23
+ * `b.dataAct.recordUserAccess({ productId, userId, dataSlice })`
24
+ * records the operator-supplied data slice for the regulator
25
+ * record.
25
26
  * - Art 5 §1 — share that data with a third-party data
26
27
  * recipient on the user's request, "without undue delay,
27
28
  * free of charge to the user, of the same quality as is
@@ -46,7 +47,7 @@
46
47
  * b.dataAct.declareProduct({ productId, dataHolder, ... })
47
48
  * b.dataAct.recordUserAccess({ productId, userId, dataSlice, ... })
48
49
  * b.dataAct.shareWithThirdParty({ productId, userId, recipient, scope })
49
- * b.dataAct.refuseGatekeeper({ recipient })
50
+ * refuses a DMA-gatekeeper recipient per Art 32 §1
50
51
  * b.dataAct.recordSwitchRequest({ customerId, targetProvider, dataSlices })
51
52
  *
52
53
  * The framework does NOT host the connected-product data itself;
package/lib/mcp.js CHANGED
@@ -399,10 +399,10 @@ function serverGuard(opts) {
399
399
  * exfiltration endpoints. The framework's defense:
400
400
  *
401
401
  * - Strip / refuse executable HTML (`<script>` / `<iframe>` /
402
- * `javascript:` URLs) composes b.guardHtml's strict profile
402
+ * `javascript:` URLs) via built-in dangerous-HTML detection
403
403
  * - Refuse known prompt-injection markers ("ignore previous
404
404
  * instructions", "system: you are now ...", role-claim prefixes)
405
- * composes b.ai.input.classify
405
+ * via a built-in injection-marker matcher
406
406
  * - Cap text length so a tool can't blow the host's context window
407
407
  * out from under it
408
408
  * - Refuse content with `image_url` / `audio_url` / `resource_link`
@@ -418,7 +418,6 @@ function serverGuard(opts) {
418
418
  * posture?: "refuse" | "sanitize" | "audit-only", // default "refuse"
419
419
  * maxTextBytes?: number, // default 64 KiB per content block
420
420
  * allowedHosts?: string[], // for image/audio/resource_link refs
421
- * classifyInput?: fn(text)→{verdict, score} | null, // default b.ai.input.classify
422
421
  * }
423
422
  *
424
423
  * @example
package/lib/retry.js CHANGED
@@ -187,6 +187,10 @@ function _validateBreakerOpts(name, opts) {
187
187
  throw new TypeError("retry.CircuitBreaker: successThreshold must be a positive integer, got " +
188
188
  typeof opts.successThreshold + " " + JSON.stringify(opts.successThreshold));
189
189
  }
190
+ if (opts.onStateChange != null && typeof opts.onStateChange !== "function") {
191
+ throw new TypeError("retry.CircuitBreaker: onStateChange must be a function, got " +
192
+ typeof opts.onStateChange);
193
+ }
190
194
  }
191
195
 
192
196
  // ---- Public surface ----
@@ -381,6 +385,19 @@ class CircuitBreaker {
381
385
  this.consecutiveFailures = 0;
382
386
  this.consecutiveSuccesses = 0;
383
387
  this.openedAt = 0;
388
+ this._stateListeners = [];
389
+ if (typeof merged.onStateChange === "function") this._stateListeners.push(merged.onStateChange);
390
+ }
391
+
392
+ // Register a state-change listener. Called with { name, from, to, at }
393
+ // on every transition (same payload the constructor's onStateChange
394
+ // opt receives). Returns this for chaining.
395
+ onStateChange(handler) {
396
+ if (typeof handler !== "function") {
397
+ throw new TypeError("retry.CircuitBreaker.onStateChange: handler must be a function, got " + typeof handler);
398
+ }
399
+ this._stateListeners.push(handler);
400
+ return this;
384
401
  }
385
402
 
386
403
  // Wrap an async function. The breaker observes outcomes and may fail-fast.
@@ -415,7 +432,16 @@ class CircuitBreaker {
415
432
  _transition(from, to) {
416
433
  if (from === to) return;
417
434
  this.state = to;
435
+ var at = Date.now();
418
436
  _emitEvent("breaker.state.change", 1, { name: this.name, from: from, to: to });
437
+ if (this._stateListeners.length > 0) {
438
+ var payload = { name: this.name, from: from, to: to, at: at };
439
+ for (var i = 0; i < this._stateListeners.length; i++) {
440
+ // Best-effort: a throwing listener must not derail the breaker's
441
+ // own state machine (the transition has already been applied).
442
+ try { this._stateListeners[i](payload); } catch (_e) { /* drop-silent */ }
443
+ }
444
+ }
419
445
  }
420
446
 
421
447
  _onSuccess() {
package/lib/sec-cyber.js CHANGED
@@ -32,7 +32,7 @@
32
32
  *
33
33
  * Public API:
34
34
  *
35
- * b.secCyber.eightKArtifact(opts) -> { artifact, deadline, audit }
35
+ * b.secCyber.eightKArtifact(opts) -> { artifact, deadline, deadlineBusinessDays }
36
36
  * opts:
37
37
  * incidentId: operator-supplied incident reference (string).
38
38
  * registrant: { name, cik, filer }
package/lib/vault-aad.js CHANGED
@@ -35,8 +35,8 @@
35
35
  * .isAadSealed(value) → boolean
36
36
  *
37
37
  * Per the framework's security-first stance:
38
- * - Symmetric key derivation uses HKDF-SHAKE256 (matching the
39
- * vault's KDF) over the vault root key concatenated with the
38
+ * - Symmetric key derivation uses SHAKE256 (matching the vault's
39
+ * KDF) over the vault root key concatenated with the
40
40
  * canonicalized AAD.
41
41
  * - AEAD: XChaCha20-Poly1305 with the AAD threaded into the tag.
42
42
  * - 24-byte nonce, generated fresh per-seal via
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.30",
3
+ "version": "0.13.32",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:1f064530-435e-44b8-9aad-0b35cd7d93b0",
5
+ "serialNumber": "urn:uuid:14f840f1-29a7-4656-93d0-e5fa6ece6ae2",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-29T00:56:19.365Z",
8
+ "timestamp": "2026-05-29T02:13:04.485Z",
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.13.30",
22
+ "bom-ref": "@blamejs/core@0.13.32",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.13.30",
25
+ "version": "0.13.32",
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.13.30",
29
+ "purl": "pkg:npm/%40blamejs/core@0.13.32",
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.13.30",
57
+ "ref": "@blamejs/core@0.13.32",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]