@blamejs/blamejs-shop 0.2.24 → 0.2.25
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 +2 -0
- package/lib/admin.js +42 -9
- package/lib/asset-manifest.json +1 -1
- package/lib/storefront.js +98 -21
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/a2a.js +11 -11
- package/lib/vendor/blamejs/lib/agent-snapshot.js +1 -1
- package/lib/vendor/blamejs/lib/ai-capability.js +20 -20
- package/lib/vendor/blamejs/lib/ai-dp.js +17 -17
- package/lib/vendor/blamejs/lib/ai-input.js +3 -3
- package/lib/vendor/blamejs/lib/ai-pref.js +9 -9
- package/lib/vendor/blamejs/lib/ai-quota.js +17 -17
- package/lib/vendor/blamejs/lib/arg-parser.js +38 -38
- package/lib/vendor/blamejs/lib/audit-sign.js +4 -4
- package/lib/vendor/blamejs/lib/auth/acr-vocabulary.js +4 -4
- package/lib/vendor/blamejs/lib/auth/auth-time-tracker.js +1 -1
- package/lib/vendor/blamejs/lib/auth/elevation-grant.js +10 -10
- package/lib/vendor/blamejs/lib/auth/step-up-policy.js +12 -12
- package/lib/vendor/blamejs/lib/auth/step-up.js +15 -15
- package/lib/vendor/blamejs/lib/boot-gates.js +6 -6
- package/lib/vendor/blamejs/lib/budr.js +6 -6
- package/lib/vendor/blamejs/lib/content-credentials.js +13 -13
- package/lib/vendor/blamejs/lib/dark-patterns.js +15 -15
- package/lib/vendor/blamejs/lib/ddl-change-control.js +37 -37
- package/lib/vendor/blamejs/lib/dr-runbook.js +7 -7
- package/lib/vendor/blamejs/lib/fapi2.js +9 -9
- package/lib/vendor/blamejs/lib/fdx.js +7 -7
- package/lib/vendor/blamejs/lib/graphql-federation.js +2 -2
- package/lib/vendor/blamejs/lib/iab-mspa.js +5 -5
- package/lib/vendor/blamejs/lib/iab-tcf.js +18 -18
- package/lib/vendor/blamejs/lib/mcp.js +13 -13
- package/lib/vendor/blamejs/lib/middleware/require-step-up.js +3 -3
- package/lib/vendor/blamejs/lib/sec-cyber.js +3 -3
- package/lib/vendor/blamejs/lib/sse.js +14 -14
- package/lib/vendor/blamejs/lib/tcpa-10dlc.js +5 -5
- package/lib/vendor/blamejs/lib/tenant-quota.js +18 -18
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.44.json +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-capability.test.js +15 -15
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-dp.test.js +16 -16
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-pref.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-quota.test.js +19 -19
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-ml-dsa-65.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/boot-gates.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/budr.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +52 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/dark-patterns.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/dr-runbook.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/fapi2.test.js +6 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/fdx.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-mspa.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-tcf.test.js +7 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +5 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/tcpa-10dlc.test.js +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.2.x
|
|
10
10
|
|
|
11
|
+
- v0.2.25 (2026-05-29) — **Harden session and admin cookies with __Host- / __Secure- name prefixes.** The session, login, and admin cookies now carry the browser-enforced `__Host-` / `__Secure-` name prefixes when served over HTTPS. These prefixes bind a cookie to a secure, same-origin context — a browser only accepts a `__Host-` cookie that was set with `Secure`, `Path=/`, and no `Domain` attribute — which closes off cookie-injection and session-fixation paths from a related origin or a non-secure context. The secure-versus-plain decision follows the forwarded request protocol, so a deployment behind a TLS-terminating proxy (the production setup) gets the hardened prefixes while local development and tests over HTTP keep the bare names and keep working. Returning visitors are not signed out by the upgrade: the cookie reader resolves both the prefixed and the legacy name during the transition. **Security:** *`__Host-` on the session and login cookies* — Over HTTPS the storefront session and login cookies are issued as `__Host-shop_sid` and `__Host-shop_auth` with `Secure`, `Path=/`, and no `Domain` — the attribute set a browser requires before it will store a `__Host-` cookie, so the session cookie can no longer be planted or overwritten by a related origin or a non-secure context. · *`__Secure-` on the admin cookie* — The admin session cookie is scoped to `Path=/admin`, so it takes the `__Secure-` prefix (which requires `Secure`) rather than `__Host-` (which mandates `Path=/`). The edge cache-skip check and the audience bucketing were updated to recognize the prefixed names so a signed-in response is never served from the shared edge cache.
|
|
12
|
+
|
|
11
13
|
- v0.2.24 (2026-05-29) — **Scope cart-line edits to your own cart, harden structured data, and redact auth errors.** A set of request-path hardening fixes. Cart line edit and remove are now scoped to your own session cart, so a cart-line id belonging to another shopper can no longer be used to change quantities on or delete their item — a mismatched id is a no-op, not a mutation. The product structured-data (JSON-LD) embedded in page heads now neutralizes every `</script` close-tag variant (with trailing space, slash, or newline), not just the exact `</script>`, closing a markup-injection path from admin-entered product fields. Account sign-in, registration, and passkey error responses now return a generic message plus a request id instead of the raw internal error string, with the full error logged server-side for the operator. And the worker DB/R2 bridge secret is checked at boot: a secret shorter than 32 characters logs a loud warning so a weak secret is visible in the boot log (the documented generator produces 43 characters, so a by-the-docs deploy stays quiet). **Security:** *Cart-line edits are scoped to your cart* — `POST /cart/lines/:id/update` and `/remove` now resolve the requester's session cart and scope the change to it (`WHERE id = ? AND cart_id = ?`), so a cart-line id from another shopper's cart can't be used to alter or delete their line — it resolves to a no-op rather than a cross-cart mutation. Legitimate edits to your own cart are unchanged. · *Structured-data close-tag escaping hardened* — JSON-LD embedded in page heads (edge and origin, byte-identical) now escapes every `</script` sequence the HTML parser treats as a tag-closer — including `</script `, `</script/`, and `</script` followed by a newline — not only the exact `</script>`. This removes a markup-injection vector from admin-entered product fields that flow into structured data. · *Account auth errors no longer leak internals* — The sign-in, registration, and passkey ceremony endpoints now return a generic message and a request id on an unexpected server error instead of the raw error string; the real error is logged server-side, correlated by that request id. Client-shape (400) and other expected responses are unchanged. · *Bridge-secret entropy warning at boot* — The worker DB/R2 bridge secret (`D1_BRIDGE_SECRET`) authorizes the database and object-storage bridge, so a weak value is high-impact. The container now logs a loud boot warning in production when it is shorter than 32 characters. The documented generator produces 43 characters; if you see the warning, regenerate the secret with the command it prints.
|
|
12
14
|
|
|
13
15
|
- v0.2.23 (2026-05-29) — **Rate limiting and cross-site request isolation on the request lifecycle.** Two request-lifecycle defenses are now composed into every storefront and admin request. A per-client-IP rate limit backs the whole site (a generous global budget that a normal browsing and checkout session never trips) with tighter budgets on the abusable POST/auth endpoints — sign-in, passkey enrolment, checkout, gift-card balance lookup, account registration, newsletter, review/question submit, and survey response — so credential and passkey spraying, gift-card brute-forcing, and checkout flooding are shut down. Separately, a fetch-metadata gate refuses cross-site state-changing requests using the browser's Sec-Fetch headers, adding CSRF defense-in-depth on top of the SameSite session cookies. The client IP is read from the edge-forwarded header (the container sits behind the Worker), so limits apply per real visitor; the payment-webhook and health-probe paths are exempt. No change for normal use. **Added:** *Per-client rate limiting* — A generous global token-bucket limit per client IP backs the whole site, with tight fixed-window budgets on the abusable POST/auth endpoints (login, passkey register/add, checkout, gift-card balance, register, newsletter, review/question, survey). Keyed on the real client IP forwarded from the edge; payment-webhook and health-probe paths are exempt. The single-instance container uses an in-memory backend. · *Cross-site request isolation (fetch-metadata)* — Cross-site state-changing requests (the CSRF vector) are refused using the browser's Sec-Fetch-Site/Mode/Dest headers — same-origin requests and direct navigations pass; payment webhooks are exempt. This restores defense-in-depth alongside the SameSite session cookies.
|
package/lib/admin.js
CHANGED
|
@@ -183,6 +183,17 @@ function _authOk(token, expected) {
|
|
|
183
183
|
// read/write) — scoped to /admin, SameSite=Strict, HttpOnly + Secure.
|
|
184
184
|
var ADMIN_COOKIE_NAME = "shop_admin";
|
|
185
185
|
|
|
186
|
+
// Cookie-prefix-hardened name. The admin session cookie is Path=/admin, so
|
|
187
|
+
// it CANNOT carry `__Host-` (which mandates Path=/); the correct prefix is
|
|
188
|
+
// `__Secure-`, which requires only the Secure attribute (RFC 6265bis
|
|
189
|
+
// §4.1.3.1). As with the storefront session cookies, the prefix moves in
|
|
190
|
+
// lockstep with Secure: a `__Secure-` cookie without Secure is invalid
|
|
191
|
+
// (silently dropped), and Secure cookies don't store over plain http. So
|
|
192
|
+
// https admin sessions carry `__Secure-shop_admin`; http dev/e2e sessions
|
|
193
|
+
// carry the bare `shop_admin`. The validity check resolves the prefixed
|
|
194
|
+
// name first and the bare name second.
|
|
195
|
+
var ADMIN_COOKIE_NAME_SECURE = "__Secure-" + ADMIN_COOKIE_NAME;
|
|
196
|
+
|
|
186
197
|
var _adminJarMemo = null;
|
|
187
198
|
function _adminJar() {
|
|
188
199
|
if (!_adminJarMemo) {
|
|
@@ -194,14 +205,33 @@ function _adminJar() {
|
|
|
194
205
|
return _adminJarMemo;
|
|
195
206
|
}
|
|
196
207
|
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
// Whether THIS request's PUBLIC connection is https — drives both the
|
|
209
|
+
// Secure attribute and the prefix choice for the admin session cookie.
|
|
210
|
+
// Same rule as the storefront: trust the Worker-set `x-forwarded-proto`
|
|
211
|
+
// (the container socket may be plain http behind the TLS-terminating
|
|
212
|
+
// Worker), and treat a direct dev/e2e connection (no forwarded header,
|
|
213
|
+
// non-encrypted socket) as http so the bare-named, non-Secure cookie is
|
|
214
|
+
// emitted and a real browser stores it.
|
|
215
|
+
function _secureForReq(req) {
|
|
216
|
+
return b.requestHelpers.requestProtocol(req, { trustProxy: true }) === "https";
|
|
217
|
+
}
|
|
218
|
+
function _adminCookieName(secure) { return secure ? ADMIN_COOKIE_NAME_SECURE : ADMIN_COOKIE_NAME; }
|
|
219
|
+
|
|
220
|
+
function _setAdminCookie(req, res) {
|
|
221
|
+
var secure = _secureForReq(req);
|
|
222
|
+
_adminJar().writeSealed(res, _adminCookieName(secure), JSON.stringify({
|
|
199
223
|
admin: true,
|
|
200
224
|
exp: Date.now() + b.constants.TIME.hours(12),
|
|
201
|
-
}), { expires: new Date(Date.now() + b.constants.TIME.hours(12)) });
|
|
225
|
+
}), { expires: new Date(Date.now() + b.constants.TIME.hours(12)), secure: secure });
|
|
202
226
|
}
|
|
203
|
-
function _clearAdminCookie(res) {
|
|
204
|
-
|
|
227
|
+
function _clearAdminCookie(req, res) {
|
|
228
|
+
// Expire-now must match the live request's protocol so the cleared
|
|
229
|
+
// cookie's attributes satisfy the `__Secure-` invariant: over https
|
|
230
|
+
// clear the prefixed name with Secure; over http clear the bare name. A
|
|
231
|
+
// request never carries both at once (the write side emits one per
|
|
232
|
+
// protocol), so clearing the protocol-matched name signs the operator out.
|
|
233
|
+
var secure = _secureForReq(req);
|
|
234
|
+
_adminJar().clear(res, _adminCookieName(secure), { secure: secure });
|
|
205
235
|
}
|
|
206
236
|
|
|
207
237
|
// One-time gift-card code reveal. The issue POST stashes the freshly-issued
|
|
@@ -224,7 +254,10 @@ function _takeGiftCardReveal(req, res, id) {
|
|
|
224
254
|
return (env && env.id === id && typeof env.code === "string") ? env.code : null;
|
|
225
255
|
}
|
|
226
256
|
function _adminCookieValid(req) {
|
|
227
|
-
|
|
257
|
+
// Resolve the prefixed name first and the bare name second so an admin
|
|
258
|
+
// session set in either environment (or mid-rollout) is recognised.
|
|
259
|
+
var raw = _adminJar().readSealed(req, ADMIN_COOKIE_NAME_SECURE);
|
|
260
|
+
if (raw === null) raw = _adminJar().readSealed(req, ADMIN_COOKIE_NAME);
|
|
228
261
|
if (raw === null) return false;
|
|
229
262
|
var env;
|
|
230
263
|
try { env = JSON.parse(raw); } catch (_e) { return false; }
|
|
@@ -4048,7 +4081,7 @@ function mount(router, deps) {
|
|
|
4048
4081
|
if (!_authOk(token, expectedToken)) {
|
|
4049
4082
|
return _sendHtml(res, 401, renderAdminLogin({ shop_name: deps.shop_name, error: true }));
|
|
4050
4083
|
}
|
|
4051
|
-
try { _setAdminCookie(res); }
|
|
4084
|
+
try { _setAdminCookie(req, res); }
|
|
4052
4085
|
catch (e) {
|
|
4053
4086
|
// Sealed cookies need an initialized vault — surface 503 rather
|
|
4054
4087
|
// than 500 so the operator knows to configure VAULT_PASSPHRASE.
|
|
@@ -4060,8 +4093,8 @@ function mount(router, deps) {
|
|
|
4060
4093
|
_redirect(res, (await _setupComplete()) ? "/admin" : "/admin/setup");
|
|
4061
4094
|
});
|
|
4062
4095
|
|
|
4063
|
-
router.post("/admin/logout", async function (
|
|
4064
|
-
_clearAdminCookie(res);
|
|
4096
|
+
router.post("/admin/logout", async function (req, res) {
|
|
4097
|
+
_clearAdminCookie(req, res);
|
|
4065
4098
|
_redirect(res, "/admin");
|
|
4066
4099
|
});
|
|
4067
4100
|
|
package/lib/asset-manifest.json
CHANGED
package/lib/storefront.js
CHANGED
|
@@ -4956,6 +4956,27 @@ var SESSION_COOKIE_NAME = "shop_sid";
|
|
|
4956
4956
|
// initiated logout-everywhere).
|
|
4957
4957
|
var AUTH_COOKIE_NAME = "shop_auth";
|
|
4958
4958
|
|
|
4959
|
+
// Cookie-prefix-hardened names for the two Path=/ session cookies. The
|
|
4960
|
+
// `__Host-` prefix is a browser-enforced integrity marker (RFC 6265bis
|
|
4961
|
+
// §4.1.3.2): a `__Host-`-named cookie is only stored when it was set
|
|
4962
|
+
// Secure, Path=/, and with NO Domain — pinning it to the exact host over
|
|
4963
|
+
// HTTPS, immune to subdomain / related-domain cookie injection + session
|
|
4964
|
+
// fixation. Both cookies are already Path=/ with no Domain, so `__Host-`
|
|
4965
|
+
// is the correct (strongest) prefix.
|
|
4966
|
+
//
|
|
4967
|
+
// The prefix moves in lockstep with the Secure attribute: a `__Host-`
|
|
4968
|
+
// cookie is INVALID (silently dropped by the browser) without Secure, and
|
|
4969
|
+
// Secure cookies are silently dropped over plain http. Local dev + the
|
|
4970
|
+
// e2e harness run over http, so the cookie there must carry the BARE name
|
|
4971
|
+
// (Secure off) or it would never store and every session would break. The
|
|
4972
|
+
// public protocol — https in production where the Cloudflare Worker
|
|
4973
|
+
// terminates TLS and forwards `x-forwarded-proto: https` to the container
|
|
4974
|
+
// — drives the choice via `_secureForReq` below. Read sites resolve the
|
|
4975
|
+
// prefixed name first and fall back to the bare name, so a request in
|
|
4976
|
+
// either environment (or mid-rollout) still resolves its session.
|
|
4977
|
+
var SESSION_COOKIE_NAME_SECURE = "__Host-" + SESSION_COOKIE_NAME;
|
|
4978
|
+
var AUTH_COOKIE_NAME_SECURE = "__Host-" + AUTH_COOKIE_NAME;
|
|
4979
|
+
|
|
4959
4980
|
// WebAuthn ceremony state cookie — short-lived envelope holding the
|
|
4960
4981
|
// random challenge + the ceremony-scoped metadata so register-finish /
|
|
4961
4982
|
// login-finish can verify the same challenge the browser was sent.
|
|
@@ -5013,6 +5034,41 @@ function _readCookie(req, name) {
|
|
|
5013
5034
|
return _cookieJar().read(req, name);
|
|
5014
5035
|
}
|
|
5015
5036
|
|
|
5037
|
+
// Whether THIS request's PUBLIC connection is https — the single source
|
|
5038
|
+
// of truth for both the Secure attribute and the cookie-prefix choice.
|
|
5039
|
+
// The container socket behind the Cloudflare Worker may be plain http even
|
|
5040
|
+
// when the visitor's connection is https, so the decision rides on the
|
|
5041
|
+
// forwarded protocol the Worker sets (`x-forwarded-proto`); `trustProxy`
|
|
5042
|
+
// opts that header in (the framework refuses the attacker-forgeable header
|
|
5043
|
+
// otherwise). Direct dev / e2e connections have no forwarded header and a
|
|
5044
|
+
// non-encrypted socket, so this returns false there and the bare-named,
|
|
5045
|
+
// non-Secure cookie is emitted (a real browser drops a Secure cookie over
|
|
5046
|
+
// http, so the bare name is what keeps dev/e2e sessions working).
|
|
5047
|
+
function _secureForReq(req) {
|
|
5048
|
+
return b.requestHelpers.requestProtocol(req, { trustProxy: true }) === "https";
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
// Resolve the on-wire name for one of the prefix-hardened cookies. Secure
|
|
5052
|
+
// requests (https) carry the `__Host-`-prefixed name; non-Secure requests
|
|
5053
|
+
// (http dev/e2e) carry the bare name so the browser actually stores it.
|
|
5054
|
+
function _sidCookieName(secure) { return secure ? SESSION_COOKIE_NAME_SECURE : SESSION_COOKIE_NAME; }
|
|
5055
|
+
function _authCookieName(secure) { return secure ? AUTH_COOKIE_NAME_SECURE : AUTH_COOKIE_NAME; }
|
|
5056
|
+
|
|
5057
|
+
// Read a prefix-hardened cookie value resolving the prefixed name first
|
|
5058
|
+
// and the bare name second, so a request resolves its session regardless
|
|
5059
|
+
// of which environment wrote the cookie (or a mid-rollout request that
|
|
5060
|
+
// still carries the old bare-named cookie). Returns the raw string or null.
|
|
5061
|
+
function _readPrefixedCookie(req, secureName, bareName) {
|
|
5062
|
+
var v = _cookieJar().read(req, secureName);
|
|
5063
|
+
if (v !== null && v !== undefined) return v;
|
|
5064
|
+
return _cookieJar().read(req, bareName);
|
|
5065
|
+
}
|
|
5066
|
+
function _readPrefixedSealed(req, secureName, bareName) {
|
|
5067
|
+
var v = _cookieJar().readSealed(req, secureName);
|
|
5068
|
+
if (v !== null) return v;
|
|
5069
|
+
return _cookieJar().readSealed(req, bareName);
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5016
5072
|
function _readSidCookie(req) {
|
|
5017
5073
|
// A cookie carrying anything but a well-shaped session id (a stale
|
|
5018
5074
|
// value from an old deploy, a tampered cookie, garbage) reads as "no
|
|
@@ -5020,27 +5076,45 @@ function _readSidCookie(req) {
|
|
|
5020
5076
|
// malformed id and would turn every page that renders the cart count
|
|
5021
5077
|
// into a 500. The cookie grants zero authority, so dropping a
|
|
5022
5078
|
// malformed one silently is safe.
|
|
5023
|
-
var v =
|
|
5079
|
+
var v = _readPrefixedCookie(req, SESSION_COOKIE_NAME_SECURE, SESSION_COOKIE_NAME);
|
|
5024
5080
|
return v && SID_SHAPE_RE.test(v) ? v : null;
|
|
5025
5081
|
}
|
|
5026
5082
|
|
|
5027
|
-
function _setSidCookie(res, sid) {
|
|
5083
|
+
function _setSidCookie(req, res, sid) {
|
|
5028
5084
|
var T = b.constants.TIME;
|
|
5029
|
-
|
|
5085
|
+
var secure = _secureForReq(req);
|
|
5086
|
+
// The jar defaults Path=/ and no Domain — exactly the `__Host-`
|
|
5087
|
+
// invariant. The explicit `secure` here moves the Secure attribute and
|
|
5088
|
+
// the prefixed name together: an https request gets `__Host-shop_sid`
|
|
5089
|
+
// with Secure; an http dev/e2e request gets `shop_sid` without Secure.
|
|
5090
|
+
_cookieJar().write(res, _sidCookieName(secure), sid, {
|
|
5091
|
+
expires: new Date(Date.now() + T.days(30)),
|
|
5092
|
+
secure: secure,
|
|
5093
|
+
});
|
|
5030
5094
|
}
|
|
5031
5095
|
|
|
5032
5096
|
// Auth + WebAuthn-challenge cookies carry a vault-sealed JSON envelope.
|
|
5033
5097
|
// writeSealed/readSealed handle the seal + the on-wire prefix; the
|
|
5034
5098
|
// caller works in plain objects.
|
|
5035
|
-
function _setAuthCookie(res, env) {
|
|
5099
|
+
function _setAuthCookie(req, res, env) {
|
|
5036
5100
|
var T = b.constants.TIME;
|
|
5037
|
-
|
|
5101
|
+
var secure = _secureForReq(req);
|
|
5102
|
+
_cookieJar().writeSealed(res, _authCookieName(secure), JSON.stringify(env), {
|
|
5103
|
+
expires: new Date(Date.now() + T.days(14)),
|
|
5104
|
+
secure: secure,
|
|
5105
|
+
});
|
|
5038
5106
|
}
|
|
5039
|
-
function _clearAuthCookie(res) {
|
|
5040
|
-
|
|
5107
|
+
function _clearAuthCookie(req, res) {
|
|
5108
|
+
// Expire-now must match the live request's protocol so the attributes
|
|
5109
|
+
// satisfy the prefix invariant: over https clear the `__Host-` name with
|
|
5110
|
+
// Secure+Path=/; over http clear the bare name. A request never carries
|
|
5111
|
+
// both names at once (the write side emits exactly one per protocol), so
|
|
5112
|
+
// clearing the protocol-matched name signs the visitor out.
|
|
5113
|
+
var secure = _secureForReq(req);
|
|
5114
|
+
_cookieJar().clear(res, _authCookieName(secure), { secure: secure });
|
|
5041
5115
|
}
|
|
5042
5116
|
function _readAuthEnv(req) {
|
|
5043
|
-
var raw =
|
|
5117
|
+
var raw = _readPrefixedSealed(req, AUTH_COOKIE_NAME_SECURE, AUTH_COOKIE_NAME);
|
|
5044
5118
|
if (raw === null) return null;
|
|
5045
5119
|
try { return JSON.parse(raw); } catch (_e) { return null; }
|
|
5046
5120
|
}
|
|
@@ -6244,7 +6318,10 @@ function mount(router, deps) {
|
|
|
6244
6318
|
router.use(function announcementMiddleware(req, _res, next) {
|
|
6245
6319
|
try {
|
|
6246
6320
|
_refreshAnnouncementCache(deps.announcementBar);
|
|
6247
|
-
|
|
6321
|
+
// Coarse logged-in/guest bucket from auth-cookie PRESENCE — resolve
|
|
6322
|
+
// the prefixed (`__Host-`, https/production) and bare (http/dev)
|
|
6323
|
+
// names so an https visitor isn't mis-bucketed as a guest.
|
|
6324
|
+
var viewerKind = _readPrefixedCookie(req, AUTH_COOKIE_NAME_SECURE, AUTH_COOKIE_NAME) ? "logged_in" : "guest";
|
|
6248
6325
|
var row = _resolveActiveAnnouncement(viewerKind);
|
|
6249
6326
|
var cur = _localeAls.getStore() || {};
|
|
6250
6327
|
_localeAls.enterWith(Object.assign({}, cur, { announcement: row }));
|
|
@@ -6549,7 +6626,7 @@ function mount(router, deps) {
|
|
|
6549
6626
|
var sid = _readSidCookie(req);
|
|
6550
6627
|
if (!sid) {
|
|
6551
6628
|
sid = b.uuid.v7();
|
|
6552
|
-
_setSidCookie(res, sid);
|
|
6629
|
+
_setSidCookie(req, res, sid);
|
|
6553
6630
|
}
|
|
6554
6631
|
var existing = await deps.cart.bySession(sid);
|
|
6555
6632
|
if (existing) return { sid: sid, cart: existing };
|
|
@@ -7098,7 +7175,7 @@ function mount(router, deps) {
|
|
|
7098
7175
|
if (sid) return sid;
|
|
7099
7176
|
if (!mint) return null;
|
|
7100
7177
|
sid = b.uuid.v7();
|
|
7101
|
-
_setSidCookie(res, sid);
|
|
7178
|
+
_setSidCookie(req, res, sid);
|
|
7102
7179
|
return sid;
|
|
7103
7180
|
}
|
|
7104
7181
|
|
|
@@ -8143,7 +8220,7 @@ function mount(router, deps) {
|
|
|
8143
8220
|
transports: transports,
|
|
8144
8221
|
});
|
|
8145
8222
|
_clearChallengeCookie(res);
|
|
8146
|
-
_setAuthCookie(res, {
|
|
8223
|
+
_setAuthCookie(req, res, {
|
|
8147
8224
|
customer_id: env.customer_id,
|
|
8148
8225
|
exp: Date.now() + b.constants.TIME.days(14),
|
|
8149
8226
|
});
|
|
@@ -8266,7 +8343,7 @@ function mount(router, deps) {
|
|
|
8266
8343
|
} catch (_e) { /* best-effort merge; sign-in itself succeeds */ }
|
|
8267
8344
|
}
|
|
8268
8345
|
_clearChallengeCookie(res);
|
|
8269
|
-
_setAuthCookie(res, {
|
|
8346
|
+
_setAuthCookie(req, res, {
|
|
8270
8347
|
customer_id: customer.id,
|
|
8271
8348
|
exp: Date.now() + b.constants.TIME.days(14),
|
|
8272
8349
|
});
|
|
@@ -8295,7 +8372,7 @@ function mount(router, deps) {
|
|
|
8295
8372
|
}
|
|
8296
8373
|
var customer = await deps.customers.get(auth.customer_id);
|
|
8297
8374
|
if (!customer) {
|
|
8298
|
-
_clearAuthCookie(res);
|
|
8375
|
+
_clearAuthCookie(req, res);
|
|
8299
8376
|
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
8300
8377
|
return res.end ? res.end() : res.send("");
|
|
8301
8378
|
}
|
|
@@ -8395,8 +8472,8 @@ function mount(router, deps) {
|
|
|
8395
8472
|
});
|
|
8396
8473
|
}
|
|
8397
8474
|
|
|
8398
|
-
router.post("/account/logout", function (
|
|
8399
|
-
_clearAuthCookie(res);
|
|
8475
|
+
router.post("/account/logout", function (req, res) {
|
|
8476
|
+
_clearAuthCookie(req, res);
|
|
8400
8477
|
res.status(303); res.setHeader && res.setHeader("location", "/");
|
|
8401
8478
|
return res.end ? res.end() : res.send("");
|
|
8402
8479
|
});
|
|
@@ -8639,7 +8716,7 @@ function mount(router, deps) {
|
|
|
8639
8716
|
var auth = _accountAuth(req, res); if (!auth) return;
|
|
8640
8717
|
var customer = await deps.customers.get(auth.customer_id);
|
|
8641
8718
|
if (!customer) {
|
|
8642
|
-
_clearAuthCookie(res);
|
|
8719
|
+
_clearAuthCookie(req, res);
|
|
8643
8720
|
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
8644
8721
|
return res.end ? res.end() : res.send("");
|
|
8645
8722
|
}
|
|
@@ -8650,7 +8727,7 @@ function mount(router, deps) {
|
|
|
8650
8727
|
var auth = _accountAuth(req, res); if (!auth) return;
|
|
8651
8728
|
var customer = await deps.customers.get(auth.customer_id);
|
|
8652
8729
|
if (!customer) {
|
|
8653
|
-
_clearAuthCookie(res);
|
|
8730
|
+
_clearAuthCookie(req, res);
|
|
8654
8731
|
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
8655
8732
|
return res.end ? res.end() : res.send("");
|
|
8656
8733
|
}
|
|
@@ -8760,7 +8837,7 @@ function mount(router, deps) {
|
|
|
8760
8837
|
} else {
|
|
8761
8838
|
try { _clearReferralCookie(res); } catch (_e) { /* best-effort */ }
|
|
8762
8839
|
}
|
|
8763
|
-
_setAuthCookie(res, { customer_id: rv.customer.id, exp: Date.now() + b.constants.TIME.days(14) });
|
|
8840
|
+
_setAuthCookie(req, res, { customer_id: rv.customer.id, exp: Date.now() + b.constants.TIME.days(14) });
|
|
8764
8841
|
res.status(303); res.setHeader && res.setHeader("location", "/account");
|
|
8765
8842
|
return res.end ? res.end() : res.send("");
|
|
8766
8843
|
});
|
|
@@ -8873,7 +8950,7 @@ function mount(router, deps) {
|
|
|
8873
8950
|
} else {
|
|
8874
8951
|
try { _clearReferralCookie(res); } catch (_e) { /* best-effort */ }
|
|
8875
8952
|
}
|
|
8876
|
-
_setAuthCookie(res, { customer_id: rv.customer.id, exp: Date.now() + b.constants.TIME.days(14) });
|
|
8953
|
+
_setAuthCookie(req, res, { customer_id: rv.customer.id, exp: Date.now() + b.constants.TIME.days(14) });
|
|
8877
8954
|
res.status(303); res.setHeader && res.setHeader("location", "/account");
|
|
8878
8955
|
return res.end ? res.end() : res.send("");
|
|
8879
8956
|
});
|
|
@@ -10546,7 +10623,7 @@ function mount(router, deps) {
|
|
|
10546
10623
|
var sid = _readSidCookie(req);
|
|
10547
10624
|
if (!sid) {
|
|
10548
10625
|
sid = b.uuid.v7();
|
|
10549
|
-
_setSidCookie(res, sid);
|
|
10626
|
+
_setSidCookie(req, res, sid);
|
|
10550
10627
|
}
|
|
10551
10628
|
return sid;
|
|
10552
10629
|
}
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.13.
|
|
7
|
-
"tag": "v0.13.
|
|
6
|
+
"version": "0.13.44",
|
|
7
|
+
"tag": "v0.13.44",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -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.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
|
+
|
|
11
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.
|
|
12
14
|
|
|
13
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.
|
|
@@ -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
|
|
|
@@ -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.
|
|
@@ -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
|
}
|