@blamejs/blamejs-shop 0.3.15 → 0.3.16
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 +136 -3
- package/lib/asset-manifest.json +1 -1
- package/lib/catalog.js +61 -2
- 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-tasks.js +6 -6
- package/lib/vendor/blamejs/lib/ai-input.js +1 -1
- package/lib/vendor/blamejs/lib/auth/acr-vocabulary.js +1 -1
- package/lib/vendor/blamejs/lib/auth/ciba.js +3 -3
- package/lib/vendor/blamejs/lib/auth/oauth.js +1 -1
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +1 -1
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +1 -1
- package/lib/vendor/blamejs/lib/breach-deadline.js +3 -3
- package/lib/vendor/blamejs/lib/calendar.js +2 -2
- package/lib/vendor/blamejs/lib/content-credentials.js +3 -3
- package/lib/vendor/blamejs/lib/ddl-change-control.js +2 -2
- package/lib/vendor/blamejs/lib/did.js +2 -2
- package/lib/vendor/blamejs/lib/dsr.js +4 -4
- package/lib/vendor/blamejs/lib/external-db.js +1 -1
- package/lib/vendor/blamejs/lib/guard-cidr.js +1 -1
- package/lib/vendor/blamejs/lib/guard-image.js +1 -1
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -1
- package/lib/vendor/blamejs/lib/guard-time.js +1 -1
- package/lib/vendor/blamejs/lib/guard-xml.js +1 -1
- package/lib/vendor/blamejs/lib/http-client-cache.js +1 -1
- package/lib/vendor/blamejs/lib/iab-tcf.js +4 -4
- package/lib/vendor/blamejs/lib/json-schema.js +1 -1
- package/lib/vendor/blamejs/lib/jtd.js +1 -1
- package/lib/vendor/blamejs/lib/mail-auth.js +1 -1
- package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-mx.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-rate-limit.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-submission.js +1 -1
- package/lib/vendor/blamejs/lib/mcp.js +7 -7
- package/lib/vendor/blamejs/lib/mdoc.js +1 -1
- package/lib/vendor/blamejs/lib/metrics.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/compose-pipeline.js +1 -1
- package/lib/vendor/blamejs/lib/network-dnssec.js +2 -2
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +1 -1
- package/lib/vendor/blamejs/lib/network-tls.js +1 -1
- package/lib/vendor/blamejs/lib/network-tsig.js +3 -3
- package/lib/vendor/blamejs/lib/rfc3339.js +2 -2
- package/lib/vendor/blamejs/lib/safe-decompress.js +1 -1
- package/lib/vendor/blamejs/lib/standard-webhooks.js +3 -3
- package/lib/vendor/blamejs/lib/stream-throttle.js +2 -2
- package/lib/vendor/blamejs/lib/structured-fields.js +1 -1
- package/lib/vendor/blamejs/lib/vault/seal-pem-file.js +1 -1
- package/lib/vendor/blamejs/lib/web-push-vapid.js +1 -1
- package/lib/vendor/blamejs/lib/webhook.js +1 -1
- package/lib/vendor/blamejs/lib/websocket.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.8.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +221 -35
- 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.3.x
|
|
10
10
|
|
|
11
|
+
- v0.3.16 (2026-05-30) — **Uploaded SVGs are sanitized, and product images can be reordered with a chosen primary.** Two things for product media in the admin console. First, when an SVG image is uploaded it now passes through a strict sanitizer: an SVG that carries script or other active content is repaired, or refused outright when it can't be made safe, so a stored image can't become a script-execution vector. Second, a product's images can now be reordered and any image promoted to the primary (hero) slot directly from the console — previously the order was fixed at attach time and the first-attached image was always the hero. The primary image is the one shown first on the product page. **Added:** *Reorder product images and set the primary from the console* — Each image on a product now has a "Make primary" control that moves it to the hero slot shown first on the product page, and a reorder control sets the full order. Both are no-JavaScript form actions. The product page renders images in this order, so changing it here changes what shoppers see first. **Changed:** *Vendored runtime updated to v0.14.8* — The vendored blamejs runtime is updated to v0.14.8, an internal source-comment and lint-hygiene release with no API, wire-format, or behavior changes. No operator action is required. **Security:** *Uploaded SVG images are sanitized or refused* — An uploaded SVG is now checked by a strict sanitizer before it is stored: active content (scripts, event handlers, external references) is stripped, and an SVG that cannot be made safe is rejected with a clear error and never written. This closes the path where a crafted SVG, once stored and served, could execute script. Raster image uploads (PNG, JPEG, WebP, GIF) are unaffected.
|
|
12
|
+
|
|
11
13
|
- v0.3.15 (2026-05-30) — **Product image galleries are interactive — click a thumbnail to view that image full size.** A product page's image gallery now lets a shopper switch between the product's images: clicking a thumbnail swaps the main image, with no JavaScript required. Previously the thumbnail strip was decorative — the additional images were unreachable, a single-image product showed empty placeholder slots, and only the first four images were shown. Now the main image stacks the product's images and the thumbnails are real, keyboard-accessible controls that select which one is shown; a product with one image shows just that image (no empty slots), and the four-image cap is raised to twelve so far more images are reachable. The picker is pure HTML and CSS, so it works without scripting and degrades cleanly. **Fixed:** *The product image gallery actually switches images now* — Thumbnails on the product page are now interactive: selecting one shows that image as the main image, using a no-JavaScript radio/label control. The empty placeholder slots a single-image product used to show are gone, the four-image cap is raised to twelve so far more images are reachable, and the strip is keyboard-navigable with accessible labels. The edge and container renderers emit identical markup.
|
|
12
14
|
|
|
13
15
|
- v0.3.14 (2026-05-30) — **Drop the inert Document-Policy header that only produced browser warnings.** Container-rendered pages were sending a Document-Policy response header whose feature tokens (document-write, unsized-media, oversized-images) are no longer recognized by current browsers — so it enforced nothing while making the browser log a warning for each token on every page. The header is now omitted; every other security header (Content-Security-Policy, Permissions-Policy, HSTS, X-Frame-Options, the COOP/CORP set, and the rest) is unchanged. The edge and container paths now agree — neither sends Document-Policy — closing a header-parity gap. **Fixed:** *No more Document-Policy console warnings* — The Document-Policy header is no longer emitted on container-rendered pages. Its tokens were rejected by current browsers (the policy applied nothing and logged a warning per token on every page), so the header is dropped rather than shipped inert; the full set of other security headers is retained unchanged. Edge and container responses are now consistent.
|
package/lib/admin.js
CHANGED
|
@@ -1200,6 +1200,43 @@ function mount(router, deps) {
|
|
|
1200
1200
|
// caller renders the problem.
|
|
1201
1201
|
async function _storeAndAttach(buf, contentType, body) {
|
|
1202
1202
|
var declared = String(contentType).split(";")[0].trim().toLowerCase();
|
|
1203
|
+
// SVG is text — magic-byte sniffing can't classify it, so the type
|
|
1204
|
+
// gate trusts the declared `image/svg+xml`. An SVG served same-origin
|
|
1205
|
+
// from the media bucket is a stored-XSS vector on direct navigation
|
|
1206
|
+
// (embedded <script>, on* handlers, javascript: URLs, foreignObject
|
|
1207
|
+
// namespace escapes). Run the bytes through the framework's SVG guard
|
|
1208
|
+
// before they reach the bucket: `refuse` (an unrepairable threat —
|
|
1209
|
+
// <script>, DOCTYPE/XXE, SVGZ) is an operational failure the caller
|
|
1210
|
+
// renders as a clean 4xx with no row written; `sanitize` stores the
|
|
1211
|
+
// repaired bytes; `serve` stores the (clean) original. Composes the
|
|
1212
|
+
// vendored b.guardSvg — never a hand-rolled SVG allowlist.
|
|
1213
|
+
if (declared === "image/svg+xml") {
|
|
1214
|
+
var verdict;
|
|
1215
|
+
try {
|
|
1216
|
+
verdict = await b.guardSvg.gate({ profile: "strict" }).check({ bytes: buf });
|
|
1217
|
+
} catch (_e) {
|
|
1218
|
+
return { status: 422, code: "svg-unsafe",
|
|
1219
|
+
detail: "the uploaded SVG could not be validated as safe to serve" };
|
|
1220
|
+
}
|
|
1221
|
+
if (!verdict || verdict.action === "refuse") {
|
|
1222
|
+
return { status: 422, code: "svg-unsafe",
|
|
1223
|
+
detail: "the uploaded SVG carries active content that can't be served safely (scripts, external entities, or a compressed payload)" };
|
|
1224
|
+
}
|
|
1225
|
+
if (verdict.action === "sanitize") {
|
|
1226
|
+
// The guard hands back the repaired bytes as a Buffer (the gate
|
|
1227
|
+
// contract) — or, defensively, a string. Anything else is a guard
|
|
1228
|
+
// malfunction; fail closed rather than store unvalidated bytes.
|
|
1229
|
+
var clean = verdict.sanitized;
|
|
1230
|
+
if (Buffer.isBuffer(clean)) {
|
|
1231
|
+
buf = clean;
|
|
1232
|
+
} else if (typeof clean === "string") {
|
|
1233
|
+
buf = Buffer.from(clean, "utf8");
|
|
1234
|
+
} else {
|
|
1235
|
+
return { status: 422, code: "svg-unsafe",
|
|
1236
|
+
detail: "the uploaded SVG could not be sanitized into safe-to-serve bytes" };
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1203
1240
|
var ext = _extFromContentType(declared);
|
|
1204
1241
|
var id = b.uuid.v7();
|
|
1205
1242
|
var key = "media/" + id + (ext ? "." + ext : "");
|
|
@@ -1432,6 +1469,76 @@ function mount(router, deps) {
|
|
|
1432
1469
|
},
|
|
1433
1470
|
));
|
|
1434
1471
|
|
|
1472
|
+
// Reorder a product's media. The PDP gallery renders the media in list
|
|
1473
|
+
// order with the first row as the hero, so this is "rearrange the gallery
|
|
1474
|
+
// + choose which image leads." Browser form posts the full ordered id set
|
|
1475
|
+
// as a comma-joined `ordered_media_ids` string (same convention as the
|
|
1476
|
+
// collections-members reorder); the JSON API accepts an array. The
|
|
1477
|
+
// primitive rewrites `position` to 0..N-1 and refuses a partial / foreign
|
|
1478
|
+
// set (TypeError → clean 400 / ?err=1). DB-only — no R2 bridge needed.
|
|
1479
|
+
router.post("/admin/products/:id/media/reorder", _pageOrApi(false,
|
|
1480
|
+
W("media.reorder", async function (req, res) {
|
|
1481
|
+
var body = req.body || {};
|
|
1482
|
+
var ids = Array.isArray(body.ordered_media_ids) ? body.ordered_media_ids : null;
|
|
1483
|
+
if (!ids || ids.length === 0) return _problem(res, 400, "bad-request", "ordered_media_ids array required");
|
|
1484
|
+
try { await catalog.media.reorder(req.params.id, ids); }
|
|
1485
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
1486
|
+
_json(res, 200, { ok: true });
|
|
1487
|
+
return { id: req.params.id };
|
|
1488
|
+
}),
|
|
1489
|
+
async function (req, res) {
|
|
1490
|
+
var id = req.params.id;
|
|
1491
|
+
var enc = encodeURIComponent(id);
|
|
1492
|
+
var body = req.body || {};
|
|
1493
|
+
// The reorder form posts a single comma-joined id field; also accept
|
|
1494
|
+
// repeated ordered_media_id rows for parity with the collections form.
|
|
1495
|
+
var ids;
|
|
1496
|
+
if (body.ordered_media_ids != null && typeof body.ordered_media_ids === "string") {
|
|
1497
|
+
ids = body.ordered_media_ids.split(",").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
1498
|
+
} else {
|
|
1499
|
+
var raw = body.ordered_media_id;
|
|
1500
|
+
var rows = raw == null ? [] : (Array.isArray(raw) ? raw : [raw]);
|
|
1501
|
+
ids = rows.map(function (s) { return String(s).trim(); }).filter(Boolean);
|
|
1502
|
+
}
|
|
1503
|
+
if (!ids.length) return _redirect(res, "/admin/products/" + enc + "?err=1");
|
|
1504
|
+
try { await catalog.media.reorder(id, ids); }
|
|
1505
|
+
catch (e) {
|
|
1506
|
+
_safeNotice(e, "media.reorder");
|
|
1507
|
+
return _redirect(res, "/admin/products/" + enc + "?err=1");
|
|
1508
|
+
}
|
|
1509
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".media.reorder", outcome: "success", metadata: { id: id } });
|
|
1510
|
+
_redirect(res, "/admin/products/" + enc + "?saved=1");
|
|
1511
|
+
},
|
|
1512
|
+
));
|
|
1513
|
+
|
|
1514
|
+
// Promote one media row to the hero slot (position 0). No body — the media
|
|
1515
|
+
// id is in the path, the product id only used to PRG back to the detail.
|
|
1516
|
+
// A malformed id throws TypeError (→ 400 / ?err=1); an unknown or
|
|
1517
|
+
// variant-only row returns false (→ 404 / ?err=1). DB-only.
|
|
1518
|
+
router.post("/admin/products/:id/media/:mid/primary", _pageOrApi(false,
|
|
1519
|
+
W("media.set_primary", async function (req, res) {
|
|
1520
|
+
var ok;
|
|
1521
|
+
try { ok = await catalog.media.setPrimary(req.params.mid); }
|
|
1522
|
+
catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
|
|
1523
|
+
if (!ok) return _problem(res, 404, "media-not-found");
|
|
1524
|
+
_json(res, 200, { ok: true });
|
|
1525
|
+
return { id: req.params.mid };
|
|
1526
|
+
}),
|
|
1527
|
+
async function (req, res) {
|
|
1528
|
+
var id = req.params.id;
|
|
1529
|
+
var enc = encodeURIComponent(id);
|
|
1530
|
+
var ok = false;
|
|
1531
|
+
try { ok = await catalog.media.setPrimary(req.params.mid); }
|
|
1532
|
+
catch (e) {
|
|
1533
|
+
_safeNotice(e, "media.set_primary");
|
|
1534
|
+
return _redirect(res, "/admin/products/" + enc + "?err=1");
|
|
1535
|
+
}
|
|
1536
|
+
if (!ok) return _redirect(res, "/admin/products/" + enc + "?err=1");
|
|
1537
|
+
b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".media.set_primary", outcome: "success", metadata: { id: req.params.mid } });
|
|
1538
|
+
_redirect(res, "/admin/products/" + enc + "?saved=1");
|
|
1539
|
+
},
|
|
1540
|
+
));
|
|
1541
|
+
|
|
1435
1542
|
// ---- bulk catalog import --------------------------------------------
|
|
1436
1543
|
|
|
1437
1544
|
// POST /admin/catalog/import — Content-Type: text/csv body. The CSV
|
|
@@ -9658,24 +9765,50 @@ function renderAdminProduct(opts) {
|
|
|
9658
9765
|
var variantsSection = "<section class=\"mt\"><h3 class=\"fs-105\">Variants</h3>" + variantsBody + addVariant + "</section>";
|
|
9659
9766
|
|
|
9660
9767
|
// ---- media -----------------------------------------------------------
|
|
9661
|
-
var mediaCards = mediaRows.map(function (m) {
|
|
9768
|
+
var mediaCards = mediaRows.map(function (m, idx) {
|
|
9662
9769
|
var mid = _htmlEscape(m.id);
|
|
9663
9770
|
var url = assetPrefix + m.r2_key;
|
|
9664
9771
|
var isImage = /^image\//.test(m.content_type || "");
|
|
9665
9772
|
var thumb = isImage
|
|
9666
9773
|
? "<img class=\"media-thumb\" src=\"" + _htmlEscape(url) + "\" alt=\"" + _htmlEscape(m.alt_text || "") + "\" loading=\"lazy\">"
|
|
9667
9774
|
: "<span class=\"media-thumb media-thumb--file\">" + _htmlEscape((m.content_type || "file").split("/")[0]) + "</span>";
|
|
9775
|
+
// The first row is the hero shown on the product page — mark it with a
|
|
9776
|
+
// pill; every other row offers a "Make primary" form that promotes it to
|
|
9777
|
+
// position 0 (the CSRF field is injected into the form by the shell).
|
|
9778
|
+
var makePrimary = (idx === 0)
|
|
9779
|
+
? "<span class=\"status-pill paid\">Primary</span>"
|
|
9780
|
+
: "<form method=\"post\" action=\"/admin/products/" + pid + "/media/" + mid + "/primary\" class=\"form-inline\">" +
|
|
9781
|
+
"<button class=\"btn btn--ghost\" type=\"submit\">Make primary</button></form>";
|
|
9668
9782
|
return "<div class=\"media-card\">" +
|
|
9669
9783
|
thumb +
|
|
9670
9784
|
"<code class=\"order-id\">" + _htmlEscape(m.r2_key) + "</code>" +
|
|
9671
9785
|
"<span class=\"u-mute\">" + _htmlEscape(m.content_type || "") + (m.alt_text ? " · " + _htmlEscape(m.alt_text) : "") + "</span>" +
|
|
9672
|
-
"<
|
|
9786
|
+
"<div class=\"actions-row mt-0\">" + makePrimary +
|
|
9787
|
+
"<a class=\"btn btn--danger\" href=\"/admin/media/" + mid + "/delete/confirm-page?product_id=" + pid + "\">Delete</a>" +
|
|
9788
|
+
"</div>" +
|
|
9673
9789
|
"</div>";
|
|
9674
9790
|
}).join("");
|
|
9675
9791
|
var mediaGrid = mediaRows.length
|
|
9676
9792
|
? "<div class=\"media-grid\">" + mediaCards + "</div>"
|
|
9677
9793
|
: "<p class=\"empty\">No media attached yet.</p>";
|
|
9678
9794
|
|
|
9795
|
+
// Reorder: a single field of the current media ids, comma-joined, that the
|
|
9796
|
+
// operator rewrites into the new order. No-JS-required and v1-defensible
|
|
9797
|
+
// — the primitive normalises positions to 0..N-1 and the first row is the
|
|
9798
|
+
// hero. Only shown when there's more than one image to order.
|
|
9799
|
+
var currentMediaIds = mediaRows.map(function (m) { return m.id; }).join(",");
|
|
9800
|
+
var mediaReorderForm = mediaRows.length > 1
|
|
9801
|
+
? "<div class=\"panel mt-1 mw-40\">" +
|
|
9802
|
+
"<h3 class=\"subhead\">Reorder images</h3>" +
|
|
9803
|
+
"<p class=\"meta\">The first image is the hero shown on the product page. Rewrite the comma-separated id list into the order you want — list every current image.</p>" +
|
|
9804
|
+
"<form method=\"post\" action=\"/admin/products/" + pid + "/media/reorder\">" +
|
|
9805
|
+
"<label class=\"form-field\"><span>Ordered media ids</span>" +
|
|
9806
|
+
"<input type=\"text\" name=\"ordered_media_ids\" value=\"" + _htmlEscape(currentMediaIds) + "\"></label>" +
|
|
9807
|
+
"<div class=\"actions-row\"><button class=\"btn btn--ghost\" type=\"submit\">Apply order</button></div>" +
|
|
9808
|
+
"</form>" +
|
|
9809
|
+
"</div>"
|
|
9810
|
+
: "";
|
|
9811
|
+
|
|
9679
9812
|
var attachForm =
|
|
9680
9813
|
"<div class=\"panel mt-1 mw-40\">" +
|
|
9681
9814
|
"<h3 class=\"subhead\">Attach media by R2 key</h3>" +
|
|
@@ -9716,7 +9849,7 @@ function renderAdminProduct(opts) {
|
|
|
9716
9849
|
"</div>"
|
|
9717
9850
|
: "";
|
|
9718
9851
|
|
|
9719
|
-
var mediaSection = "<section class=\"mt\"><h3 class=\"fs-105\">Media</h3>" + mediaGrid + fileUploadForm + attachForm + uploadForm + "</section>";
|
|
9852
|
+
var mediaSection = "<section class=\"mt\"><h3 class=\"fs-105\">Media</h3>" + mediaGrid + mediaReorderForm + fileUploadForm + attachForm + uploadForm + "</section>";
|
|
9720
9853
|
|
|
9721
9854
|
// ---- head + assembly -------------------------------------------------
|
|
9722
9855
|
var statusCls = p.status === "active" ? "paid" : (p.status === "archived" ? "refunded" : "pending");
|
package/lib/asset-manifest.json
CHANGED
package/lib/catalog.js
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* Five sub-modules:
|
|
9
9
|
*
|
|
10
10
|
* - `products` — create, get, bySlug, list, update, archive, restore
|
|
11
|
-
* - `variants` — create, get, listForProduct, update, delete
|
|
11
|
+
* - `variants` — create, get, listForProduct, update, delete
|
|
12
12
|
* - `prices` — set (versioned), current, history
|
|
13
13
|
* - `inventory` — create, get, decrement, release, restock
|
|
14
|
-
* - `media` — attach, listForProduct, listForVariant, delete,
|
|
14
|
+
* - `media` — attach, get, listForProduct, listForVariant, delete,
|
|
15
|
+
* reorder, setPrimary
|
|
15
16
|
*
|
|
16
17
|
* Every write validates input at entry; bad input throws TypeError
|
|
17
18
|
* with a descriptive message so the calling route handler can
|
|
@@ -789,6 +790,64 @@ function _mediaModule(query) {
|
|
|
789
790
|
var r = await query("DELETE FROM media WHERE id = ?1", [id]);
|
|
790
791
|
return r.rowCount > 0;
|
|
791
792
|
},
|
|
793
|
+
|
|
794
|
+
// Reorder a product's media. `orderedIds` is the FULL set of this
|
|
795
|
+
// product's media ids in the desired order; `position` is rewritten to
|
|
796
|
+
// 0..N-1 to match. Throws TypeError on a non-array / id-shape error or
|
|
797
|
+
// when the set doesn't exactly match the product's current media (a
|
|
798
|
+
// missing / extra / foreign id) — a partial reorder would silently drop
|
|
799
|
+
// a row's ordering. D1 has no cross-statement transaction over the HTTP
|
|
800
|
+
// bridge, so positions are written one UPDATE per row, each scoped by
|
|
801
|
+
// product_id so a crafted id list can never reposition another product's
|
|
802
|
+
// row out of band. Returns the count written.
|
|
803
|
+
reorder: async function (productId, orderedIds) {
|
|
804
|
+
_id(productId, "product_id");
|
|
805
|
+
if (!Array.isArray(orderedIds) || orderedIds.length === 0) {
|
|
806
|
+
throw new TypeError("catalog.media.reorder: orderedIds must be a non-empty array");
|
|
807
|
+
}
|
|
808
|
+
orderedIds.forEach(function (id) { _id(id, "media id"); });
|
|
809
|
+
var existing = await query("SELECT id FROM media WHERE product_id = ?1", [productId]);
|
|
810
|
+
var have = existing.rows.map(function (r) { return r.id; }).sort();
|
|
811
|
+
var want = orderedIds.slice().sort();
|
|
812
|
+
if (have.length !== want.length ||
|
|
813
|
+
have.some(function (id, i) { return id !== want[i]; })) {
|
|
814
|
+
throw new TypeError("catalog.media.reorder: orderedIds must list exactly this product's current media");
|
|
815
|
+
}
|
|
816
|
+
for (var i = 0; i < orderedIds.length; i += 1) {
|
|
817
|
+
await query(
|
|
818
|
+
"UPDATE media SET position = ?1 WHERE id = ?2 AND product_id = ?3",
|
|
819
|
+
[i, orderedIds[i], productId],
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
return orderedIds.length;
|
|
823
|
+
},
|
|
824
|
+
|
|
825
|
+
// Promote one media row to the hero slot (position 0) and push every
|
|
826
|
+
// other row of the same product down by one, preserving their relative
|
|
827
|
+
// order. The PDP gallery renders media[0] as the hero, so this is the
|
|
828
|
+
// "set primary image" operation. Throws TypeError on a malformed id;
|
|
829
|
+
// returns false when the id doesn't resolve to a product-scoped row
|
|
830
|
+
// (an unknown id, or a variant-only row that has no product_id). Every
|
|
831
|
+
// UPDATE is scoped by product_id — same cross-product-IDOR guard as
|
|
832
|
+
// reorder.
|
|
833
|
+
setPrimary: async function (mediaId) {
|
|
834
|
+
_id(mediaId, "media id");
|
|
835
|
+
var row = (await query("SELECT id, product_id FROM media WHERE id = ?1", [mediaId])).rows[0];
|
|
836
|
+
if (!row || !row.product_id) return false;
|
|
837
|
+
var ordered = (await query(
|
|
838
|
+
"SELECT id FROM media WHERE product_id = ?1 ORDER BY position ASC, created_at ASC",
|
|
839
|
+
[row.product_id],
|
|
840
|
+
)).rows.map(function (r) { return r.id; });
|
|
841
|
+
var without = ordered.filter(function (id) { return id !== mediaId; });
|
|
842
|
+
var next = [mediaId].concat(without);
|
|
843
|
+
for (var i = 0; i < next.length; i += 1) {
|
|
844
|
+
await query(
|
|
845
|
+
"UPDATE media SET position = ?1 WHERE id = ?2 AND product_id = ?3",
|
|
846
|
+
[i, next[i], row.product_id],
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return true;
|
|
850
|
+
},
|
|
792
851
|
};
|
|
793
852
|
}
|
|
794
853
|
|
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.14.
|
|
7
|
-
"tag": "v0.14.
|
|
6
|
+
"version": "0.14.8",
|
|
7
|
+
"tag": "v0.14.8",
|
|
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.14.x
|
|
10
10
|
|
|
11
|
+
- v0.14.8 (2026-05-30) — **Source-comment and codebase-check hygiene, plus a new require-block alignment check; no API or behaviour changes.** Internal lint and comment cleanup with no operator-facing surface change. Several codebase-check comments and one stub helper name that described behaviour the check no longer has are corrected; an unused lint-suppression class and a set of stale duplicate-cluster qualifiers (functions that were since renamed or extracted) are pruned or re-pointed. Fifty-nine `// allow:` markers that named the byte-size or time-literal check on values those checks no longer flag are removed, and twenty self-negating rationales on markers the time check genuinely fires on are rewritten to say why the value coincidentally matches. A new codebase check holds top-of-file require blocks to consistent `=` column alignment, with the files that currently carry drift listed as a migration allowlist. No exported API, error code, wire format, or runtime behaviour changes. **Changed:** *Lint-suppression and codebase-check comment cleanup* — Corrected codebase-check comments that overstated their check's scope (a duplicate-code threshold described as three files when the advisory threshold is two, a narrowed byte-literal check carrying its pre-narrowing description, and a deferred-scan helper named as though it enforced a guarantee it does not yet provide). Removed an unused lint-suppression class and its one dead in-code marker, and pruned or re-pointed stale duplicate-cluster qualifiers that named functions since renamed or extracted into shared helpers. Removed fifty-nine `// allow:` markers that suppressed nothing, and rewrote twenty self-negating marker rationales (which read "not seconds" while sitting on a value the time check fires on) to explain the coincidental match. Source-comment and test hygiene only. **Detectors:** *Require-block `=` alignment check* — A new codebase check flags a top-of-file require block that mixes its `=` column alignment — a fittable line whose `=` drifts off the column the rest of the block shares. Compact single-space blocks are exempt (only blocks that declare alignment intent are checked), as are destructures and long names physically too wide to reach the column, and blank- or comment-separated tiers align independently. The files that currently carry such drift are an explicit migration allowlist, reflowed over time; new code is held to the rule.
|
|
12
|
+
|
|
11
13
|
- v0.14.7 (2026-05-30) — **Storage and audit-trail hardening: queries are gated to declared columns, raw SQL refuses embedded literals, the database key is bound to its location, sealed-column lookup hashes gain a keyed mode, audit-chain purges can require dual control, and breach deadlines ship a running clock.** This release tightens the data and audit layers against a set of failure modes that were previously reachable. Database queries are now checked against the columns a table declared in its schema: a reference to an undeclared column fails closed by default instead of silently matching nothing, and the `whereRaw` escape hatch refuses an embedded string literal so values bind through placeholders. The database encryption key is sealed with its purpose, data directory, and key path as additional authenticated data, so a key file cannot be relocated to another deployment and unsealed there; an older key without that binding upgrades itself on first load. Sealed-column equality-lookup hashes can now be computed as a keyed MAC (HMAC-SHAKE256) off a per-deployment key, making the lookup hash unforgeable without that key, while the salted-SHA3 default is unchanged. Purging the tamper-evident audit chain can be placed under a two-authorizer dual-control grant so one operator cannot erase it alone, and database credential-rejection audits now record which relation the rejected credential tried to reach. Finally, breach-notification deadlines get a running clock that raises approaching and passed alerts as each regime's window elapses. One behavior change to note: the column gate defaults to reject — if a service issues queries against columns it did not declare in its schema, set `db.init({ columnGate: "warn" })` (audited, allowed) or `"off"` while the schema is reconciled. **Added:** *Column-membership gate on every query* — `b.db.from(table)` now checks each referenced column against the table's declared schema. The mode is set with `db.init({ columnGate: "reject" | "warn" | "off" })` (default `reject`), and `query.allowedColumns([...])` narrows a single query to an explicit allowlist that is always enforced. `b.db.getDeclaredColumns(table)` returns a table's declared column names (or `null` for an unknown table). This is defense in depth against typo'd or caller-influenced column names reaching the SQL layer (CWE-89). · *Keyed mode for sealed-column lookup hashes* — Equality-lookup ("derived") hashes for sealed columns can be computed as `hmac-shake256` — a keyed MAC over a per-deployment key — instead of the default `salted-sha3`. Set it per table with `cryptoField.registerTable(name, { derivedHashMode: "hmac-shake256" })` or per column with `{ from, mode: "hmac-shake256" }`. The keyed hash is unforgeable and un-correlatable without the deployment's MAC key, which raises the bar against offline lookup-table attacks on low-entropy sealed values (CWE-916). `b.vault.getDerivedHashMacKey()` exposes the 32-byte per-deployment key; it is created on first use and re-sealed across key rotation automatically. · *Dual-control gate on audit-chain purge* — `b.auditTools.purge` accepts `dualControlGrant`. When `audit_log` is placed under dual control, a verified archive and `confirm: true` are no longer sufficient: the purge additionally requires a consumed m-of-n grant whose action is bound to the purge, so a grant minted for another operation cannot be replayed and one operator cannot erase the tamper-evident chain alone (NIST SP 800-53 AU-9, separation of duties). · *Running clock for breach-notification deadlines* — `b.incident.report.createDeadlineClock({ notify, approachThresholds })` tracks open incidents and raises `deadline_approaching` and `deadline_passed` alerts as each regime's window elapses (GDPR 72h, DORA, NIS2, and the rest of the registry). Alerts are deduplicated per incident and stage, suppressed once a submission stage is acknowledged, and the clock can run on an interval or be ticked manually. **Changed:** *Queries against undeclared columns now fail closed by default* — The column gate defaults to `reject`: a query that references a column the table did not declare throws rather than silently matching nothing. A service that intentionally queries undeclared columns can set `db.init({ columnGate: "warn" })` to audit and allow, or `"off"` to disable the gate, while its schema is reconciled. Framework-declared columns (including `_id` and derived-hash columns) are always members. **Security:** *Database encryption key bound to its location* — `db.key.enc` is sealed with additional authenticated data over its purpose, resolved data directory, and resolved key path. A sealed key copied to a different deployment or path no longer unseals there — the AEAD authentication fails — which prevents silent key relocation. A legacy key sealed without this binding is detected and re-sealed in the bound format on first load, with no operator action required. · *`whereRaw` refuses embedded string literals* — `whereRaw(sql, params)` and `WhereBuilder.raw(sql, params)` reject a raw fragment containing a string literal (`'...'`); values must bind through the `params` array. A static, operator-controlled literal can opt in with `{ allowLiterals: true }`. This closes a path where a value concatenated into a raw fragment would reintroduce SQL injection (CWE-89). · *Credential-rejection audits record the attempted relation* — A `db.auth.failed` audit row (SQLSTATE 28000 / 28P01 / 42501) now carries `attemptedTable`, the relation the rejected credential tried to reach, extracted defensively from the statement. Triage can scope the blast radius of a credential-abuse event without correlating back to the raw SQL log (CWE-778). **Detectors:** *Audit-purge dual-control gate* — A new check fails the build if a call to `purgeAuditChain` appears in a file that does not also route through the dual-control gate, so a future caller cannot physically delete chain rows without two-authorizer enforcement. · *Raw-SQL literal/interpolation guard* — A new check fails the build on a `whereRaw` / `.raw` call whose SQL argument is built by template interpolation or string concatenation, keeping the bound-params discipline enforceable in framework code. · *Hand-rolled lookup-hash guard* — A new check fails the build if a sealed-column lookup hash is derived from the per-deployment salt outside the canonical helper, so call sites cannot bypass the keyed-mode and per-column mode policy. · *Auth-audit attempted-relation guard* — A new check fails the build if a `db.auth.failed` audit is emitted in a file that does not name `attemptedTable`, so the forensic field cannot be dropped from a future emitter.
|
|
12
14
|
|
|
13
15
|
- v0.14.6 (2026-05-30) — **Access-refusal middleware can return RFC 9457 problem+json or a custom response, and several documented-but-uncallable APIs are now reachable.** Every access-refusal middleware — the auth gates (bearer, DPoP, mTLS, AAL, bound-key), CSRF, CORS, rate-limit, bot-guard, age-gate, the host and network allowlists, and the method and content-type gates — now accepts two uniform options: `problemDetails: true` returns an RFC 9457 `application/problem+json` body, and `onDeny(req, res, info)` hands the response to the caller. With neither set the refusal is byte-for-byte what it was, so this is a drop-in change that lets a service standardize one error envelope across its API instead of working around each middleware's hardcoded body. Alongside that: `b.middleware.requireBoundKey` is now exported (it was documented and tested but never wired into the middleware surface), `b.middleware.bearerAuth` accepts `requiredScopes` (previously rejected at construction, which made its scope-enforcement path unreachable), API-key refusals send the RFC 6750 challenge code that matches the failure, two documented call paths that named a missing namespace segment are corrected, and the release flow now flags stale GitHub Actions and vendored bundles — with a ready-to-paste pin — before a dependency PR is needed. **Added:** *Uniform `onDeny` and `problemDetails` options on every access-refusal middleware* — Each request-lifecycle middleware that refuses a request now takes `problemDetails: true` to emit an RFC 9457 `application/problem+json` body (composing `b.problemDetails`) and `onDeny(req, res, info)` to take over the response entirely; `info` carries the status, a machine reason, and the middleware-specific fields. The deny-path response headers (`Allow`, `WWW-Authenticate`, `Retry-After`, `Accept`) survive every mode. When neither option is set the response is unchanged. Covers `requireAuth`, `requireAal`, `requireMethods`, `requireContentType`, `requireMtls`, `requireBoundKey`, `bearerAuth`, `dpop`, `csrfProtect`, `fetchMetadata`, `botGuard`, `ageGate`, `hostAllowlist`, `networkAllowlist`, `cors`, `rateLimit`, and `dailyByteQuota` (whose existing `onExceeded` keeps working as an alias of `onDeny`). **Fixed:** *`b.middleware.requireBoundKey` is now callable* — The Bearer-API-key middleware was documented (with examples and tests) but never exported on `b.middleware`, so `b.middleware.requireBoundKey(...)` threw `undefined is not a function`. It is now wired into the middleware surface. · *`b.middleware.bearerAuth` accepts `requiredScopes`* — The RFC 6750 scope-enforcement path read `opts.requiredScopes`, but the option was rejected at construction with `unknown option`, making the 403 `insufficient_scope` behavior unreachable. `requiredScopes` is now an accepted option. · *RFC 6750 challenge codes on API-key refusals* — `b.middleware.requireBoundKey` now sends the `WWW-Authenticate` error code that matches the failure: `insufficient_scope` on a 403 missing-scope, `invalid_token` on an unknown or revoked token, and no error code on a 401 that presented no credentials (RFC 6750 §3). It previously sent `invalid_request` for every refusal. · *Corrected two documented call paths* — The compliance and network references named a path that dropped a namespace segment: the conformity-assessment scaffold is at `b.cra.report.conformityAssessment` (not `b.cra.conformityAssessment`), and the per-socket tuning helper is at `b.network.socket.applyToSocket` (not `b.network.applyToSocket`). The documented signatures now match the callable paths. · *GitHub Actions pins refreshed* — `github/codeql-action` 4.35.5 to 4.36.0, and `docker/login-action`, `docker/setup-buildx-action`, and `docker/setup-qemu-action` to their latest releases. **Detectors:** *`@primitive` reachability gate* — A new check resolves every documented `b.X.Y` primitive against the actual public surface and fails the build when a documented path is not callable (factory-instance shorthands excluded). This is the gate that would have caught the `requireBoundKey` and call-path issues above. · *Deny-path composition gate* — A new check requires every access-refusal middleware to route its refusal through the shared deny-response writer, so a future middleware cannot reintroduce a hardcoded body that locks callers out of `onDeny` / `problemDetails`. · *Actions and vendor currency in the release flow* — The release flow now fails the cut when a SHA-pinned GitHub Action or a vendored bundle is behind its latest upstream release. The actions report prints a ready-to-paste `owner/repo@<sha> # vX.Y.Z` pin and every file and line that uses it, so the bump is copy-paste rather than an after-the-fact dependency PR. Transient registry or API errors stay advisory so a flaky network response does not block an unrelated release.
|
|
@@ -62,17 +62,17 @@ var A2aTasksError = defineClass("A2aTasksError", { alwaysPermanent: true });
|
|
|
62
62
|
var JSONRPC_VERSION = "2.0";
|
|
63
63
|
|
|
64
64
|
// JSON-RPC 2.0 fixed error codes — A2A inherits these.
|
|
65
|
-
var JSONRPC_PARSE_ERROR = -32700; // allow:raw-time-literal — not
|
|
66
|
-
var JSONRPC_INVALID_REQUEST = -32600;
|
|
67
|
-
var JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
68
|
-
var JSONRPC_INVALID_PARAMS = -32602;
|
|
69
|
-
var JSONRPC_INTERNAL_ERROR = -32603;
|
|
65
|
+
var JSONRPC_PARSE_ERROR = -32700; // allow:raw-time-literal — JSON-RPC error code -32700; coincidental multiple-of-60, not a time value, C.TIME N/A
|
|
66
|
+
var JSONRPC_INVALID_REQUEST = -32600;
|
|
67
|
+
var JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
68
|
+
var JSONRPC_INVALID_PARAMS = -32602;
|
|
69
|
+
var JSONRPC_INTERNAL_ERROR = -32603;
|
|
70
70
|
|
|
71
71
|
// A2A-specific error codes per the spec's task-error vocabulary.
|
|
72
72
|
// A2A_TASK_NOT_FOUND (-32002) + A2A_TASK_NOT_CANCELABLE (-32003) are
|
|
73
73
|
// raised by operator handlers — they're reserved here for documentation
|
|
74
74
|
// purposes only.
|
|
75
|
-
var A2A_SCOPE_DENIED = -32001;
|
|
75
|
+
var A2A_SCOPE_DENIED = -32001;
|
|
76
76
|
|
|
77
77
|
var ALLOWED_METHODS = Object.freeze(["tasks/send", "tasks/get", "tasks/cancel"]);
|
|
78
78
|
|
|
@@ -26,7 +26,7 @@ var audit = require("./audit");
|
|
|
26
26
|
var { AiInputError } = require("./framework-error");
|
|
27
27
|
|
|
28
28
|
var SAMPLE_TRUNC = 80; // sample truncation length, not bytes
|
|
29
|
-
var CONFIDENCE_BASE = 60; // allow:raw-time-literal — not
|
|
29
|
+
var CONFIDENCE_BASE = 60; // allow:raw-time-literal — confidence-score base 60; coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
30
30
|
|
|
31
31
|
var PATTERNS = [
|
|
32
32
|
{ id: "ignore-prior-instructions", severity: 3, re:
|
|
@@ -90,7 +90,7 @@ var BUILTIN_RANKS = {
|
|
|
90
90
|
"urn:mace:incommon:iap:silver": 32, // ACR rank, not bytes
|
|
91
91
|
|
|
92
92
|
// Phishing-resistant multi-factor (passkey UV)
|
|
93
|
-
"phrh": 60, // allow:raw-time-literal — ACR rank, not
|
|
93
|
+
"phrh": 60, // allow:raw-time-literal — ACR rank value 60; coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
94
94
|
|
|
95
95
|
// Hardware-bound + phishing-resistant + multi-factor
|
|
96
96
|
"loa3": 70,
|
|
@@ -68,9 +68,9 @@ var emit = validateOpts.makeNamespacedEmitters("auth.ciba", { audit: audit, obse
|
|
|
68
68
|
var DEFAULT_INTERVAL_SEC = 5;
|
|
69
69
|
var DEFAULT_EXPIRES_SEC = 600;
|
|
70
70
|
var MAX_BINDING_MSG_LEN = 200;
|
|
71
|
-
var MAX_RESPONSE_BYTES = 64 * 1024;
|
|
71
|
+
var MAX_RESPONSE_BYTES = 64 * 1024;
|
|
72
72
|
var MIN_INTERVAL_SEC = 1;
|
|
73
|
-
var MAX_INTERVAL_SEC = 300; // allow:raw-time-literal — interval ceiling
|
|
73
|
+
var MAX_INTERVAL_SEC = 300; // allow:raw-time-literal — 300s polling-interval ceiling; time-derived literal, C.TIME.seconds(300) applies, retained pending migration
|
|
74
74
|
|
|
75
75
|
// _emitAudit emits under the "auth.ciba.<action>" namespace; _emitMetric
|
|
76
76
|
// fires the matching observability counter. Implementations live in
|
|
@@ -490,7 +490,7 @@ function create(opts) {
|
|
|
490
490
|
var current = entry ? entry.interval : DEFAULT_INTERVAL_SEC;
|
|
491
491
|
var idpSuggested = err.cibaError && typeof err.cibaError.interval === "number"
|
|
492
492
|
? err.cibaError.interval : null;
|
|
493
|
-
var next = current + 5;
|
|
493
|
+
var next = current + 5;
|
|
494
494
|
if (idpSuggested !== null && idpSuggested > next && idpSuggested <= MAX_INTERVAL_SEC) {
|
|
495
495
|
next = idpSuggested;
|
|
496
496
|
}
|
|
@@ -220,7 +220,7 @@ var PSS_SALT_BYTES_SHA512 = C.BYTES.bytes(64);
|
|
|
220
220
|
// CSPRNG output and refuses pathological payloads.
|
|
221
221
|
var MAX_DEVICE_CODE_BYTES = C.BYTES.kib(8);
|
|
222
222
|
// RFC 8628 §3.4 — 5s is the spec-documented MINIMUM polling interval.
|
|
223
|
-
var MIN_DEVICE_POLL_INTERVAL_SEC = 5;
|
|
223
|
+
var MIN_DEVICE_POLL_INTERVAL_SEC = 5;
|
|
224
224
|
// OIDC Back-Channel Logout §2.6 — replay defense via jti store catches
|
|
225
225
|
// duplicate-jti reuse, but pre-v0.9.x an old captured logout-token
|
|
226
226
|
// with a fresh jti could still pass. Enforce iat freshness against
|
|
@@ -69,7 +69,7 @@ var emit = validateOpts.makeNamespacedEmitters("auth.oid4vci", { audit: audit, o
|
|
|
69
69
|
var DEFAULT_PRE_AUTH_TTL_MS = C.TIME.minutes(5);
|
|
70
70
|
var DEFAULT_ACCESS_TOKEN_TTL = C.TIME.minutes(15);
|
|
71
71
|
var DEFAULT_C_NONCE_TTL_MS = C.TIME.minutes(5);
|
|
72
|
-
var MAX_PROOF_BYTES = 32 * 1024;
|
|
72
|
+
var MAX_PROOF_BYTES = 32 * 1024;
|
|
73
73
|
var SUPPORTED_CREDENTIAL_FORMATS = ["vc+sd-jwt", "dc+sd-jwt"];
|
|
74
74
|
|
|
75
75
|
var _emitAudit = emit.audit;
|
|
@@ -81,7 +81,7 @@ var _emitAudit = emit.audit;
|
|
|
81
81
|
var _emitMetric = emit.metric;
|
|
82
82
|
|
|
83
83
|
var SUPPORTED_ALGS = ["ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "EdDSA"];
|
|
84
|
-
var MAX_STATEMENT_BYTES = 64 * 1024;
|
|
84
|
+
var MAX_STATEMENT_BYTES = 64 * 1024;
|
|
85
85
|
var MAX_CHAIN_DEPTH = 10; // federation chain depth ceiling
|
|
86
86
|
|
|
87
87
|
function _b64uDecodeStr(s) { return Buffer.from(s, "base64url").toString("utf8"); }
|
|
@@ -48,8 +48,8 @@ var STATE_DEADLINES = Object.freeze({
|
|
|
48
48
|
// Each entry: { days, statute, asapCeilingDays }
|
|
49
49
|
// Days = statutory hard deadline in days. asapCeilingDays = the
|
|
50
50
|
// operator-defensible ceiling for "without unreasonable delay" states.
|
|
51
|
-
AL: { days: 45, statute: "Ala. Code §8-38-5" },
|
|
52
|
-
AK: { days: 45, statute: "Alaska Stat. §45.48.010" },
|
|
51
|
+
AL: { days: 45, statute: "Ala. Code §8-38-5" },
|
|
52
|
+
AK: { days: 45, statute: "Alaska Stat. §45.48.010" },
|
|
53
53
|
AZ: { days: 45, statute: "Ariz. Rev. Stat. §18-552" }, /* allow:raw-time-literal — statutory deadline days */
|
|
54
54
|
AR: { days: 45, statute: "Ark. Code §4-110-105" }, /* allow:raw-time-literal — statutory deadline days */
|
|
55
55
|
CA: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Cal. Civ. Code §1798.82", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
|
|
@@ -67,7 +67,7 @@ var STATE_DEADLINES = Object.freeze({
|
|
|
67
67
|
KS: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Kan. Stat. §50-7a02", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
|
|
68
68
|
KY: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Ky. Rev. Stat. §365.732", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
|
|
69
69
|
LA: { days: 60, statute: "La. Rev. Stat. §51:3074" }, /* allow:raw-time-literal — statutory deadline days */
|
|
70
|
-
ME: { days: 30, statute: "Me. Rev. Stat. tit. 10 §1348" },
|
|
70
|
+
ME: { days: 30, statute: "Me. Rev. Stat. tit. 10 §1348" },
|
|
71
71
|
MD: { days: 45, statute: "Md. Code Com. Law §14-3504" }, /* allow:raw-time-literal — statutory deadline days */
|
|
72
72
|
MA: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Mass. Gen. Laws ch. 93H §3", asapCeilingDays: 30 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
|
|
73
73
|
MI: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Mich. Comp. Laws §445.72", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
|
|
@@ -697,7 +697,7 @@ function _expandSingleRule(rule, startMs, ctx) {
|
|
|
697
697
|
}
|
|
698
698
|
var byHourSet = _bySet(rule.byHour, 0, 23); // RFC 5545 hour range
|
|
699
699
|
var byMinuteSet = _bySet(rule.byMinute, 0, 59); // RFC 5545 minute range
|
|
700
|
-
var bySecondSet = _bySet(rule.bySecond, 0, 60); // allow:raw-time-literal — second-of-minute bound, not a duration
|
|
700
|
+
var bySecondSet = _bySet(rule.bySecond, 0, 60); // allow:raw-time-literal — second-of-minute upper bound (0..60); coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
701
701
|
|
|
702
702
|
function _isoWeekParts(d) {
|
|
703
703
|
// ISO 8601 week-of-year + week-year. The week-YEAR can differ
|
|
@@ -709,7 +709,7 @@ function _expandSingleRule(rule, startMs, ctx) {
|
|
|
709
709
|
tmp.setUTCDate(tmp.getUTCDate() + 4 - dayOfWeek); // ISO week-year anchor (Thursday)
|
|
710
710
|
var weekYear = tmp.getUTCFullYear();
|
|
711
711
|
var yearStart = new Date(Date.UTC(weekYear, 0, 1));
|
|
712
|
-
var week = Math.ceil((((tmp - yearStart) / 86400000) + 1) / 7);
|
|
712
|
+
var week = Math.ceil((((tmp - yearStart) / 86400000) + 1) / 7);
|
|
713
713
|
return { week: week, year: weekYear };
|
|
714
714
|
}
|
|
715
715
|
function _isoWeekOf(d) {
|
|
@@ -492,7 +492,7 @@ function signCose(manifest, opts) {
|
|
|
492
492
|
}
|
|
493
493
|
unprotectedHdr = Buffer.concat([
|
|
494
494
|
_cborMapHeader(1),
|
|
495
|
-
_cborInt(33), // allow:raw-time-literal — RFC 9360 x5chain header label, not a duration
|
|
495
|
+
_cborInt(33), // allow:raw-time-literal — RFC 9360 x5chain COSE header label; coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
496
496
|
chainArray,
|
|
497
497
|
]);
|
|
498
498
|
} else {
|
|
@@ -590,7 +590,7 @@ var CAC_KIND_ENUM = Object.freeze({
|
|
|
590
590
|
text: true, image: true, audio: true, video: true,
|
|
591
591
|
"virtual-scene": true, other: true,
|
|
592
592
|
});
|
|
593
|
-
var CAC_USCC_RE = /^[0-9A-HJ-NPQRTUWXY]{18}$/;
|
|
593
|
+
var CAC_USCC_RE = /^[0-9A-HJ-NPQRTUWXY]{18}$/;
|
|
594
594
|
var ISO8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
595
595
|
|
|
596
596
|
function cacImplicitLabel(opts) {
|
|
@@ -605,7 +605,7 @@ function cacImplicitLabel(opts) {
|
|
|
605
605
|
throw new ContentCredentialsError("cac-implicit-label/oversize-provider-name",
|
|
606
606
|
"cacImplicitLabel: providerName exceeds " + STR_LEN_MAX + " bytes (UTF-8)");
|
|
607
607
|
}
|
|
608
|
-
if (typeof opts.providerCode !== "string" || opts.providerCode.length !== 18 ||
|
|
608
|
+
if (typeof opts.providerCode !== "string" || opts.providerCode.length !== 18 ||
|
|
609
609
|
!CAC_USCC_RE.test(opts.providerCode)) { // allow:regex-no-length-cap — length-bounded immediately above
|
|
610
610
|
throw new ContentCredentialsError("cac-implicit-label/bad-provider-code",
|
|
611
611
|
"cacImplicitLabel: providerCode must be an 18-char unified social credit code " +
|
|
@@ -132,7 +132,7 @@ function _parseHHMM(s) {
|
|
|
132
132
|
throw new DdlChangeControlError("ddl-change-control/bad-window",
|
|
133
133
|
"windowSpec time out of range - got " + s);
|
|
134
134
|
}
|
|
135
|
-
return hh * 60 + mm; // allow:raw-time-literal —
|
|
135
|
+
return hh * 60 + mm; // allow:raw-time-literal — HH*60+MM minute-of-day conversion; coincidental multiple-of-60 factor, not a duration, C.TIME N/A
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function _isInWindow(window, nowMs) {
|
|
@@ -141,7 +141,7 @@ function _isInWindow(window, nowMs) {
|
|
|
141
141
|
var d = new Date(nowMs);
|
|
142
142
|
var dayIdx = d.getUTCDay();
|
|
143
143
|
if (!window.days.has(dayIdx)) return false;
|
|
144
|
-
var min = d.getUTCHours() * 60 + d.getUTCMinutes(); // allow:raw-time-literal —
|
|
144
|
+
var min = d.getUTCHours() * 60 + d.getUTCMinutes(); // allow:raw-time-literal — HH*60+MM minute-of-day conversion; coincidental multiple-of-60 factor, not a duration, C.TIME N/A
|
|
145
145
|
return min >= window.startMin && min < window.endMin;
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -65,7 +65,7 @@ var MAX_JWK_B64_CHARS = 8192; // bounded did:jwk encode
|
|
|
65
65
|
// keyLen is the multicodec payload: Ed25519 raw 32; EC compressed point.
|
|
66
66
|
var MULTICODEC = {
|
|
67
67
|
0xed: { name: "Ed25519", kind: "okp" }, // ed25519-pub
|
|
68
|
-
0x1200: { name: "P-256", kind: "ec", curveOid: "1.2.840.10045.3.1.7" },
|
|
68
|
+
0x1200: { name: "P-256", kind: "ec", curveOid: "1.2.840.10045.3.1.7" },
|
|
69
69
|
0x1201: { name: "P-384", kind: "ec", curveOid: "1.3.132.0.34" }, // p384-pub multicodec code
|
|
70
70
|
0xe7: { name: "secp256k1", kind: "ec", curveOid: "1.3.132.0.10" }, // secp256k1-pub
|
|
71
71
|
};
|
|
@@ -159,7 +159,7 @@ function _keyObjectFromMulticodec(code, keyBytes) {
|
|
|
159
159
|
|
|
160
160
|
// AlgorithmIdentifier SEQUENCE { id-ecPublicKey, namedCurve OID }.
|
|
161
161
|
function _ecAlgId(curveOid) {
|
|
162
|
-
var idEcPublicKey = Buffer.from("06072a8648ce3d0201", "hex");
|
|
162
|
+
var idEcPublicKey = Buffer.from("06072a8648ce3d0201", "hex");
|
|
163
163
|
var curve = _oidDer(curveOid);
|
|
164
164
|
var inner = Buffer.concat([idEcPublicKey, curve]);
|
|
165
165
|
return Buffer.concat([Buffer.from([0x30, inner.length]), inner]);
|
|
@@ -1094,7 +1094,7 @@ function dbTicketStore(opts) {
|
|
|
1094
1094
|
|
|
1095
1095
|
// State DSR rule table — `responseDays` / `extensionDays` / `cureDays`
|
|
1096
1096
|
// are integer day-counts from per-state statutes (not durations in
|
|
1097
|
-
// seconds/ms).
|
|
1097
|
+
// seconds/ms).
|
|
1098
1098
|
var STATE_RULES = Object.freeze({
|
|
1099
1099
|
"vcdpa": { posture: "vcdpa", state: "VA", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01" }, // allow:raw-time-literal
|
|
1100
1100
|
"co-cpa": { posture: "co-cpa", state: "CO", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01; UOOM (GPC) mandatory" }, // allow:raw-time-literal
|
|
@@ -1110,7 +1110,7 @@ var STATE_RULES = Object.freeze({
|
|
|
1110
1110
|
"nj-njdpa": { posture: "nj-njdpa", state: "NJ", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 17, notes: "Under-17 opt-in default" }, // allow:raw-time-literal
|
|
1111
1111
|
"ky-kcdpa": { posture: "ky-kcdpa", state: "KY", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01" }, // allow:raw-time-literal
|
|
1112
1112
|
"tn-tipa": { posture: "tn-tipa", state: "TN", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "NIST CSF safe-harbor available" }, // allow:raw-time-literal
|
|
1113
|
-
"mn-mncdpa": { posture: "mn-mncdpa", state: "MN", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-07-31; profiling opt-out for consequential decisions" },
|
|
1113
|
+
"mn-mncdpa": { posture: "mn-mncdpa", state: "MN", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-07-31; profiling opt-out for consequential decisions" },
|
|
1114
1114
|
"ri-ricpa": { posture: "ri-ricpa", state: "RI", responseDays: 45, extensionDays: 45, cureDays: 0, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01; no cure period" }, // allow:raw-time-literal
|
|
1115
1115
|
"ne-dpa": { posture: "ne-dpa", state: "NE", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2025-01-01" }, // allow:raw-time-literal
|
|
1116
1116
|
"nv-sb370": { posture: "nv-sb370", state: "NV", responseDays: 60, extensionDays: 30, cureDays: 0, profilingOptOut: false, minorOptIn: null, notes: "Consumer-health data only" }, // allow:raw-time-literal
|
|
@@ -1118,8 +1118,8 @@ var STATE_RULES = Object.freeze({
|
|
|
1118
1118
|
"ct-sb3": { posture: "ct-sb3", state: "CT", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: false, minorOptIn: null, notes: "Consumer-health data only" }, // allow:raw-time-literal
|
|
1119
1119
|
"tx-cubi": { posture: "tx-cubi", state: "TX", responseDays: 0, extensionDays: 0, cureDays: 0, profilingOptOut: false, minorOptIn: null, notes: "Biometric-only; private-right-of-action absent" }, // allow:raw-time-literal
|
|
1120
1120
|
"modpa": { posture: "modpa", state: "MD", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Strict data-minimization; effective 2026-10-01" }, // allow:raw-time-literal
|
|
1121
|
-
"quebec-25": { posture: "quebec-25", state: "QC", responseDays: 30, extensionDays: 30, cureDays: 0, profilingOptOut: true, minorOptIn: 14, notes: "DPIA + automated-decision opt-out; FR-language obligations" },
|
|
1122
|
-
"fl-fdbr": { posture: "fl-fdbr", state: "FL", responseDays: 45, extensionDays: 15, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Narrow scope ($1B+ revenue threshold); effective 2024-07-01; AG-only enforcement" },
|
|
1121
|
+
"quebec-25": { posture: "quebec-25", state: "QC", responseDays: 30, extensionDays: 30, cureDays: 0, profilingOptOut: true, minorOptIn: 14, notes: "DPIA + automated-decision opt-out; FR-language obligations" },
|
|
1122
|
+
"fl-fdbr": { posture: "fl-fdbr", state: "FL", responseDays: 45, extensionDays: 15, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Narrow scope ($1B+ revenue threshold); effective 2024-07-01; AG-only enforcement" },
|
|
1123
1123
|
});
|
|
1124
1124
|
|
|
1125
1125
|
/**
|
|
@@ -803,7 +803,7 @@ async function transaction(fn, opts) {
|
|
|
803
803
|
_emitMetric("externaldb.transaction.retry", 1,
|
|
804
804
|
{ backend: b.name, code: txErr.code, attempt: String(attempt) });
|
|
805
805
|
var jitter = bCrypto.randomInt(0, 6); // 0-5ms jitter
|
|
806
|
-
await safeAsync.sleep(attempt * 5 + jitter);
|
|
806
|
+
await safeAsync.sleep(attempt * 5 + jitter);
|
|
807
807
|
continue;
|
|
808
808
|
}
|
|
809
809
|
var failureMs = Date.now() - t0;
|
|
@@ -73,7 +73,7 @@ var IPV4_RESERVED = Object.freeze([
|
|
|
73
73
|
{ net: _ipv4ToUint32([127, 0, 0, 0]), prefix: 8, label: "loopback" }, // IPv4 octets
|
|
74
74
|
{ net: _ipv4ToUint32([169, 254, 0, 0]), prefix: 16, label: "link-local" }, // IPv4 octets
|
|
75
75
|
{ net: _ipv4ToUint32([224, 0, 0, 0]), prefix: 4, label: "multicast" }, // IPv4 octets
|
|
76
|
-
{ net: _ipv4ToUint32([240, 0, 0, 0]), prefix: 4, label: "reserved-class-e" }, // allow:raw-time-literal —
|
|
76
|
+
{ net: _ipv4ToUint32([240, 0, 0, 0]), prefix: 4, label: "reserved-class-e" }, // allow:raw-time-literal — IPv4 octet 240; coincidental multiple-of-60, not a duration, C.TIME N/A
|
|
77
77
|
{ net: _ipv4ToUint32([192, 0, 2, 0]), prefix: 24, label: "documentation-test-net-1" }, // IPv4 octets
|
|
78
78
|
{ net: _ipv4ToUint32([198, 51, 100, 0]), prefix: 24, label: "documentation-test-net-2" }, // IPv4 octets
|
|
79
79
|
{ net: _ipv4ToUint32([203, 0, 113, 0]), prefix: 24, label: "documentation-test-net-3" }, // IPv4 octets
|