@blamejs/core 0.13.43 → 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,10 @@ 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
+
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).
14
+
11
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.
12
16
 
13
17
  - v0.13.42 (2026-05-29) — **S/MIME trust-chain validation binds the leaf to the key that verified the signature.** When b.mail.crypto.smime.verify is given trust anchors, it validates the supplied certificate chain — but it picked the chain leaf unconditionally (the first cert) and never tied it to signerPublicKey, the key that actually verified the signature. A SignedData blob could therefore carry a validly-chained certificate for one identity while the signature was verified under an unrelated key, and the chain-validated result would imply a cert↔signer binding the code never made. Chain validation now selects the leaf as the certificate whose public key matches signerPublicKey, and refuses (signer-not-in-chain) when no certificate in the chain carries that key — so a chain-validated signature is bound to the cert it claims. **Security:** *Trust-chain leaf is bound to the verified signer key* — `smime.verify({ trustAnchorCertsPem })` validated the supplied chain starting from `chain[0]` without checking that the leaf's public key was the one that verified the signature (the operator-supplied `signerPublicKey`). A crafted `SignedData` could pair a validly-chained certificate for identity A with a signature verified under an unrelated key, and the chain-valid result would assert a binding that didn't hold. The chain walk now selects the leaf as the certificate whose public key equals `signerPublicKey` (matched against the certificate's SPKI, raw key or full-encoding form), and throws `mail-crypto/smime/signer-not-in-chain` when no certificate in `SignedData.certificates` carries that key. A certificate whose key cannot be extracted is treated as a non-match, so validation fails closed rather than trusting an unverifiable binding.
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/a2a.js CHANGED
@@ -48,52 +48,52 @@ var SEMVER_RE = /^[0-9]+\.[0-9]+(?:\.[0-9]+)?(?:-[A-Za-z0-9.-]+)?$/;
48
48
 
49
49
  function _validateCardShape(card, errorClass) {
50
50
  if (!card || typeof card !== "object" || Array.isArray(card)) {
51
- throw errorClass.factory("BAD_CARD",
51
+ throw errorClass.factory("a2a/bad-card",
52
52
  "a2a: card must be an object");
53
53
  }
54
54
  for (var i = 0; i < REQUIRED_CARD_FIELDS.length; i += 1) {
55
55
  var f = REQUIRED_CARD_FIELDS[i];
56
56
  if (typeof card[f] === "undefined" || card[f] === null) {
57
- throw errorClass.factory("MISSING_FIELD",
57
+ throw errorClass.factory("a2a/missing-field",
58
58
  "a2a: card." + f + " is required");
59
59
  }
60
60
  }
61
61
  if (typeof card.issuer !== "string" || card.issuer.length > ID_MAX || !ID_RE.test(card.issuer)) {
62
- throw errorClass.factory("BAD_FIELD",
62
+ throw errorClass.factory("a2a/bad-field",
63
63
  "a2a: card.issuer shape (must match " + ID_RE + ")");
64
64
  }
65
65
  if (typeof card.agentId !== "string" || card.agentId.length > ID_MAX || !ID_RE.test(card.agentId)) {
66
- throw errorClass.factory("BAD_FIELD",
66
+ throw errorClass.factory("a2a/bad-field",
67
67
  "a2a: card.agentId shape");
68
68
  }
69
69
  if (typeof card.version !== "string" || card.version.length > SEMVER_MAX || !SEMVER_RE.test(card.version)) {
70
- throw errorClass.factory("BAD_FIELD",
70
+ throw errorClass.factory("a2a/bad-field",
71
71
  "a2a: card.version must be semver");
72
72
  }
73
73
  if (!Array.isArray(card.capabilities)) {
74
- throw errorClass.factory("BAD_FIELD",
74
+ throw errorClass.factory("a2a/bad-field",
75
75
  "a2a: card.capabilities must be an array");
76
76
  }
77
77
  for (var c = 0; c < card.capabilities.length; c += 1) {
78
78
  var cap = card.capabilities[c];
79
79
  if (typeof cap !== "string" || cap.length === 0 || cap.length > CAP_NAME_MAX) {
80
- throw errorClass.factory("BAD_FIELD",
80
+ throw errorClass.factory("a2a/bad-field",
81
81
  "a2a: card.capabilities[" + c + "] must be 1-128 char string");
82
82
  }
83
83
  }
84
84
  if (card.endpoints !== undefined) {
85
85
  if (!Array.isArray(card.endpoints)) {
86
- throw errorClass.factory("BAD_FIELD",
86
+ throw errorClass.factory("a2a/bad-field",
87
87
  "a2a: card.endpoints must be an array");
88
88
  }
89
89
  for (var e = 0; e < card.endpoints.length; e += 1) {
90
90
  var ep = card.endpoints[e];
91
91
  if (!ep || typeof ep !== "object" || typeof ep.url !== "string") {
92
- throw errorClass.factory("BAD_FIELD",
92
+ throw errorClass.factory("a2a/bad-field",
93
93
  "a2a: card.endpoints[" + e + "] must have a string url");
94
94
  }
95
95
  if (!/^https:\/\//.test(ep.url) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])/.test(ep.url)) {
96
- throw errorClass.factory("INSECURE_ENDPOINT",
96
+ throw errorClass.factory("a2a/insecure-endpoint",
97
97
  "a2a: card.endpoints[" + e + "].url must be HTTPS (or localhost)");
98
98
  }
99
99
  }
@@ -226,7 +226,7 @@ function signCard(card, privateKeyPem, opts) {
226
226
  _validateCardShape(card, errorClass);
227
227
 
228
228
  if (typeof privateKeyPem !== "string" || privateKeyPem.length === 0) {
229
- throw errorClass.factory("BAD_KEY",
229
+ throw errorClass.factory("a2a/bad-key",
230
230
  "a2a.signCard: privateKeyPem required");
231
231
  }
232
232
 
@@ -170,7 +170,7 @@ function _resolveSigner(ctx) {
170
170
  var as;
171
171
  try { as = auditSign(); } catch (_e) { as = null; }
172
172
  if (as && typeof as.sign === "function" && typeof as.verify === "function") {
173
- // b.auditSign.sign throws "auditSign/not-initialized" when called
173
+ // b.auditSign.sign throws "audit-sign/not-initialized" when called
174
174
  // pre-init — surface that here as the snapshot's signer-not-wired
175
175
  // error so the caller's message is consistent regardless of which
176
176
  // dependency landed unwired.
@@ -108,31 +108,31 @@ function _isStringArray(a) {
108
108
  // surfaces at config time rather than as a silent mis-route.
109
109
  function _normalizeDescriptor(modelId, d) {
110
110
  if (!d || typeof d !== "object" || Array.isArray(d)) {
111
- throw new AiCapabilityError("aiCapability/bad-descriptor",
111
+ throw new AiCapabilityError("ai-capability/bad-descriptor",
112
112
  "ai.capability: descriptor for '" + modelId + "' must be a plain object");
113
113
  }
114
114
  validateOpts(d, DESCRIPTOR_KEYS, "ai.capability descriptor['" + modelId + "']");
115
115
 
116
116
  if (!_isPositiveInt(d.maxContextTokens)) {
117
- throw new AiCapabilityError("aiCapability/bad-descriptor",
117
+ throw new AiCapabilityError("ai-capability/bad-descriptor",
118
118
  "ai.capability: '" + modelId + "'.maxContextTokens must be a positive integer");
119
119
  }
120
120
  var maxOut = (d.maxOutputTokens == null) ? d.maxContextTokens : d.maxOutputTokens;
121
121
  if (!_isPositiveInt(maxOut)) {
122
- throw new AiCapabilityError("aiCapability/bad-descriptor",
122
+ throw new AiCapabilityError("ai-capability/bad-descriptor",
123
123
  "ai.capability: '" + modelId + "'.maxOutputTokens must be a positive integer");
124
124
  }
125
125
 
126
126
  var modIn = (d.modalitiesIn == null) ? ["text"] : d.modalitiesIn;
127
127
  var modOut = (d.modalitiesOut == null) ? ["text"] : d.modalitiesOut;
128
128
  if (!_isStringArray(modIn) || !_isStringArray(modOut)) {
129
- throw new AiCapabilityError("aiCapability/bad-descriptor",
129
+ throw new AiCapabilityError("ai-capability/bad-descriptor",
130
130
  "ai.capability: '" + modelId + "'.modalitiesIn / modalitiesOut must be arrays of non-empty strings");
131
131
  }
132
132
 
133
133
  var tier = (d.reasoningTier == null) ? "standard" : d.reasoningTier;
134
134
  if (REASONING_TIERS.indexOf(tier) === -1) {
135
- throw new AiCapabilityError("aiCapability/bad-descriptor",
135
+ throw new AiCapabilityError("ai-capability/bad-descriptor",
136
136
  "ai.capability: '" + modelId + "'.reasoningTier must be one of " + REASONING_TIERS.join(" / "));
137
137
  }
138
138
 
@@ -140,7 +140,7 @@ function _normalizeDescriptor(modelId, d) {
140
140
  var costIn = (d.costPer1kInputTokens == null) ? 0 : d.costPer1kInputTokens;
141
141
  var costOut = (d.costPer1kOutputTokens == null) ? 0 : d.costPer1kOutputTokens;
142
142
  if (!_isNonNegFinite(cachingMax) || !_isNonNegFinite(costIn) || !_isNonNegFinite(costOut)) {
143
- throw new AiCapabilityError("aiCapability/bad-descriptor",
143
+ throw new AiCapabilityError("ai-capability/bad-descriptor",
144
144
  "ai.capability: '" + modelId + "'.promptCachingMaxTokens / costPer1kInputTokens / " +
145
145
  "costPer1kOutputTokens must be non-negative finite numbers");
146
146
  }
@@ -224,12 +224,12 @@ function create(opts) {
224
224
  validateOpts(opts, ["models", "audit"], "ai.capability.create");
225
225
 
226
226
  if (!opts.models || typeof opts.models !== "object" || Array.isArray(opts.models)) {
227
- throw new AiCapabilityError("aiCapability/bad-models",
227
+ throw new AiCapabilityError("ai-capability/bad-models",
228
228
  "ai.capability.create: models must be a plain object { modelId: descriptor }");
229
229
  }
230
230
  var ids = Object.keys(opts.models);
231
231
  if (ids.length === 0) {
232
- throw new AiCapabilityError("aiCapability/bad-models",
232
+ throw new AiCapabilityError("ai-capability/bad-models",
233
233
  "ai.capability.create: models must declare at least one model");
234
234
  }
235
235
 
@@ -249,7 +249,7 @@ function create(opts) {
249
249
  function describe(modelId) {
250
250
  var d = registry.get(modelId);
251
251
  if (!d) {
252
- throw new AiCapabilityError("aiCapability/unknown-model",
252
+ throw new AiCapabilityError("ai-capability/unknown-model",
253
253
  "ai.capability.describe: unknown model '" + modelId + "'");
254
254
  }
255
255
  return d;
@@ -261,7 +261,7 @@ function create(opts) {
261
261
 
262
262
  function register(modelId, descriptor) {
263
263
  validateOpts.requireNonEmptyString(modelId,
264
- "ai.capability.register: modelId", AiCapabilityError, "aiCapability/bad-model");
264
+ "ai.capability.register: modelId", AiCapabilityError, "ai-capability/bad-model");
265
265
  registry.set(modelId, _normalizeDescriptor(modelId, descriptor));
266
266
  return registry.get(modelId);
267
267
  }
@@ -323,21 +323,21 @@ function create(opts) {
323
323
  function _validateRequirements(requirements) {
324
324
  if (requirements == null) return {};
325
325
  if (typeof requirements !== "object" || Array.isArray(requirements)) {
326
- throw new AiCapabilityError("aiCapability/bad-requirements",
326
+ throw new AiCapabilityError("ai-capability/bad-requirements",
327
327
  "ai.capability: requirements must be a plain object");
328
328
  }
329
329
  validateOpts(requirements, REQUIREMENT_KEYS, "ai.capability requirements");
330
330
  if (requirements.minReasoningTier != null &&
331
331
  REASONING_TIERS.indexOf(requirements.minReasoningTier) === -1) {
332
- throw new AiCapabilityError("aiCapability/bad-requirements",
332
+ throw new AiCapabilityError("ai-capability/bad-requirements",
333
333
  "ai.capability: minReasoningTier must be one of " + REASONING_TIERS.join(" / "));
334
334
  }
335
335
  if (requirements.modalitiesIn != null && !_isStringArray(requirements.modalitiesIn)) {
336
- throw new AiCapabilityError("aiCapability/bad-requirements",
336
+ throw new AiCapabilityError("ai-capability/bad-requirements",
337
337
  "ai.capability: requirements.modalitiesIn must be an array of non-empty strings");
338
338
  }
339
339
  if (requirements.modalitiesOut != null && !_isStringArray(requirements.modalitiesOut)) {
340
- throw new AiCapabilityError("aiCapability/bad-requirements",
340
+ throw new AiCapabilityError("ai-capability/bad-requirements",
341
341
  "ai.capability: requirements.modalitiesOut must be an array of non-empty strings");
342
342
  }
343
343
  // Numeric minimums are compared with `<` against the descriptor; a
@@ -349,7 +349,7 @@ function create(opts) {
349
349
  for (var ni = 0; ni < numericMins.length; ni++) {
350
350
  var nk = numericMins[ni];
351
351
  if (requirements[nk] != null && !_isNonNegFinite(requirements[nk])) {
352
- throw new AiCapabilityError("aiCapability/bad-requirements",
352
+ throw new AiCapabilityError("ai-capability/bad-requirements",
353
353
  "ai.capability: requirements." + nk + " must be a non-negative finite number");
354
354
  }
355
355
  }
@@ -360,7 +360,7 @@ function create(opts) {
360
360
  for (var bi = 0; bi < booleanReqs.length; bi++) {
361
361
  var bk = booleanReqs[bi];
362
362
  if (requirements[bk] != null && typeof requirements[bk] !== "boolean") {
363
- throw new AiCapabilityError("aiCapability/bad-requirements",
363
+ throw new AiCapabilityError("ai-capability/bad-requirements",
364
364
  "ai.capability: requirements." + bk + " must be a boolean");
365
365
  }
366
366
  }
@@ -392,7 +392,7 @@ function create(opts) {
392
392
  var costBasis = null;
393
393
  if (routeOpts.costBasis != null) {
394
394
  if (typeof routeOpts.costBasis !== "object" || Array.isArray(routeOpts.costBasis)) {
395
- throw new AiCapabilityError("aiCapability/bad-requirements",
395
+ throw new AiCapabilityError("ai-capability/bad-requirements",
396
396
  "ai.capability.route: costBasis must be a plain object { inputTokens, outputTokens }");
397
397
  }
398
398
  validateOpts(routeOpts.costBasis, ["inputTokens", "outputTokens"],
@@ -405,7 +405,7 @@ function create(opts) {
405
405
  for (var ci = 0; ci < cbFields.length; ci++) {
406
406
  var ck = cbFields[ci];
407
407
  if (routeOpts.costBasis[ck] != null && !_isNonNegFinite(routeOpts.costBasis[ck])) {
408
- throw new AiCapabilityError("aiCapability/bad-requirements",
408
+ throw new AiCapabilityError("ai-capability/bad-requirements",
409
409
  "ai.capability.route: costBasis." + ck + " must be a non-negative finite number");
410
410
  }
411
411
  }
@@ -446,7 +446,7 @@ function create(opts) {
446
446
  if (routeOpts.fallback != null) {
447
447
  var fb = registry.get(routeOpts.fallback);
448
448
  if (!fb) {
449
- throw new AiCapabilityError("aiCapability/unknown-model",
449
+ throw new AiCapabilityError("ai-capability/unknown-model",
450
450
  "ai.capability.route: fallback '" + routeOpts.fallback + "' is not a registered model");
451
451
  }
452
452
  _emitAudit("ai/capability-fallback", "allowed", {
@@ -461,7 +461,7 @@ function create(opts) {
461
461
  }
462
462
 
463
463
  _emitAudit("ai/capability-no-candidate", "denied", { requirements: requirements });
464
- throw new AiCapabilityError("aiCapability/no-candidate",
464
+ throw new AiCapabilityError("ai-capability/no-candidate",
465
465
  "ai.capability.route: no registered model satisfies the requirements " +
466
466
  "and no fallback was supplied");
467
467
  }
package/lib/ai-dp.js CHANGED
@@ -102,7 +102,7 @@ function _frGt(a, b) { return a.num * b.den > b.num * a.den; } // a > b
102
102
  // Uniform BigInt in [0, m) via rejection sampling on CSPRNG bytes —
103
103
  // no modulo bias.
104
104
  function _uniformBelow(m) {
105
- if (m <= 0n) throw new AiDpError("aiDp/internal", "ai.dp: _uniformBelow needs m > 0");
105
+ if (m <= 0n) throw new AiDpError("ai-dp/internal", "ai.dp: _uniformBelow needs m > 0");
106
106
  if (m === 1n) return 0n;
107
107
  var bits = m.toString(2).length;
108
108
  var bytes = Math.ceil(bits / 8); // allow:raw-byte-literal — bits-per-byte divisor, not a size
@@ -296,23 +296,23 @@ function mechanism(opts) {
296
296
  validateOpts(opts, ["type", "sensitivity", "epsilon", "delta", "bound"], "ai.dp.mechanism");
297
297
 
298
298
  if (MECHANISMS.indexOf(opts.type) === -1) {
299
- throw new AiDpError("aiDp/bad-mechanism",
299
+ throw new AiDpError("ai-dp/bad-mechanism",
300
300
  "ai.dp.mechanism: type must be one of " + MECHANISMS.join(" / ") +
301
301
  " (exponential / sparse-vector are deferred — their float-safe constructions " +
302
302
  "re-open on demand)");
303
303
  }
304
304
  if (typeof opts.sensitivity !== "number" || !isFinite(opts.sensitivity) || opts.sensitivity <= 0) {
305
- throw new AiDpError("aiDp/bad-sensitivity",
305
+ throw new AiDpError("ai-dp/bad-sensitivity",
306
306
  "ai.dp.mechanism: sensitivity must be a positive finite number");
307
307
  }
308
308
  if (typeof opts.epsilon !== "number" || !isFinite(opts.epsilon) || opts.epsilon <= 0) {
309
- throw new AiDpError("aiDp/bad-epsilon",
309
+ throw new AiDpError("ai-dp/bad-epsilon",
310
310
  "ai.dp.mechanism: epsilon must be a positive finite number");
311
311
  }
312
312
 
313
313
  if (opts.type === "laplace") {
314
314
  if (typeof opts.bound !== "number" || !isFinite(opts.bound) || opts.bound <= 0) {
315
- throw new AiDpError("aiDp/bad-bound",
315
+ throw new AiDpError("ai-dp/bad-bound",
316
316
  "ai.dp.mechanism: laplace requires bound > 0 (the snapping clamp; the " +
317
317
  "privacy guarantee depends on it)");
318
318
  }
@@ -325,11 +325,11 @@ function mechanism(opts) {
325
325
 
326
326
  // gaussian
327
327
  if (typeof opts.delta !== "number" || !isFinite(opts.delta) || opts.delta <= 0 || opts.delta >= 1) {
328
- throw new AiDpError("aiDp/bad-delta",
328
+ throw new AiDpError("ai-dp/bad-delta",
329
329
  "ai.dp.mechanism: gaussian requires 0 < delta < 1");
330
330
  }
331
331
  if (opts.epsilon > 1) {
332
- throw new AiDpError("aiDp/epsilon-too-large",
332
+ throw new AiDpError("ai-dp/epsilon-too-large",
333
333
  "ai.dp.mechanism: the classic Gaussian calibration is proven for epsilon <= 1; " +
334
334
  "split into multiple releases under an rdp budget, or the analytic Gaussian " +
335
335
  "mechanism (Balle-Wang 2018) re-opens this path on demand");
@@ -346,7 +346,7 @@ function mechanism(opts) {
346
346
  // budget wraps this).
347
347
  function _applyMechanism(m, value) {
348
348
  if (typeof value !== "number" || !isFinite(value)) {
349
- throw new AiDpError("aiDp/bad-value", "ai.dp: value must be a finite number");
349
+ throw new AiDpError("ai-dp/bad-value", "ai.dp: value must be a finite number");
350
350
  }
351
351
  if (m.type === "laplace") {
352
352
  return _snappingLaplace(value, m.scale, m.bound);
@@ -403,22 +403,22 @@ function budget(opts) {
403
403
  validateOpts(opts, ["scope", "epsilon", "delta", "accounting", "audit"], "ai.dp.budget");
404
404
 
405
405
  validateOpts.requireNonEmptyString(opts.scope,
406
- "ai.dp.budget: scope", AiDpError, "aiDp/bad-scope");
406
+ "ai.dp.budget: scope", AiDpError, "ai-dp/bad-scope");
407
407
  if (typeof opts.epsilon !== "number" || !isFinite(opts.epsilon) || opts.epsilon <= 0) {
408
- throw new AiDpError("aiDp/bad-epsilon", "ai.dp.budget: epsilon must be a positive finite number");
408
+ throw new AiDpError("ai-dp/bad-epsilon", "ai.dp.budget: epsilon must be a positive finite number");
409
409
  }
410
410
  var totalEpsilon = opts.epsilon;
411
411
  var totalDelta = (opts.delta == null) ? 0 : opts.delta;
412
412
  if (typeof totalDelta !== "number" || !isFinite(totalDelta) || totalDelta < 0 || totalDelta >= 1) {
413
- throw new AiDpError("aiDp/bad-delta", "ai.dp.budget: delta must be in [0, 1)");
413
+ throw new AiDpError("ai-dp/bad-delta", "ai.dp.budget: delta must be in [0, 1)");
414
414
  }
415
415
  var accounting = (opts.accounting == null) ? "basic" : opts.accounting;
416
416
  if (ACCOUNTINGS.indexOf(accounting) === -1) {
417
- throw new AiDpError("aiDp/bad-accounting",
417
+ throw new AiDpError("ai-dp/bad-accounting",
418
418
  "ai.dp.budget: accounting must be one of " + ACCOUNTINGS.join(" / "));
419
419
  }
420
420
  if (accounting === "rdp" && totalDelta <= 0) {
421
- throw new AiDpError("aiDp/bad-accounting",
421
+ throw new AiDpError("ai-dp/bad-accounting",
422
422
  "ai.dp.budget: rdp accounting requires delta > 0 (the RDP→(ε,δ) conversion is " +
423
423
  "undefined at delta = 0; use basic accounting for pure-ε budgets)");
424
424
  }
@@ -457,11 +457,11 @@ function budget(opts) {
457
457
 
458
458
  function consume(m, value) {
459
459
  if (!m || typeof m !== "object" || MECHANISMS.indexOf(m.type) === -1) {
460
- throw new AiDpError("aiDp/bad-mechanism",
460
+ throw new AiDpError("ai-dp/bad-mechanism",
461
461
  "ai.dp.budget.consume: first argument must be a b.ai.dp.mechanism");
462
462
  }
463
463
  if (m.type === "gaussian" && totalDelta <= 0) {
464
- throw new AiDpError("aiDp/bad-delta",
464
+ throw new AiDpError("ai-dp/bad-delta",
465
465
  "ai.dp.budget.consume: a gaussian mechanism needs a scope delta > 0");
466
466
  }
467
467
 
@@ -475,7 +475,7 @@ function budget(opts) {
475
475
  requestEpsilon: m.epsilon, requestDelta: m.delta,
476
476
  spentEpsilon: spentEpsilon, totalEpsilon: totalEpsilon,
477
477
  });
478
- throw new AiDpError("aiDp/budget-exhausted",
478
+ throw new AiDpError("ai-dp/budget-exhausted",
479
479
  "ai.dp.budget.consume: scope '" + scope + "' would spend ε=" +
480
480
  (spentEpsilon + m.epsilon) + "/" + totalEpsilon + ", δ=" +
481
481
  (spentDelta + m.delta) + "/" + totalDelta + "; refused");
@@ -489,7 +489,7 @@ function budget(opts) {
489
489
  scope: scope, accounting: accounting, mechanism: m.type,
490
490
  projectedEpsilon: trialEps, totalEpsilon: totalEpsilon,
491
491
  });
492
- throw new AiDpError("aiDp/budget-exhausted",
492
+ throw new AiDpError("ai-dp/budget-exhausted",
493
493
  "ai.dp.budget.consume: scope '" + scope + "' would reach ε=" +
494
494
  trialEps.toFixed(4) + " of " + totalEpsilon + " at δ=" + totalDelta + "; refused");
495
495
  }
package/lib/ai-input.js CHANGED
@@ -104,12 +104,12 @@ function classify(input, opts) {
104
104
  var auditOn = opts.audit !== false;
105
105
 
106
106
  if (typeof input !== "string") {
107
- throw errorClass.factory("BAD_INPUT",
107
+ throw errorClass.factory("ai-input/bad-input",
108
108
  "aiInput.classify: input must be a string");
109
109
  }
110
110
  var byteLen = Buffer.byteLength(input, "utf8");
111
111
  if (byteLen > maxBytes) {
112
- throw errorClass.factory("INPUT_TOO_LARGE",
112
+ throw errorClass.factory("ai-input/input-too-large",
113
113
  "aiInput.classify: input exceeds " + maxBytes + " bytes (got " + byteLen + ")");
114
114
  }
115
115
 
@@ -187,7 +187,7 @@ function refuseIfMalicious(input, opts) {
187
187
  var errorClass = opts.errorClass || AiInputError;
188
188
  var result = classify(input, opts);
189
189
  if (result.verdict === "malicious") {
190
- throw errorClass.factory("MALICIOUS_INPUT",
190
+ throw errorClass.factory("ai-input/malicious-input",
191
191
  "aiInput: input flagged as malicious (signals: " +
192
192
  result.signals.map(function (s) { return s.id; }).join(", ") + ")");
193
193
  }
package/lib/ai-pref.js CHANGED
@@ -38,25 +38,25 @@ var SNIPPET_VALUES = ["allow", "deny"];
38
38
 
39
39
  function _validate(opts) {
40
40
  if (!opts || typeof opts !== "object") {
41
- throw AiPrefError.factory("BAD_OPTS",
41
+ throw AiPrefError.factory("ai-pref/bad-opts",
42
42
  "aiPref: opts required");
43
43
  }
44
44
  var train = opts.train || "deny";
45
45
  var infer = opts.infer || "allow";
46
46
  var snippet = opts.snippet || "allow";
47
47
  if (TRAIN_VALUES.indexOf(train) === -1) {
48
- throw AiPrefError.factory("BAD_TRAIN", "aiPref: train must be one of " + TRAIN_VALUES.join(", "));
48
+ throw AiPrefError.factory("ai-pref/bad-train", "aiPref: train must be one of " + TRAIN_VALUES.join(", "));
49
49
  }
50
50
  if (INFER_VALUES.indexOf(infer) === -1) {
51
- throw AiPrefError.factory("BAD_INFER", "aiPref: infer must be one of " + INFER_VALUES.join(", "));
51
+ throw AiPrefError.factory("ai-pref/bad-infer", "aiPref: infer must be one of " + INFER_VALUES.join(", "));
52
52
  }
53
53
  if (SNIPPET_VALUES.indexOf(snippet) === -1) {
54
- throw AiPrefError.factory("BAD_SNIPPET", "aiPref: snippet must be one of " + SNIPPET_VALUES.join(", "));
54
+ throw AiPrefError.factory("ai-pref/bad-snippet", "aiPref: snippet must be one of " + SNIPPET_VALUES.join(", "));
55
55
  }
56
56
  if ((train === "paid" || infer === "paid") &&
57
57
  (!opts.price || typeof opts.price.amountUsd !== "number" ||
58
58
  !isFinite(opts.price.amountUsd) || opts.price.amountUsd <= 0)) {
59
- throw AiPrefError.factory("BAD_PRICE",
59
+ throw AiPrefError.factory("ai-pref/bad-price",
60
60
  "aiPref: price.amountUsd (positive finite number) required when train or infer is 'paid'");
61
61
  }
62
62
  return { train: train, infer: infer, snippet: snippet, price: opts.price || null };
@@ -140,10 +140,10 @@ function serializeHeader(opts) {
140
140
  */
141
141
  function parseHeader(value) {
142
142
  if (typeof value !== "string" || value.length === 0) {
143
- throw AiPrefError.factory("BAD_HEADER", "aiPref.parseHeader: value required");
143
+ throw AiPrefError.factory("ai-pref/bad-header", "aiPref.parseHeader: value required");
144
144
  }
145
145
  if (value.length > 1024) { // allow:raw-byte-literal — header value cap, not bytes
146
- throw AiPrefError.factory("HEADER_TOO_LARGE",
146
+ throw AiPrefError.factory("ai-pref/header-too-large",
147
147
  "aiPref.parseHeader: value exceeds 1024 chars");
148
148
  }
149
149
  structuredFields.refuseControlBytes(value, {
@@ -206,7 +206,7 @@ function robotsBlock(opts) {
206
206
  var v = _validate(opts);
207
207
  var ua = opts.userAgent || "*";
208
208
  if (typeof ua !== "string" || ua.length === 0 || ua.length > 256) { // allow:raw-byte-literal — UA-string cap, not bytes
209
- throw AiPrefError.factory("BAD_USER_AGENT",
209
+ throw AiPrefError.factory("ai-pref/bad-user-agent",
210
210
  "aiPref.robotsBlock: userAgent must be 1-256 char string (or omit for *)");
211
211
  }
212
212
  return "User-agent: " + ua + "\n" +
@@ -298,7 +298,7 @@ function middleware(opts) {
298
298
  */
299
299
  function refusePaidCrawl(req, res, opts) {
300
300
  if (!opts || !opts.price || typeof opts.price.amountUsd !== "number") {
301
- throw AiPrefError.factory("BAD_PRICE",
301
+ throw AiPrefError.factory("ai-pref/bad-price",
302
302
  "aiPref.refusePaidCrawl: opts.price.amountUsd required");
303
303
  }
304
304
  var body = JSON.stringify({
package/lib/ai-quota.js CHANGED
@@ -257,20 +257,20 @@ function create(opts) {
257
257
 
258
258
  var dimension = opts.dimension;
259
259
  if (DIMENSIONS.indexOf(dimension) === -1) {
260
- throw new AiQuotaError("aiQuota/bad-dimension",
260
+ throw new AiQuotaError("ai-quota/bad-dimension",
261
261
  "ai.quota.create: dimension must be one of " + DIMENSIONS.join(" / ") +
262
262
  " (got " + JSON.stringify(dimension) + ")");
263
263
  }
264
264
 
265
265
  var period = opts.period;
266
266
  if (PERIODS.indexOf(period) === -1) {
267
- throw new AiQuotaError("aiQuota/bad-period",
267
+ throw new AiQuotaError("ai-quota/bad-period",
268
268
  "ai.quota.create: period must be one of " + PERIODS.join(" / ") +
269
269
  " (got " + JSON.stringify(period) + ")");
270
270
  }
271
271
 
272
272
  if (typeof opts.limit !== "number" || !isFinite(opts.limit) || opts.limit <= 0) {
273
- throw new AiQuotaError("aiQuota/bad-limit",
273
+ throw new AiQuotaError("ai-quota/bad-limit",
274
274
  "ai.quota.create: limit must be a positive finite number");
275
275
  }
276
276
  var defaultLimit = opts.limit;
@@ -281,7 +281,7 @@ function create(opts) {
281
281
 
282
282
  var enforcement = (opts.enforcement == null) ? "hard" : opts.enforcement;
283
283
  if (ENFORCEMENTS.indexOf(enforcement) === -1) {
284
- throw new AiQuotaError("aiQuota/bad-enforcement",
284
+ throw new AiQuotaError("ai-quota/bad-enforcement",
285
285
  "ai.quota.create: enforcement must be one of " + ENFORCEMENTS.join(" / ") +
286
286
  " (got " + JSON.stringify(enforcement) + ")");
287
287
  }
@@ -353,11 +353,11 @@ function create(opts) {
353
353
 
354
354
  function consume(tenantId, model, amount, consumeOpts) {
355
355
  validateOpts.requireNonEmptyString(tenantId,
356
- "ai.quota.consume: tenantId", AiQuotaError, "aiQuota/bad-tenant");
356
+ "ai.quota.consume: tenantId", AiQuotaError, "ai-quota/bad-tenant");
357
357
  validateOpts.requireNonEmptyString(model,
358
- "ai.quota.consume: model", AiQuotaError, "aiQuota/bad-model");
358
+ "ai.quota.consume: model", AiQuotaError, "ai-quota/bad-model");
359
359
  if (typeof amount !== "number" || !isFinite(amount) || amount < 0) {
360
- throw new AiQuotaError("aiQuota/bad-amount",
360
+ throw new AiQuotaError("ai-quota/bad-amount",
361
361
  "ai.quota.consume: amount must be a non-negative finite number");
362
362
  }
363
363
  consumeOpts = consumeOpts || {};
@@ -366,7 +366,7 @@ function create(opts) {
366
366
  // enforcer; still validated against the allowlist.
367
367
  var mode = (consumeOpts.enforcement == null) ? enforcement : consumeOpts.enforcement;
368
368
  if (ENFORCEMENTS.indexOf(mode) === -1) {
369
- throw new AiQuotaError("aiQuota/bad-enforcement",
369
+ throw new AiQuotaError("ai-quota/bad-enforcement",
370
370
  "ai.quota.consume: enforcement override must be one of " + ENFORCEMENTS.join(" / "));
371
371
  }
372
372
 
@@ -401,7 +401,7 @@ function create(opts) {
401
401
  enforcement: mode, nodeId: _nodeId(),
402
402
  });
403
403
  _emitMetric("ai.quota.exceeded", 1);
404
- throw new AiQuotaError("aiQuota/exceeded",
404
+ throw new AiQuotaError("ai-quota/exceeded",
405
405
  "ai.quota.consume: tenant '" + tenantId + "' model '" + model +
406
406
  "' is at " + rv.used + " of " + limit + " " + dimension +
407
407
  " this " + period + "; consuming " + amount + " would exceed the budget — call refused");
@@ -432,9 +432,9 @@ function create(opts) {
432
432
 
433
433
  function check(tenantId, model) {
434
434
  validateOpts.requireNonEmptyString(tenantId,
435
- "ai.quota.check: tenantId", AiQuotaError, "aiQuota/bad-tenant");
435
+ "ai.quota.check: tenantId", AiQuotaError, "ai-quota/bad-tenant");
436
436
  validateOpts.requireNonEmptyString(model,
437
- "ai.quota.check: model", AiQuotaError, "aiQuota/bad-model");
437
+ "ai.quota.check: model", AiQuotaError, "ai-quota/bad-model");
438
438
  var now = Date.now();
439
439
  var windowStart = _windowStartFor(period, now);
440
440
  var resetsAt = _resetsAtFor(period, windowStart);
@@ -454,10 +454,10 @@ function create(opts) {
454
454
  return;
455
455
  }
456
456
  validateOpts.requireNonEmptyString(tenantId,
457
- "ai.quota.reset: tenantId", AiQuotaError, "aiQuota/bad-tenant");
457
+ "ai.quota.reset: tenantId", AiQuotaError, "ai-quota/bad-tenant");
458
458
  if (model !== undefined) {
459
459
  validateOpts.requireNonEmptyString(model,
460
- "ai.quota.reset: model", AiQuotaError, "aiQuota/bad-model");
460
+ "ai.quota.reset: model", AiQuotaError, "ai-quota/bad-model");
461
461
  store.reset(_keyFor(tenantId, model, windowStart));
462
462
  return;
463
463
  }
@@ -470,7 +470,7 @@ function create(opts) {
470
470
  for (var i = 0; i < keys.length; i++) store.reset(keys[i]);
471
471
  return;
472
472
  }
473
- throw new AiQuotaError("aiQuota/reset-unsupported",
473
+ throw new AiQuotaError("ai-quota/reset-unsupported",
474
474
  "ai.quota.reset: tenant-wide reset with an external store requires " +
475
475
  "an explicit model argument (per-key) or a store-side prefix delete");
476
476
  }
@@ -491,14 +491,14 @@ function create(opts) {
491
491
  function _validateLimitMap(map, label) {
492
492
  if (map == null) return {};
493
493
  if (typeof map !== "object" || Array.isArray(map)) {
494
- throw new AiQuotaError("aiQuota/bad-override",
494
+ throw new AiQuotaError("ai-quota/bad-override",
495
495
  "ai.quota.create: " + label + " must be a plain object { key: limit }");
496
496
  }
497
497
  var keys = Object.keys(map);
498
498
  for (var i = 0; i < keys.length; i++) {
499
499
  var v = map[keys[i]];
500
500
  if (typeof v !== "number" || !isFinite(v) || v <= 0) {
501
- throw new AiQuotaError("aiQuota/bad-override",
501
+ throw new AiQuotaError("ai-quota/bad-override",
502
502
  "ai.quota.create: " + label + "['" + keys[i] +
503
503
  "'] must be a positive finite number");
504
504
  }
@@ -512,7 +512,7 @@ function _validateStore(store) {
512
512
  typeof store.add !== "function" ||
513
513
  typeof store.get !== "function" ||
514
514
  typeof store.reset !== "function") {
515
- throw new AiQuotaError("aiQuota/bad-store",
515
+ throw new AiQuotaError("ai-quota/bad-store",
516
516
  "ai.quota.create: store must expose reserve / add / get / reset functions");
517
517
  }
518
518
  }