@blamejs/core 0.13.44 → 0.13.45

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.13.x
10
10
 
11
+ - v0.13.45 (2026-05-29) — **`b.cert` now fetches and staples a validated OCSP response per certificate, and validates declared compliance postures at create().** Two capabilities that b.cert documented but did not act on are now wired through. OCSP stapling: the cert manager fetches the leaf's OCSP response from the responder named in its Authority Information Access extension, validates it against the issuer (status, nonce, serial) via b.network.tls.ocsp, caches the DER, and exposes it on getContext().ocspResponse so a TLS server's OCSPRequest handler can staple it. The fetch runs in the background on a refresh timer and never blocks cert.start() — a slow or unreachable responder produces an audited per-certificate failure, not a stalled boot. Compliance postures: opts.compliance names are now validated against b.compliance.KNOWN_POSTURES at create() (an unknown name throws cert/unknown-compliance-posture instead of being silently recorded) and are surfaced on getContext().compliance for an auditor. Storage-confidentiality postures hold by construction because cert keys and certificates are always sealed at rest. The supporting composition primitive b.network.tls.ocsp.fetch (build request, POST to the responder through b.httpClient, validate the response) is now part of the public OCSP surface. **Added:** *b.network.tls.ocsp.fetch — fetch and validate an OCSP response* — The OCSP helper set previously built requests and evaluated responses but had no way to actually retrieve one. b.network.tls.ocsp.fetch({ leafPem, issuerPem, nonce?, timeoutMs? }) reads the responder URL from the leaf certificate's Authority Information Access extension, builds the request, POSTs it through b.httpClient (so the SSRF guard and pinned DNS apply), and validates the response against the issuer — returning the validated DER plus the parsed evaluation. It rejects when the leaf carries no OCSP responder URL or the response fails validation. · *b.cert staples a validated OCSP response per certificate* — With ocsp.stapling enabled (the default), the cert manager refreshes each certificate's OCSP response on a timer (ocsp.refreshMs, default 12h) and caches the validated DER. getContext(serverName).ocspResponse returns that DER for a TLS server to hand back from its OCSPRequest handler. The refresh runs in the background and is never on the path of cert.start(): an unreachable or slow responder is recorded as an audited cert.ocsp.refresh failure for that certificate and leaves the rest of the manager running. **Changed:** *opts.compliance posture names are validated at create()* — b.cert.create now checks each name in opts.compliance against b.compliance.KNOWN_POSTURES and throws cert/unknown-compliance-posture on an unrecognized name, so a typo is caught at construction rather than being silently recorded. The declared postures are surfaced on getContext().compliance. Cert keys and certificates are always sealed at rest, so storage-confidentiality postures are satisfied by construction.
12
+
11
13
  - v0.13.44 (2026-05-29) — **Error codes on the consent, compliance, and protocol namespaces now follow the namespace/kebab-case contract.** The framework's error contract is `err.code = "namespace/kebab-case"`, and the vast majority of namespaces already followed it. This release normalizes the holdouts: fifteen namespaces that threw bare UPPER_SNAKE codes with no namespace, and nine that used a camelCase namespace prefix. After this release every error these namespaces throw carries a `namespace/kebab-case` code, so an operator switching on `err.code` no longer has to special-case them. This is a breaking change for code that matches the old strings — pre-1.0, there is no compatibility shim, so update any `err.code` comparisons against the listed namespaces. A codebase check now enforces the convention so it cannot regress. A small set of older codes (the cluster, scheduler, circuit-breaker, object-store, and upload subsystems) is intentionally left for the 1.0 release, where it will carry a deprecation cycle. **Changed:** *Bare UPPER_SNAKE error codes are now namespaced (breaking)* — Fifteen namespaces threw bare UPPER_SNAKE error codes with no namespace prefix (for example `mcp` threw `BAD_JSON`, `BAD_ENVELOPE`, `BAD_METHOD`). Their `err.code` values are now `namespace/kebab-case` — `mcp/bad-json`, `mcp/bad-envelope`, and so on. The affected namespaces are `b.a2a`, `b.aiInput`, `b.aiPref`, `b.budr`, `b.contentCredentials`, `b.darkPatterns`, `b.fapi2`, `b.fdx`, `b.graphqlFederation`, `b.iabTcf`, `b.iabMspa`, `b.mcp`, `b.secCyber`, `b.sse`, and `b.tcpa10dlc`. Operators matching the old bare codes on `err.code` must update those comparisons; the error message text is unchanged. · *camelCase error-code namespaces are now kebab-case (breaking)* — Nine namespaces emitted error codes whose namespace segment was camelCase (for example `aiDp/bad-bound`, `argParser/flag-duplicate`). The namespace segment is now kebab-case to match every other code: `ai-dp/`, `ai-capability/`, `ai-quota/`, `arg-parser/`, `audit-sign/`, `auth-step-up/`, `ddl-change-control/`, `dr-runbook/`, `tenant-quota/`, and `boot-gates/`. The `b.*` API namespace keys themselves are unchanged (those remain camelCase, e.g. `b.argParser`); only the `err.code` string changed. Operators matching these `err.code` strings must update them. **Detectors:** *Error-code shape is enforced* — A codebase check now flags any error code constructed via `new XError(...)` or the per-class `factory(...)` whose value is a bare UPPER_SNAKE string or carries a camelCase namespace segment, so the `namespace/kebab-case` contract cannot silently regress. It correctly ignores native error constructors (whose first argument is the message, not a code).
