@blamejs/core 0.13.42 → 0.13.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/LTS-CALENDAR.md +6 -2
- package/lib/a2a.js +11 -11
- package/lib/agent-snapshot.js +1 -1
- package/lib/ai-capability.js +20 -20
- package/lib/ai-content-detect.js +4 -3
- package/lib/ai-dp.js +17 -17
- package/lib/ai-input.js +3 -3
- package/lib/ai-pref.js +9 -9
- package/lib/ai-quota.js +17 -17
- package/lib/archive-read.js +10 -7
- package/lib/arg-parser.js +38 -38
- package/lib/audit-sign.js +4 -4
- package/lib/auth/acr-vocabulary.js +4 -4
- package/lib/auth/auth-time-tracker.js +1 -1
- package/lib/auth/elevation-grant.js +10 -10
- package/lib/auth/step-up-policy.js +12 -12
- package/lib/auth/step-up.js +15 -15
- package/lib/boot-gates.js +6 -6
- package/lib/break-glass.js +1 -1
- package/lib/budr.js +6 -6
- package/lib/cms-codec.js +10 -9
- package/lib/content-credentials.js +13 -13
- package/lib/dark-patterns.js +15 -15
- package/lib/ddl-change-control.js +37 -37
- package/lib/dr-runbook.js +7 -7
- package/lib/fapi2.js +9 -9
- package/lib/fdx.js +7 -7
- package/lib/graphql-federation.js +2 -2
- package/lib/iab-mspa.js +5 -5
- package/lib/iab-tcf.js +18 -18
- package/lib/mail-crypto-smime.js +10 -6
- package/lib/mcp.js +13 -13
- package/lib/middleware/require-step-up.js +3 -3
- package/lib/mtls-ca.js +2 -2
- package/lib/safe-archive.js +8 -7
- package/lib/sec-cyber.js +3 -3
- package/lib/sse.js +14 -14
- package/lib/tcpa-10dlc.js +5 -5
- package/lib/tenant-quota.js +18 -18
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- 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
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
11
15
|
- 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.
|
|
12
16
|
|
|
13
17
|
- v0.13.41 (2026-05-29) — **Agent registry reads can be tenant-scoped; compliance-erasure docs clarify actor is an audit field.** The agent orchestrator's registry reads (list and lookup) gated only on the flat agent-registry:read scope, so any holder could enumerate every tenant's agents and resolve a handle to one — even though the event bus already scopes subscribe and delivery by tenant. The orchestrator now mirrors that: with the new tenantScope option enabled, list returns only the actor's own tenant's agents and lookup refuses a cross-tenant name, unless the actor holds the framework cross-tenant-admin scope. Off by default, so single-tenant deployments are unaffected. Separately, the subject-erasure docs now state explicitly that the recorded actor is an audit field, not authentication — the caller must be authorized upstream. **Added:** *Tenant-scoped agent registry reads (opts.tenantScope)* — `b.agent.orchestrator.create({ tenantScope: true })` now scopes `list` and `lookup` to the calling actor's tenant: `list` filters out agents in other tenants and `lookup` returns null for a cross-tenant name, unless the actor holds the cross-tenant-admin scope (`b.agent.tenant.CROSS_TENANT_ADMIN_SCOPE`). This closes a cross-tenant metadata-enumeration and handle-acquisition path — `agent-registry:read` alone no longer exposes other tenants' agents — and mirrors the tenant scoping the event bus enforces on subscribe and delivery. The option defaults off; existing single-tenant orchestrators behave exactly as before. The `tenantId` argument to `list` remains a convenience filter, distinct from this authorization boundary. **Changed:** *Subject-erasure docs clarify the actor is an audit field, not authentication* — `b.subject.erase` and `b.subject.eraseHard` gate the deletion on operator acknowledgements and the legal-hold registry, not on caller identity. Their documentation now states explicitly that the recorded `actor` is an audit-record field, not authentication — the caller MUST be authenticated and authorized by the route before invoking. No behavior change; this removes an implicit assumption that could otherwise be read as the primitive authorizing the call.
|
package/LTS-CALENDAR.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# LTS calendar
|
|
2
2
|
|
|
3
3
|
`@blamejs/core` ships on a published major cadence. Each major receives
|
|
4
|
-
**
|
|
4
|
+
**24 months of security-only patches** starting the day the next major is
|
|
5
5
|
published. Feature backports are not promised.
|
|
6
6
|
|
|
7
7
|
| Version | First release | Security patches through | Node minimum | KEM | Cipher | KDF | Sigs |
|
|
8
8
|
|---------------|---------------|-----------------------------|---------------|----------------------|-----------------------|----------|-----------------------|
|
|
9
9
|
| `v0.x` (pre-1.0) | 2026-04-25 | until v1.0 ships | 24 | ML-KEM-1024 + P-384 | XChaCha20-Poly1305 | SHAKE256 | SLH-DSA-SHAKE-256f |
|
|
10
|
-
| `v1.x` | TBD | first release +
|
|
10
|
+
| `v1.x` | TBD | first release + 24 months | current LTS | ML-KEM-1024 + P-384 | XChaCha20-Poly1305 | SHAKE256 | SLH-DSA-SHAKE-256f |
|
|
11
11
|
|
|
12
12
|
## What "security patches" means
|
|
13
13
|
|
|
@@ -27,3 +27,7 @@ The "Node minimum" column is the lowest Node major the framework supports for th
|
|
|
27
27
|
## Pre-1.0 caveat
|
|
28
28
|
|
|
29
29
|
`v0.x` has no LTS commitment. Every release may change something operators depend on; the algorithm posture is intentionally evolving. Read [CHANGELOG.md](CHANGELOG.md) before upgrading across more than a few patches at a time. The LTS calendar takes effect at v1.0.
|
|
30
|
+
|
|
31
|
+
## Experimental primitives are exempt
|
|
32
|
+
|
|
33
|
+
Primitives documented `@status experimental` (shown as "experimental" on each wiki page, and via the `experimental` segment in namespaces such as `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 the deprecation cycle that stable primitives get. This applies on the LTS line too. The exemption exists so the framework can ship primitives that track in-flight standards (draft RFCs, pre-IANA codepoints, newly published W3C surfaces) without freezing an unsettled format for a major's full support window. A primitive graduates to stable by dropping the `@status experimental` marker in a release whose notes call out the graduation.
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
229
|
+
throw errorClass.factory("a2a/bad-key",
|
|
230
230
|
"a2a.signCard: privateKeyPem required");
|
|
231
231
|
}
|
|
232
232
|
|
package/lib/agent-snapshot.js
CHANGED
|
@@ -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 "
|
|
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.
|
package/lib/ai-capability.js
CHANGED
|
@@ -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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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, "
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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-content-detect.js
CHANGED
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
* IPTC `digitalSourceType` PhotoMetadata reading is forward-watch —
|
|
31
31
|
* the framework ships no XMP / EXIF parser yet, so operators that
|
|
32
32
|
* want IPTC detection pre-parse with their tool of choice and pass
|
|
33
|
-
* the field via `opts.ipmd`. AB-853 names C2PA as "widely adopted"
|
|
34
|
-
* IPTC PhotoMetadata reader
|
|
35
|
-
* decision
|
|
33
|
+
* the field via `opts.ipmd`. AB-853 names C2PA as "widely adopted".
|
|
34
|
+
* A built-in IPTC PhotoMetadata reader is deferred pending a vendoring
|
|
35
|
+
* decision for an XMP/EXIF parser; the `opts.ipmd` escape hatch covers
|
|
36
|
+
* the gap until then.
|
|
36
37
|
*
|
|
37
38
|
* @card
|
|
38
39
|
* Inbound provenance detector — composes C2PA verify + CAC implicit-label parser + operator-supplied IPTC field, returns a normalized report for AB-853 / EU AI Act Art. 50 / CAC disclosure UIs.
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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, "
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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, "
|
|
356
|
+
"ai.quota.consume: tenantId", AiQuotaError, "ai-quota/bad-tenant");
|
|
357
357
|
validateOpts.requireNonEmptyString(model,
|
|
358
|
-
"ai.quota.consume: model", AiQuotaError, "
|
|
358
|
+
"ai.quota.consume: model", AiQuotaError, "ai-quota/bad-model");
|
|
359
359
|
if (typeof amount !== "number" || !isFinite(amount) || amount < 0) {
|
|
360
|
-
throw new AiQuotaError("
|
|
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("
|
|
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("
|
|
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, "
|
|
435
|
+
"ai.quota.check: tenantId", AiQuotaError, "ai-quota/bad-tenant");
|
|
436
436
|
validateOpts.requireNonEmptyString(model,
|
|
437
|
-
"ai.quota.check: model", AiQuotaError, "
|
|
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, "
|
|
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, "
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
}
|