@blamejs/core 0.10.13 → 0.10.14

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,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.10.x
10
10
 
11
+ - v0.10.14 (2026-05-18) — **codebase-patterns hardening — test-side catalog gains 3 new detectors + 1 migration; lib-side catalog gains 1 new detector with comment-skip preprocessing.** Closes the same class of bug that caused the v0.10.13 macOS hang (test-discipline-without-enforcement). **(a) `test-codebase-patterns.test.js` — test-side antipattern runner** now supports `matchOn: "basename"` mode and `requires` companion-content checks. **(b) M1 migration —** `testNoReleaseNamedTestFiles` moves from the lib-side catalog to the test-side catalog (the rule scans test-file basenames, not lib-source content). **(c) N1 — `Promise + setTimeout` direct sleep in tests refused.** Tests calling `await new Promise(r => setTimeout(r, N))` for synchronization MUST use `helpers.waitUntil` per CLAUDE.md rule §11b. 49 pre-existing files are allowlisted as a documented v0.10.14 migration backlog; the gate prevents new occurrences. **(d) N2 — hardcoded server bind ports refused.** Tests calling `.listen(N)` with a literal non-zero port MUST use `.listen(0)` + `server.address().port` to avoid `SMOKE_PARALLEL=64` bind races. Detector scoped to the bind path (`.listen(...)`); read-only protocol-constant references (`port: 993` / `port: 587` in autoconfig XML) don't trip. **(e) N3 — tests creating `b.db` handles without an isolation primitive refused.** Any test calling `b.db.create(` MUST also wire one of `helpers.setupTestDb` / `helpers.setupVaultOnly` / `node:fs.mkdtempSync`. Leaked per-test SQLite state corrupts subsequent tests under `SMOKE_PARALLEL=64`. **(f) N4 — raw `audit.emit(...)` outside drop-silent wrap refused (lib).** Per CLAUDE.md rule §5 hot-path audit sinks must be drop-silent. Detector found and fixed an existing violation in `lib/subject.js:_writeAudit` whose comment promised swallowing but actually let the throw escape. The lib-side runner gains a `skipCommentLines` per-entry opt so docstring `@example` lines don't trip detectors that match comment-friendly tokens. **(g) N5 deferred —** `Date.now()` vs `process.hrtime()` for elapsed-time math needs semantic distinction (elapsed-math vs row-age); regex alone is too noisy. v0.10.13's stream-throttle elapsed-clamp shipped the highest-value fix already; remaining call sites get per-file review in a later patch.
11
12
  - v0.10.13 (2026-05-18) — **`b.cms` CMS codec + `b.streamThrottle` bandwidth limiter + histogram-aware snapshot writer + Windows-safe daemonize.** **`b.cms` — RFC 5652 Cryptographic Message Syntax codec.** New crypto primitive: in-tree CMS encoder + decoder built on the existing `b.asn1Der` walker and the vendored noble-post-quantum primitives. **(a) `b.cms.encodeSignedData({ encapContent, digestAlg, signers })`** — emits a DER-encoded `ContentInfo` carrying `SignedData` per RFC 5652 §5 with PQC signers: ML-DSA-65 + ML-DSA-87 ([RFC 9909](https://www.rfc-editor.org/rfc/rfc9909.html)) and SLH-DSA-SHAKE-256f ([RFC 9881](https://www.rfc-editor.org/rfc/rfc9881.html)). Digest algorithms are SHA3-256 or SHA3-512 (PQC-first; SHA-2 family refused with `cms/bad-digest`). Signed-attributes carry `contentType` + `messageDigest` + `signingTime` in DER-canonical SET-OF ordering; the signature input re-tags the IMPLICIT `[0]` to the universal SET (`0x31`) per §5.4 paragraph 3 so signatures round-trip with any conforming verifier. Signer identifiers carry the full `issuerAndSerialNumber` extracted from the operator-supplied cert DER ([RFC 5652 §10.2.4](https://www.rfc-editor.org/rfc/rfc5652#section-10.2.4)). **(b) `b.cms.encodeEnvelopedData({ plaintext, recipients })`** — emits a DER-encoded `ContentInfo` carrying `EnvelopedData` with `KEMRecipientInfo` recipients per [RFC 9629](https://www.rfc-editor.org/rfc/rfc9629.html) and ML-KEM-1024 per [RFC 9936](https://www.rfc-editor.org/rfc/rfc9936.html). Each recipient encapsulates against the operator-supplied ML-KEM-1024 public key; the framework's SHAKE256 KDF derives a 32-byte content-encryption KEK from the KEM shared-secret bound to the literal label `cms/kemri/chacha20-poly1305` (so a key derived for this composition cannot be confused with one derived for any other framework path). Content encryption is ChaCha20-Poly1305 ([RFC 8103](https://www.rfc-editor.org/rfc/rfc8103.html) OID); the AEAD tag makes Efail-class CBC-malleability impossible by construction ([CVE-2017-17688](https://nvd.nist.gov/vuln/detail/CVE-2017-17688) / [CVE-2017-17689](https://nvd.nist.gov/vuln/detail/CVE-2017-17689)). **(c) `b.cms.decode(buf, { maxBytes? })`** — returns `{ contentType, content }` where `contentType` is the dotted-OID string and `content` is the inner `asn1-der` node. Refuses input past `maxBytes` (default 64 MiB), non-SEQUENCE top-level, missing `[0] EXPLICIT` content, and malformed OID encodings (closes the [CVE-2022-47629](https://nvd.nist.gov/vuln/detail/CVE-2022-47629) libksba class via the existing `b.asn1Der` strict-decode posture). **(d) Refusal posture documented in `lib/cms-codec.js` @intro**: only PQC signature algorithms (`cms/bad-sig-alg`), only ML-KEM-1024 recipients (`cms/bad-recipient-type`), non-empty signers / recipients required at encode (`cms/no-signers` / `cms/no-recipients`). **Operator impact:** no breaking changes; new primitive at `b.cms`. **Deferred to v0.10.14:** the on-the-wire S/MIME 4.0 layer ([RFC 8551](https://www.rfc-editor.org/rfc/rfc8551.html) `multipart/signed` framing, base64 DER body, `micalg` mapping) and OpenPGP encrypt + decrypt + WKD discovery ([RFC 9580](https://www.rfc-editor.org/rfc/rfc9580.html) §5.1 / §5.13 packets plus [draft-koch-openpgp-webkey-service](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/)) land together so the mail-crypto surface lights up coherently rather than half-on-each-side across two patches. AuthEnvelopedData ([RFC 5083](https://www.rfc-editor.org/rfc/rfc5083.html)) as a distinct `ContentInfo` shape is deferred — EnvelopedData with ChaCha20-Poly1305 is already AEAD by construction; the §5083 OID rewrap lights up alongside S/MIME for peers that refuse the EnvelopedData form. References: [RFC 5652 CMS](https://www.rfc-editor.org/rfc/rfc5652.html) · [RFC 9629 KEMRecipientInfo](https://www.rfc-editor.org/rfc/rfc9629.html) · [RFC 9909 ML-DSA in X.509+CMS](https://www.rfc-editor.org/rfc/rfc9909.html) · [RFC 9881 SLH-DSA in X.509+CMS](https://www.rfc-editor.org/rfc/rfc9881.html) · [RFC 9936 ML-KEM in CMS](https://www.rfc-editor.org/rfc/rfc9936.html) · [RFC 8103 ChaCha20-Poly1305 in CMS](https://www.rfc-editor.org/rfc/rfc8103.html). **(e) `b.streamThrottle` — token-bucket bandwidth limiter.** New primitive: `b.streamThrottle.create({ bytesPerSec, burstBytes? })` returns a shared token bucket whose `.transform()` instances each consume from the same budget. The missing primitive between per-request rate-limit and per-process worker pools: N parallel transfers share the operator-configured byte budget rather than each getting their own. Composes with `node:stream.pipeline` as a regular `stream.Transform`; chunks larger than `burstBytes` refuse with `stream-throttle/oversize-chunk` unless `transform({ allowOversize: true })`. Algorithm is the [RFC 2697 srTCM](https://www.rfc-editor.org/rfc/rfc2697.html) single-rate token-bucket shape, with lazy refill so there is no per-throttle background timer. **(f) Histogram-aware metrics snapshot writer.** `b.metrics.snapshot.startWriter` gains an opt-in `registry` field. When supplied, the JSON snapshot grows a `metrics` field carrying every registered counter / gauge / histogram in structured form — histograms include `buckets` + `observations: [{ labels, counts, sum, count }]`, so sidecar readers compose `histogram_quantile()` against the snapshot file without running a separate `/metrics` HTTP endpoint. `fileMode` default unchanged (0o640). **(g) Windows-safe daemonize.** `b.daemon.start` detached-fork mode now branches by platform. POSIX continues inheriting the parent-opened log FD via `stdio: ["ignore", logFd, logFd]` (unchanged). Windows now uses `stdio: "ignore"` + `windowsHide: true` so the child has no inherited handles that the OS invalidates on parent exit — the previously-broken Windows daemonize path now produces a survivable detached process. The child is responsible for opening its own log file (operators pass `--log` in `opts.args`). `daemon.started` audit gains `stdioMode` so operators can grep for the chosen strategy. **Closes: [#94](https://github.com/blamejs/blamejs/issues/94), [#100](https://github.com/blamejs/blamejs/issues/100), [#101](https://github.com/blamejs/blamejs/issues/101); also closes [#92](https://github.com/blamejs/blamejs/issues/92) and [#93](https://github.com/blamejs/blamejs/issues/93) (already shipped in v0.10.9 as `b.promisePool` / `b.sdNotify` — left open until now).**
12
13
  - v0.10.12 (2026-05-18) — **`b.agent.tenant` adoption across the mail-server listeners.** The v0.10.11 shared `b.mail.serverRegistry` primitive gains optional `opts.tenantScope` (a `b.agent.tenant.create()` instance) + `opts.agentTenantId` (the tenant this listener serves). When supplied, every method dispatch first gates on `tenantScope.check(state.actor, agentTenantId)` BEFORE guard validation or audit emission; cross-tenant access surfaces as the v0.9.25-typed `agent-tenant/cross-tenant-access-refused` which the listener's catch-path converts to the protocol's `BAD` / `NO` refusal reply. **(a) `b.mail.server.imap.create({ tenantScope, agentTenantId })`** — IMAP dispatch is gated for every command after AUTH. **(b) `b.mail.server.jmap.create({ tenantScope, agentTenantId })`** — JMAP per-method dispatch routes through the tenant scope alongside its existing per-`accountId` isolation. **(c) `b.mail.server.managesieve.create({ tenantScope, agentTenantId })`** — ManageSieve same pattern. **(d) `b.mail.server.submission.create({ tenantScope, agentTenantId })`** — submission listener gates at the AUTH-success boundary (before `state.actor` is committed) so cross-tenant authentication surfaces as `535 5.7.0 Authentication rejected (cross-tenant)` and the SMTP envelope never begins under the wrong tenant. **(e) `b.mail.server.pop3.create({ tenantScope, agentTenantId })`** — same AUTH-success gate; cross-tenant refusal returns `-ERR Authentication rejected (cross-tenant)`. New audit events: `mail.server.submission.cross_tenant_refused` and `mail.server.pop3.cross_tenant_refused`. **Operator impact:** no breaking changes — `tenantScope` / `agentTenantId` are optional; operators not running multi-tenant see identical behavior. Operators with multi-tenant deployments wire `b.agent.tenant.create({...})` once and pass the same scope to every per-tenant listener instance — cross-tenant isolation becomes structural rather than per-handler opt-in. **Deferred to v0.10.12.1:** per-tenant `b.mailStore` seal-key derivation via `tenantScope.derivedKey(tenantId, "seal")` and per-tenant audit namespaces via `tenantScope.auditFor(tenantId)`. Today every mail listener seals through the framework primary vault key — adequate for single-tenant and multi-tenant-trusted deployments; the v0.10.12.1 follow-up adds per-tenant key separation for compromise-isolation use cases. References: [RFC 9051 IMAP4rev2 §3 state machine](https://www.rfc-editor.org/rfc/rfc9051#section-3) · [RFC 8620 JMAP Core §1.6.2 accountId](https://www.rfc-editor.org/rfc/rfc8620#section-1.6.2) · [RFC 6409 Submission §6.1 actor-to-MAIL-FROM identity binding](https://www.rfc-editor.org/rfc/rfc6409#section-6.1) · [RFC 1939 POP3 §6 transaction state](https://www.rfc-editor.org/rfc/rfc1939#section-6) · v0.9.25 `b.agent.tenant` contract.
13
14
  - v0.10.11 (2026-05-18) — **Mail-server per-method registration sweep.** New shared primitive `b.mail.serverRegistry` (`lib/mail-server-registry.js`) replaces the hand-rolled `switch (verb)` dispatchers in the IMAP, JMAP, and ManageSieve listener factories. Operators can now override individual command / method handlers (e.g. IMAP `FETCH`, JMAP `Email/query`, ManageSieve `PUTSCRIPT`) via `opts.overrides: { NAME: { fn, maxHandlerBytes, maxHandlerMs } }` without re-implementing wire-protocol state machines or bypassing the guard substrate. **(a) Per-handler resource budgets — required at registration.** Operators MUST supply `maxHandlerBytes` (≤ 256 MiB) and `maxHandlerMs` (≤ 5 min) on every override; the registration throws `mail-server-registry/bad-max-handler-bytes` / `bad-max-handler-ms` on missing or out-of-range budgets. Defends [CVE-2024-34055](https://nvd.nist.gov/vuln/detail/CVE-2024-34055) (Cyrus authenticated OOM) and [CVE-2026-26312](https://nvd.nist.gov/vuln/detail/CVE-2026-26312) (Stalwart malformed nested `message/rfc822` cyclical OOM) by forcing operators to declare the resource ceiling explicitly. **(b) Catalogue gate.** Per-protocol method names outside the IANA / RFC catalogue refuse registration unless `allowExperimental: true` is supplied — opting in audits the registration so operators can grep for off-spec handlers. **(c) Guard chain preserved.** The listener factories run `b.guardImapCommand` / `b.guardJmap` / `b.guardManagesieveCommand` BEFORE the registry lookup; operator overrides cannot bypass the wire-protocol validation, smuggling defenses, or rate-limit budgets. **(d) Handler timeout.** Promise-returning handlers wrap through `b.safeAsync.withTimeout(maxHandlerMs)`; a runaway override raises `mail-server-registry/handler-timeout` rather than pinning the connection. **(e) Defaults seeded.** IMAP picks up 30 verbs (CAPABILITY, NOOP, LOGOUT, ID, STARTTLS, AUTHENTICATE, LOGIN, ENABLE, SELECT, EXAMINE, LIST, STATUS, NAMESPACE, APPEND, CHECK, CLOSE, UNSELECT, EXPUNGE, FETCH, STORE, UID, IDLE, DONE — plus the previously-undispatched SEARCH / CREATE / DELETE / RENAME / SUBSCRIBE / UNSUBSCRIBE / COPY / MOVE which default to `NO not-configured` until operator overrides); ManageSieve picks up 12 verbs (CAPABILITY, NOOP, STARTTLS, LOGOUT, AUTHENTICATE, HAVESPACE, PUTSCRIPT, LISTSCRIPTS, SETACTIVE, GETSCRIPT, DELETESCRIPT, RENAMESCRIPT); JMAP wraps the existing `opts.methods` map with a one-time deprecation audit (`mail.server.jmap.methods_opt_deprecated`) and routes through the same registry — operators migrate to `opts.overrides` with explicit budgets. **(f) New audit event:** `mail.serverRegistry.method_dispatch` carries `{ protocol, name, source: "builtin" | "operator-override" }` on every dispatch; `mail.serverRegistry.experimental_registration` audits opt-in off-catalogue registrations. **Operator impact:** existing JMAP `opts.methods` callers see the deprecation audit but continue to function (legacy auto-budget = 10 MiB / 30 s); existing IMAP / ManageSieve operators have no migration burden — the listener factories continue to accept the same opts shape. Operators wiring NEW overrides MUST supply explicit budgets. References: [RFC 9051 IMAP4rev2](https://www.rfc-editor.org/rfc/rfc9051) · [RFC 8620 JMAP Core](https://www.rfc-editor.org/rfc/rfc8620) · [RFC 8621 JMAP for Mail](https://www.rfc-editor.org/rfc/rfc8621) · [RFC 5804 ManageSieve](https://www.rfc-editor.org/rfc/rfc5804) · [RFC 2971 IMAP4 ID](https://www.rfc-editor.org/rfc/rfc2971) · [RFC 2177 IMAP IDLE](https://www.rfc-editor.org/rfc/rfc2177) · [CVE-2024-34055](https://nvd.nist.gov/vuln/detail/CVE-2024-34055) · [CVE-2026-26312](https://nvd.nist.gov/vuln/detail/CVE-2026-26312).
package/lib/subject.js CHANGED
@@ -635,16 +635,20 @@ function _subjectHash(subjectId) {
635
635
 
636
636
  function _writeAudit(action, subjectId, outcome, metadata) {
637
637
  // recordSafe — audit failure must not roll back the subject mutation
638
- // that already touched the database. Best-effort emission with errors
639
- // swallowed; operators see them in stderr if they fire.
640
- audit.emit({
641
- actor: {},
642
- action: action,
643
- resource: { kind: "subject", id: subjectId },
644
- outcome: outcome,
645
- reason: metadata && metadata.requestReason ? metadata.requestReason : null,
646
- metadata: metadata || null,
647
- });
638
+ // that already touched the database. Drop-silent per CLAUDE.md rule
639
+ // §5 (hot-path audit sinks): swallow any throw from audit.emit so a
640
+ // misconfigured sink doesn't crash a partially-committed subject
641
+ // mutation. Errors surface via the audit sink's own logger.
642
+ try {
643
+ audit.emit({
644
+ actor: {},
645
+ action: action,
646
+ resource: { kind: "subject", id: subjectId },
647
+ outcome: outcome,
648
+ reason: metadata && metadata.requestReason ? metadata.requestReason : null,
649
+ metadata: metadata || null,
650
+ });
651
+ } catch (_e) { /* drop-silent — audit emit failure must not block subject mutation */ }
648
652
  }
649
653
 
650
654
  function _resetForTest() { db.reset(); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.10.13",
3
+ "version": "0.10.14",
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.6",
5
- "serialNumber": "urn:uuid:7f2411e6-1488-4a9f-954b-bef06640070c",
5
+ "serialNumber": "urn:uuid:538b90b1-594c-4130-b06c-a5a105757062",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-18T20:01:39.367Z",
8
+ "timestamp": "2026-05-18T21:01:40.359Z",
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.10.13",
22
+ "bom-ref": "@blamejs/core@0.10.14",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.10.13",
25
+ "version": "0.10.14",
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.10.13",
29
+ "purl": "pkg:npm/%40blamejs/core@0.10.14",
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.10.13",
57
+ "ref": "@blamejs/core@0.10.14",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]