12
14
 
13
15
  - v0.13.43 (2026-05-29) — **LTS window stated consistently as 24 months, experimental primitives declared semver-exempt, and stale version references cleaned up.** Documentation and operator-facing string hygiene ahead of the 1.0 stability contract. The LTS support window is now stated as 24 months everywhere (GOVERNANCE.md and the LTS calendar previously disagreed — 24 vs 18). The LTS calendar gains an explicit clause that primitives marked experimental are exempt from the stability/LTS contract, so operators can tell at a glance which surfaces may change between minors. Several error messages and doc blocks that pinned to long-past version numbers ("lands in v0.10.9", "not supported in v0.12.7", "ships in v0.6.45+") are restated version-agnostically with their escape hatch, and the S/MIME module now points operators at the live PGP encrypt/decrypt path for confidentiality today. No API or behavior changes. **Changed:** *LTS support window is consistently 24 months* — `GOVERNANCE.md` promised a 24-month LTS window while `LTS-CALENDAR.md` and `SECURITY.md` stated 18 — a six-month contradiction in the single most load-bearing number of the support contract. All three now state 24 months of security-only patches per major. The calendar table and the supported-versions prose are aligned. · *Experimental primitives are declared exempt from the stability contract* — `LTS-CALENDAR.md` now states explicitly that primitives documented as experimental (shown as "experimental" on their wiki page, and via the `experimental` segment in namespaces like `b.jose.jwe.experimental`) are not covered by the stability contract or the LTS window — they may change signature, behavior, or wire format, or be removed, in any minor without a deprecation cycle. This lets the framework ship primitives that track in-flight standards without freezing an unsettled format, and tells operators precisely which surfaces are not yet frozen. **Fixed:** *Stale version references removed from operator-facing errors and docs* — Error messages and documentation that pinned to long-past versions are restated version-agnostically with the relevant escape hatch: ZIP64 and unsupported-compression errors in archive reading, the CMS AuthEnvelopedData / fielded-decoder notes, the mTLS CRL-engine error, the safe-archive format-detection summary (which also now correctly lists the supported zip / tar / tar.gz set rather than claiming only zip), and the AI-content IPTC-reader note. None changed behavior; they no longer read as broken promises against the published version history. · *S/MIME confidentiality deferral points to the working PGP path* — `b.mail.crypto.smime` ships sign + verify; encrypt/decrypt is deferred. The deferral note previously cited an open-ended internal condition; it now names the escape hatch directly — use `b.mail.crypto.pgp.encrypt` / `decrypt` for mail confidentiality today — and states the concrete trigger that would re-open S/MIME-specific (X.509-recipient) encryption. · *Governance doc no longer references an internal file operators cannot see* — `GOVERNANCE.md` cited a rule number in a contributor-only file that does not ship in the repository. The deprecation-policy statement is now self-contained.
package/README.md CHANGED
@@ -114,7 +114,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
114
114
  - **CMS codec** — RFC 5652 Cryptographic Message Syntax encoder + decoder with PQC signers (ML-DSA-65 / ML-DSA-87 / SLH-DSA-SHAKE-256f; RFC 9909 + 9881) and KEMRecipientInfo recipients (ML-KEM-1024; RFC 9629 + 9936); ChaCha20-Poly1305 content encryption (RFC 8103) so Efail-class malleability cannot apply (`b.cms`)
115
115
  - **Stream throttle** — shared token-bucket bandwidth limiter (RFC 2697 srTCM shape); N concurrent `node:stream` pipelines draw from one operator-configured `bytesPerSec` budget (`b.streamThrottle`)
116
116
  - **TLS-RPT receiver** — RFC 8460 inbound aggregate-report ingest; HTTPS POST handler + §4.4 schema parser with gzip-bomb / ratio-bomb / depth-bomb defenses (`b.mail.deploy.parseTlsRptReport` / `b.mail.deploy.tlsRptIngestHttp`)
117
- - **TLS / channel binding** — RFC 9266 TLS-Exporter token-to-session pinning (`b.tlsExporter`); RFC 9162 CT v2 inclusion-proof verification (`b.network.tls.ct.verifyInclusion`); RFC 8555 ACME + RFC 9773 ARI for 47-day certs with `{ jitter: true }` fleet-scheduling (`b.acme.renewIfDue`); draft-aaron-acme-profiles (`acme.listProfiles()` + `newOrder({ profile })`); draft-ietf-acme-dns-account-label (`acme.dnsAccount01ChallengeRecord(token, { identifier })`); RFC 8470 0-RTT inbound posture refuse / replay-cache (`b.router.create({tls0Rtt})`); RFC 9794 SecP256r1MLKEM768 in preferred-group order (`b.network.tls.preferredGroups`)
117
+ - **TLS / channel binding** — RFC 9266 TLS-Exporter token-to-session pinning (`b.tlsExporter`); RFC 9162 CT v2 inclusion-proof verification (`b.network.tls.ct.verifyInclusion`); RFC 8555 ACME + RFC 9773 ARI for 47-day certs with `{ jitter: true }` fleet-scheduling (`b.acme.renewIfDue`); draft-aaron-acme-profiles (`acme.listProfiles()` + `newOrder({ profile })`); draft-ietf-acme-dns-account-label (`acme.dnsAccount01ChallengeRecord(token, { identifier })`); RFC 8470 0-RTT inbound posture refuse / replay-cache (`b.router.create({tls0Rtt})`); RFC 9794 SecP256r1MLKEM768 in preferred-group order (`b.network.tls.preferredGroups`); RFC 6960 OCSP stapling — the cert manager (`b.cert`) fetches + validates each managed certificate's OCSP response (`b.network.tls.ocsp.fetch`) on a refresh cadence and exposes it on the served context for a TLS server's `OCSPRequest` handler to staple
118
118
  - **mTLS CA** — pure-JS, issues clientAuth / serverAuth / dual-EKU certs with SAN; auto-detects highest-PQC signature alg (today ECDSA-P384-SHA384; self-upgrades to SLH-DSA / ML-DSA when X.509 ecosystem catches up); PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`)
119
119
  ### HTTP
120
120
 
package/lib/cert.js CHANGED
@@ -22,9 +22,9 @@
22
22
  * - `b.acme.create` → ACME orders, JWS, ARI fetch
23
23
  * - `b.vault.seal` → sealed-disk persistence of certs + keys + account material
24
24
  * - `b.safeAsync.repeating` → renewal scheduler with drop-silent error path
25
- * - `b.network.tls.ocsp` → server-side stapling helpers
25
+ * - `b.network.tls.ocsp` → fetches + caches a validated OCSP response per cert for server-side stapling
26
26
  * - `b.audit` → cert.* lifecycle audit chain
27
- * - `b.compliance` → posture refusals (e.g. plaintext storage refused under HIPAA / PCI)
27
+ * - `b.compliance` → validates the declared posture names; storage-confidentiality postures hold because keys/certs are always sealed at rest
28
28
  *
29
29
  * Does NOT ship the challenge-solver implementations (HTTP-01 server,
30
30
  * DNS provider integrations, TLS-ALPN-01 socket). Those are operator-
@@ -60,6 +60,7 @@ var acme = lazyRequire(function () { return require("./acme"); });
60
60
  var vault = lazyRequire(function () { return require("./vault"); });
61
61
  var audit = lazyRequire(function () { return require("./audit"); });
62
62
  var networkTls = lazyRequire(function () { return require("./network-tls"); });
63
+ var compliance = lazyRequire(function () { return require("./compliance"); });
63
64
  var bCrypto = lazyRequire(function () { return require("./crypto"); });
64
65
 
65
66
  var CertError = defineClass("CertError");
@@ -222,7 +223,7 @@ function _createSealedDiskStorage(opts) {
222
223
  * refreshMs: number, // default 12h — OCSP-response cache lifetime
223
224
  * },
224
225
  * audit: boolean | object, // default true — emit cert.* lifecycle events via b.audit.safeEmit
225
- * compliance: Array<string>, // optional — posture refusals (e.g. ["hipaa"]); refuses plaintext storage etc.
226
+ * compliance: Array<string>, // optional — posture names (e.g. ["hipaa"]); validated against b.compliance.KNOWN_POSTURES (throws on an unknown name) + surfaced on getContext().compliance. Cert keys/certs are always sealed at rest, so storage-confidentiality postures hold by construction.
226
227
  *
227
228
  * @example
228
229
  * var mgr = b.cert.create({
@@ -383,13 +384,31 @@ function create(opts) {
383
384
 
384
385
  // ---- Audit + compliance ----
385
386
  var auditEnabled = opts.audit !== false;
386
- var compliance = Array.isArray(opts.compliance) ? opts.compliance.slice() : [];
387
+ var compliancePostures = Array.isArray(opts.compliance) ? opts.compliance.slice() : [];
388
+ // Validate posture names against the framework catalog so a typo is
389
+ // caught at create() rather than silently ignored. The cert manager
390
+ // satisfies the storage-confidentiality postures (HIPAA / PCI-DSS /
391
+ // GDPR …) by construction — keys + certs are always sealed at rest
392
+ // (storage.type is enforced to "sealed-disk"), so there is no plaintext-
393
+ // storage state for a posture to fail to. The postures are recorded +
394
+ // surfaced on the served context for an auditor.
395
+ if (compliancePostures.length > 0) {
396
+ var knownPostures = compliance().KNOWN_POSTURES;
397
+ compliancePostures.forEach(function (p) {
398
+ if (knownPostures.indexOf(p) === -1) {
399
+ throw new CertError("cert/unknown-compliance-posture",
400
+ "cert.create: opts.compliance posture '" + p + "' is not a known posture; " +
401
+ "see b.compliance.KNOWN_POSTURES");
402
+ }
403
+ });
404
+ }
387
405
 
388
406
  // ---- Internal state ----
389
407
  var emitter = new EventEmitter();
390
- var loadedContexts = Object.create(null); // name → { cert, key, ca, expiresAt, fingerprintSha256, sniNames }
408
+ var loadedContexts = Object.create(null); // name → { cert, key, ca, expiresAt, fingerprintSha256, sniNames, ocspResponse }
391
409
  var acmeClient = null;
392
410
  var scheduler = null;
411
+ var ocspTimer = null;
393
412
  var stopped = false;
394
413
 
395
414
  function _emitAudit(action, outcome, metadata) {
@@ -689,6 +708,39 @@ function create(opts) {
689
708
  }
690
709
  }
691
710
 
711
+ // Split a PEM chain into individual certificate blocks (leaf first).
712
+ function _splitPemChain(pem) {
713
+ return pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) || [];
714
+ }
715
+
716
+ // Fetch + cache a validated OCSP response for one managed cert, for
717
+ // server-side stapling. Fail-soft: a responder error, or no issuer in the
718
+ // served chain, leaves any prior staple in place and never throws — an
719
+ // absent staple degrades gracefully (clients fall back to their own
720
+ // revocation checking). The validated DER is exposed on
721
+ // getContext().ocspResponse for the operator's TLS server to staple via
722
+ // its 'OCSPRequest' handler.
723
+ async function _refreshOcspFor(name) {
724
+ var ctx = loadedContexts[name];
725
+ if (!ctx || !ocspStapling) return;
726
+ var chain = _splitPemChain(ctx.cert);
727
+ if (chain.length < 2) return; // no issuer in the served chain
728
+ try {
729
+ // allow:raw-outbound-http — b.network.tls.ocsp.fetch composes b.httpClient internally (SSRF guard + pinned DNS); not a raw outbound call
730
+ var rv = await networkTls().ocsp.fetch({ leafPem: chain[0], issuerPem: chain[1] });
731
+ ctx.ocspResponse = rv.ocspDer;
732
+ _emitAudit("cert.ocsp.refreshed", "success", { name: name });
733
+ } catch (e) {
734
+ _emitAudit("cert.ocsp.refresh-failed", "failure",
735
+ { name: name, error: (e && e.message) || String(e) });
736
+ }
737
+ }
738
+
739
+ async function _refreshAllOcsp() {
740
+ var keys = Object.keys(loadedContexts);
741
+ for (var i = 0; i < keys.length; i += 1) { await _refreshOcspFor(keys[i]); }
742
+ }
743
+
692
744
  async function start() {
693
745
  if (stopped) {
694
746
  throw new CertError("cert/already-stopped",
@@ -706,12 +758,21 @@ function create(opts) {
706
758
  await _renewCheckOne(certsByName[keys[ki]]);
707
759
  }
708
760
  }, renewIntervalMs, { name: "cert-renew" });
761
+ // 3. OCSP stapling. The initial fetch runs in the background so a slow
762
+ // responder never delays start(); the staple becomes available
763
+ // shortly after, and the timer refreshes on the configured cadence.
764
+ if (ocspStapling) {
765
+ _refreshAllOcsp().catch(function () { /* per-cert errors already audited */ });
766
+ ocspTimer = safeAsync.repeating(_refreshAllOcsp, ocspRefreshMs, { name: "cert-ocsp" });
767
+ }
709
768
  }
710
769
 
711
770
  async function stop() {
712
771
  stopped = true;
713
772
  if (scheduler && typeof scheduler.stop === "function") scheduler.stop();
714
773
  scheduler = null;
774
+ if (ocspTimer && typeof ocspTimer.stop === "function") ocspTimer.stop();
775
+ ocspTimer = null;
715
776
  }
716
777
 
717
778
  function getContext(name) {
@@ -729,6 +790,11 @@ function create(opts) {
729
790
  key: ctx.key,
730
791
  expiresAt: ctx.expiresAt,
731
792
  fingerprintSha256: ctx.fingerprintSha256,
793
+ // The cached, validated OCSP response (DER Buffer) when ocsp.stapling
794
+ // is on and a response has been fetched; null otherwise. Staple it
795
+ // from the TLS server's 'OCSPRequest' handler: cb(null, ocspResponse).
796
+ ocspResponse: ctx.ocspResponse || null,
797
+ compliance: compliancePostures.slice(),
732
798
  };
733
799
  }
734
800
 
@@ -798,10 +864,6 @@ function create(opts) {
798
864
  function off(event, handler) { emitter.off(event, handler); return this; }
799
865
  function once(event, handler) { emitter.once(event, handler); return this; }
800
866
 
801
- // Suppress unused-warnings for ocsp + compliance until those branches
802
- // wire up in v0.11.23+ follow-up.
803
- void ocspStapling; void ocspRefreshMs; void compliance; void networkTls;
804
-
805
867
  return {
806
868
  start: start,
807
869
  stop: stop,
@@ -20,6 +20,7 @@ var NetworkTlsError = defineClass("NetworkTlsError", { alwaysPermanent: true });
20
20
  var observability = lazyRequire(function () { return require("./observability"); });
21
21
  var audit = lazyRequire(function () { return require("./audit"); });
22
22
  var networkDns = lazyRequire(function () { return require("./network-dns"); });
23
+ var httpClient = lazyRequire(function () { return require("./http-client"); });
23
24
  var asn1 = require("./asn1-der");
24
25
 
25
26
  // STATE.tlsKeyShares is initialized to the default PQC group list at
@@ -1194,9 +1195,10 @@ function evaluateOcspResponse(ocspDer, opts) {
1194
1195
  //
1195
1196
  // Constructs a DER-encoded OCSPRequest for a single (leafCertDer,
1196
1197
  // issuerCertDer) pair, optionally with an RFC 8954 nonce extension.
1197
- // Operators send the returned `requestDer` to the OCSP responder URL
1198
- // (e.g. via b.httpClient with `Content-Type: application/ocsp-request`)
1199
- // and pass `nonce` to `ocsp.evaluate(responseDer, { expectedNonce })`
1198
+ // `ocsp.fetch` composes this with `b.httpClient` to POST the request to
1199
+ // the cert's responder and return a validated response; operators who
1200
+ // need the raw request (custom transport, batched requests) call this
1201
+ // directly and pass `nonce` to `ocsp.evaluate(responseDer, { expectedNonce })`
1200
1202
  // to defend against replay attacks.
1201
1203
  //
1202
1204
  // Nonce DEFAULT ON — defense in depth. RFC 6960 §4.4.1 marks nonce
@@ -1291,8 +1293,10 @@ function buildOcspRequest(opts) {
1291
1293
  // framework that touches SHA-1" need a signal. Emit an audit row
1292
1294
  // on every OCSP request build so the algorithm choice is visible
1293
1295
  // in the chain.
1294
- var nameHash = nodeCrypto.createHash("sha1").update(iss.issuerNameDer).digest();
1295
- var keyHash = nodeCrypto.createHash("sha1").update(iss.issuerKey).digest();
1296
+ // lgtm[js/weak-cryptographic-algorithm] RFC 6960 §4.1.1 CertID lookup hash over the PUBLIC issuer name; a name/key lookup, not an integrity or secrecy operation. SHA-256 CertIDs are §4.3-optional and rejected by most responders.
1297
+ var nameHash = nodeCrypto.createHash("sha1").update(iss.issuerNameDer).digest(); // lgtm[js/weak-cryptographic-algorithm]
1298
+ // lgtm[js/weak-cryptographic-algorithm] — RFC 6960 §4.1.1 CertID lookup hash over the PUBLIC issuer key; a name/key lookup, not an integrity or secrecy operation.
1299
+ var keyHash = nodeCrypto.createHash("sha1").update(iss.issuerKey).digest(); // lgtm[js/weak-cryptographic-algorithm]
1296
1300
  setImmediate(function () {
1297
1301
  try {
1298
1302
  var auditMod = require("./audit"); // allow:inline-require — circular-load defense (audit imports network-tls)
@@ -1339,6 +1343,77 @@ function buildOcspRequest(opts) {
1339
1343
  return { requestDer: requestDer, nonce: nonceBytes };
1340
1344
  }
1341
1345
 
1346
+ // _ocspResponderUrl — pull the OCSP responder URL out of a cert's
1347
+ // Authority Information Access extension. node:crypto exposes it as a
1348
+ // multi-line string ("OCSP - URI:http://...\nCA Issuers - URI:...\n").
1349
+ function _ocspResponderUrl(x509) {
1350
+ var ia = x509 && x509.infoAccess;
1351
+ if (typeof ia !== "string") return null;
1352
+ var m = ia.match(/OCSP\s*-\s*URI:(\S+)/i);
1353
+ return m ? m[1].trim() : null;
1354
+ }
1355
+
1356
+ // fetch — POST a freshly-built OCSPRequest to the cert's responder and
1357
+ // return the validated, known-good response bytes. Composes buildRequest +
1358
+ // b.httpClient + evaluate, completing the server-side-stapling fetch path
1359
+ // (the response is what a TLS server staples via its 'OCSPRequest' handler).
1360
+ // The responder URL is taken from the leaf cert's AIA extension unless
1361
+ // opts.responderUrl overrides it. Throws TlsTrustError on any failure
1362
+ // (no responder, transport error, non-good certStatus, signature mismatch);
1363
+ // callers that staple should treat a throw as "no staple this cycle".
1364
+ async function fetchOcspResponse(opts) {
1365
+ opts = opts || {};
1366
+ if (typeof opts.leafPem !== "string" || typeof opts.issuerPem !== "string") {
1367
+ throw new TlsTrustError("tls/ocsp-bad-input",
1368
+ "ocsp.fetch: opts.leafPem and opts.issuerPem (PEM strings) are required");
1369
+ }
1370
+ var leafX, issuerX;
1371
+ try {
1372
+ leafX = new nodeCrypto.X509Certificate(opts.leafPem);
1373
+ issuerX = new nodeCrypto.X509Certificate(opts.issuerPem);
1374
+ } catch (e) {
1375
+ throw new TlsTrustError("tls/ocsp-bad-cert",
1376
+ "ocsp.fetch: could not parse leaf/issuer PEM: " + ((e && e.message) || String(e)));
1377
+ }
1378
+ var responderUrl = opts.responderUrl || _ocspResponderUrl(leafX);
1379
+ if (!responderUrl) {
1380
+ throw new TlsTrustError("tls/ocsp-no-responder",
1381
+ "ocsp.fetch: cert has no AIA OCSP responder URL; pass opts.responderUrl");
1382
+ }
1383
+ var built = buildOcspRequest({
1384
+ leafCertDer: leafX.raw, issuerCertDer: issuerX.raw,
1385
+ nonce: opts.nonce, nonceLen: opts.nonceLen,
1386
+ });
1387
+ var res;
1388
+ try {
1389
+ res = await httpClient().request({
1390
+ url: responderUrl,
1391
+ method: "POST",
1392
+ headers: { "content-type": "application/ocsp-request", "accept": "application/ocsp-response" },
1393
+ body: built.requestDer,
1394
+ responseMode: "buffer",
1395
+ timeoutMs: opts.timeoutMs || C.TIME.seconds(10),
1396
+ });
1397
+ } catch (e) {
1398
+ throw new TlsTrustError("tls/ocsp-fetch-failed",
1399
+ "ocsp.fetch: responder request to " + responderUrl + " failed: " + ((e && e.message) || String(e)));
1400
+ }
1401
+ if (res.status !== 200 || !Buffer.isBuffer(res.body) || res.body.length === 0) {
1402
+ throw new TlsTrustError("tls/ocsp-fetch-bad-status",
1403
+ "ocsp.fetch: responder returned status " + res.status + " with an empty/non-buffer body");
1404
+ }
1405
+ var evald = evaluateOcspResponse(res.body, {
1406
+ issuerPem: opts.issuerPem,
1407
+ serialHex: opts.serialHex || null,
1408
+ expectedNonce: opts.nonce === false ? null : built.nonce,
1409
+ });
1410
+ if (!evald.ok) {
1411
+ throw new TlsTrustError("tls/ocsp-not-good",
1412
+ "ocsp.fetch: response is not good: " + (evald.errors || []).join("; "));
1413
+ }
1414
+ return { ocspDer: res.body, evaluation: evald, responderUrl: responderUrl };
1415
+ }
1416
+
1342
1417
  var ocsp = Object.freeze({
1343
1418
  // Connect with OCSP requested. Returns { authorized, ocspBytes,
1344
1419
  // peerCert }. requireStapled: true makes empty / not-stapled responses
@@ -1379,6 +1454,7 @@ var ocsp = Object.freeze({
1379
1454
  },
1380
1455
  parseResponse: parseOcspResponse,
1381
1456
  evaluate: evaluateOcspResponse,
1457
+ fetch: fetchOcspResponse,
1382
1458
  // buildRequest — construct a DER-encoded OCSPRequest for a single
1383
1459
  // (leafCertDer, issuerCertDer) pair. RFC 8954 nonce extension is ON
1384
1460
  // by default (16 random bytes; opts.nonceLen overrides within RFC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.44",
3
+ "version": "0.13.45",
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:43cf0213-9944-42ad-9695-86dbc4acd548",
5
+ "serialNumber": "urn:uuid:4f2a624b-9183-4e8a-8807-eb827d32fb2b",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-30T00:54:18.743Z",
8
+ "timestamp": "2026-05-30T01:55:57.060Z",
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.44",
22
+ "bom-ref": "@blamejs/core@0.13.45",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.13.44",
25
+ "version": "0.13.45",
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.44",
29
+ "purl": "pkg:npm/%40blamejs/core@0.13.45",
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.44",
57
+ "ref": "@blamejs/core@0.13.45",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]