@blamejs/blamejs-shop 0.0.121 → 0.0.123

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +5 -1
  3. package/SECURITY.md +11 -2
  4. package/lib/admin.js +51 -0
  5. package/lib/order.js +23 -0
  6. package/lib/reviews.js +29 -0
  7. package/lib/storefront.js +538 -10
  8. package/lib/vendor/MANIFEST.json +2 -2
  9. package/lib/vendor/blamejs/CHANGELOG.md +16 -0
  10. package/lib/vendor/blamejs/README.md +3 -0
  11. package/lib/vendor/blamejs/SECURITY.md +2 -0
  12. package/lib/vendor/blamejs/api-snapshot.json +220 -2
  13. package/lib/vendor/blamejs/index.js +3 -0
  14. package/lib/vendor/blamejs/lib/ai-capability.js +482 -0
  15. package/lib/vendor/blamejs/lib/ai-disclosure.js +107 -0
  16. package/lib/vendor/blamejs/lib/ai-dp.js +539 -0
  17. package/lib/vendor/blamejs/lib/ai-quota.js +526 -0
  18. package/lib/vendor/blamejs/lib/backup/index.js +210 -1
  19. package/lib/vendor/blamejs/lib/compliance.js +48 -1
  20. package/lib/vendor/blamejs/lib/crypto.js +9 -2
  21. package/lib/vendor/blamejs/package.json +1 -1
  22. package/lib/vendor/blamejs/release-notes/v0.12.22.json +18 -0
  23. package/lib/vendor/blamejs/release-notes/v0.12.23.json +18 -0
  24. package/lib/vendor/blamejs/release-notes/v0.12.24.json +18 -0
  25. package/lib/vendor/blamejs/release-notes/v0.12.25.json +18 -0
  26. package/lib/vendor/blamejs/release-notes/v0.12.26.json +30 -0
  27. package/lib/vendor/blamejs/release-notes/v0.12.27.json +26 -0
  28. package/lib/vendor/blamejs/release-notes/v0.12.28.json +26 -0
  29. package/lib/vendor/blamejs/release-notes/v0.12.29.json +31 -0
  30. package/lib/vendor/blamejs/test/layer-0-primitives/ai-capability.test.js +228 -0
  31. package/lib/vendor/blamejs/test/layer-0-primitives/ai-disclosure-apply-all.test.js +126 -0
  32. package/lib/vendor/blamejs/test/layer-0-primitives/ai-dp.test.js +167 -0
  33. package/lib/vendor/blamejs/test/layer-0-primitives/ai-quota.test.js +264 -0
  34. package/lib/vendor/blamejs/test/layer-0-primitives/backup-clone-bundle.test.js +178 -0
  35. package/lib/vendor/blamejs/test/layer-0-primitives/backup-find-bundles.test.js +104 -0
  36. package/lib/vendor/blamejs/test/layer-0-primitives/backup-rewrap-all.test.js +168 -0
  37. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +60 -0
  38. package/lib/vendor/blamejs/test/layer-0-primitives/compliance-eu-ai-act-posture.test.js +93 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-random-int.test.js +21 -0
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.0.x
10
10
 
11
+ - v0.0.123 (2026-05-24) — **Wishlist — save products to your account, with social-proof counts on the product page.** Signed-in customers can now save products to a wishlist from the product page. A "N shoppers saved this" count surfaces social proof, and saved items live on a new account page where they can be removed or reopened. The save control and count render identically on the edge and container paths; the toggle is idempotent (saving twice is a no-op, toggling again removes) and login-gated, since a wishlist is scoped to one customer. **Added:** *Save to wishlist on the product page* — The PDP renders a "Save to wishlist" control and, once any customer has saved it, a "N shoppers saved this" social-proof count. Both render server-side on the edge and container paths. The count is public; the save action requires sign-in. · *`/account/wishlist` — saved items* — A new account page lists the customer's saved products with a thumbnail, a link back to the product, and a Remove control. Entries whose product was archived render as "no longer available" rather than breaking the list (wishlist rows are orphan-tolerant by design). The account dashboard links to it. · *`POST /wishlist/toggle`* — Login-required endpoint that saves the product if it isn't saved and removes it if it is. Idempotent (`INSERT OR IGNORE`). Redirects back to the product by resolving its canonical slug from the product id, or to a safe same-origin `return_to` (the account page's Remove uses it) — a forged or off-site redirect target is rejected.
12
+
13
+ - v0.0.122 (2026-05-24) — **Product reviews on the storefront — verified-buyer submission, operator moderation, and rich-snippet ratings.** The product page now shows customer reviews: an average rating, a per-star distribution, and the published review list, with `AggregateRating` structured data so star ratings can surface in search results. Submission is gated to signed-in customers who have actually purchased the product — the route confirms a completed order for that product before accepting a review, and re-checks on submit. Reviews land in a pending state; operators publish or reject them through new bearer-token admin endpoints. Display and structured data are identical whether the page is served at the edge or from the container. **Added:** *Reviews on the product page* — The PDP renders an average rating, a per-star distribution bar chart, and the published reviews (newest first, verified-buyer badge, date). Products with no reviews show an invite to be the first. Rendered server-side on both the edge and container paths. · *Verified-buyer review submission* — `GET /products/:slug/review` shows the review form to a signed-in customer who has purchased the product; everyone else is redirected to sign in or told only verified buyers can review. `POST /products/:slug/review` re-checks the purchase before accepting the review, so a direct POST can't bypass the gate. Submitted reviews start pending. · *Operator review moderation* — `GET /admin/reviews?status=pending` lists the moderation queue across all products; `GET /admin/reviews/:id` reads one; `POST /admin/reviews/:id/publish` and `POST /admin/reviews/:id/reject` move it. Bearer-token-gated like the rest of the admin API. Pending and rejected reviews never appear on the storefront. · *`AggregateRating` structured data* — The product page emits Schema.org `AggregateRating` (rating value + review count) nested in the existing `Product` JSON-LD when published reviews exist, so eligible products can show star ratings in search results. Omitted entirely at zero reviews to stay valid. The container render path now emits the full `Product` + `AggregateOffer` + `BreadcrumbList` JSON-LD that the edge already did. · *`order.hasPurchasedProduct(customerId, productId)`* — Existence check — true when the customer has an order line for any variant of the product in an order that reached `paid` or later (excludes `pending` and `cancelled`). Backs the review purchase gate. · *`reviews.listByStatus(status, opts)`* — Lists reviews across all products by status, newest first, with the same opaque tuple cursor as `listForProduct`. Backs the admin moderation queue.
14
+
11
15
  - v0.0.121 (2026-05-24) — **Codebase-patterns detector blocks SHA3 primitives in `worker/` — prevents the regression that broke newsletter signup.** v0.0.120 fixed the newsletter signup by routing the POST to the container instead of computing `b.crypto.namespaceHash` (SHA3-512) at the edge — Workers' `nodejs_compat` doesn't expose SHA3-family digests. New `worker-uses-sha3-primitive` codebase-patterns detector flags any `b.crypto.{sha3Hash, hmacSha3, namespaceHash, shake256, shake512, hkdfSha3}(...)` call under `worker/` so the next operator can't re-introduce the substrate-mismatch bug. Catalog grows 121 → 122. Detector targets the exact SHA3-family primitive names rather than a general regex match — Worker code that legitimately uses `b.crypto.toBase64Url`, `b.crypto.timingSafeEqual`, `b.crypto.generateBytes`, `b.crypto.hmacSha256` (the Stripe webhook augment), etc. is unaffected. **Added:** *`worker-uses-sha3-primitive` detector* — Flags `b.crypto.sha3Hash(...)`, `hmacSha3(...)`, `namespaceHash(...)`, `shake256(...)`, `shake512(...)`, and `hkdfSha3(...)` in any file under `worker/`. Catches the substrate-mismatch class that broke v0.0.92's edge newsletter handler (live for 28 versions before being routed back to the container in v0.0.120). When edge code needs a stable hash: route to the container OR use `b.crypto.hmacSha256` IFF both sides read with the same SHA-256 path. Never have one substrate write SHA3 and another read SHA-256 — silent divergence breaks cross-substrate lookups (e.g. unsubscribe-by-email_hash).
12
16
 
13
17
  - v0.0.120 (2026-05-24) — **Route POST /newsletter through the container — Workers `nodejs_compat` can't compute SHA3-512 (`b.crypto.namespaceHash`).** The v0.0.92 edge-served newsletter handler was 500-ing every submission with `Error: Digest method not supported`. Root cause surfaced via the temporary `X-Newsletter-Diag` header (added in v0.0.119): Cloudflare Workers' `nodejs_compat` runtime exposes `node:crypto` but the supported digest set is a subset of full Node — `createHash("sha3-512")` isn't in it. Working around with a Web-Crypto-API SHA-256 fallback would silently diverge the Worker's `email_hash` values from the container's SHA3-512 values, breaking the unsubscribe lookup (different hash → different row → silent unsubscribe failure). The edge handler was the wrong substrate for this primitive. The POST falls through to `_forwardToContainer` so the framework's SHA3-512 path runs server-side and the `email_hash` column stays consistent across reads. The ~200ms container hop on signup submit is paid back by a working unsubscribe flow. The dead `_edgeNewsletter` function + its `renderNewsletterThanks` / `renderNewsletterError` imports + the diagnostic `X-Newsletter-Diag` header are all removed in the same patch. **Fixed:** *Newsletter signup now uses the framework's SHA3-512 hash via the container* — Removed: `_edgeNewsletter` handler (123 lines), `renderNewsletterThanks` / `renderNewsletterError` imports in `worker/index.js`, the `X-Newsletter-Diag` diagnostic response header (its job is done). The dispatch comment above the now-removed `if (pathname === "/newsletter" && ...)` block documents the substrate decision + the SHA3-512 / Workers-runtime constraint so the next operator doesn't re-introduce the edge handler.
package/README.md CHANGED
@@ -63,9 +63,11 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
63
63
  | **`lib/email.js`** | Transactional templates — order receipt, ship notification, refund confirmation. Strict `{{var}}` renderer with HTML escape + refusal of unknown / unused placeholders. Composed on `b.mail` (DKIM/SPF/DMARC/BIMI upstream). |
64
64
  | **`lib/storefront.js`** | Server-rendered HTML — utility bar + sticky header + dark hero with code-preview card + primitives marquee + featured-product callout + collections grid + framework feature band + designed catalog grid + newsletter band + four-column footer. Designed surfaces also for PDP, cart, checkout, pay, order, account login / register / dashboard, search results, `/admin` API landing, 404. Image-bearing cards on the home + search grids pull from `catalog.media`. The default theme stylesheet is external (R2-served `themes/default/assets/css/main.css`) and CSP-compliant — operators override by uploading a replacement at the same key, by passing `opts.theme_css` to renderers, or by registering a named theme through the `theme` primitive. |
65
65
  | **`lib/customers.js`** | Customer accounts — passkey-only (WebAuthn). Email is stored hash-only (`b.crypto.namespaceHash` namespace `customer-email`); the raw address never lands in D1. Passkey credentials carry CBOR-encoded public keys, transport hints, and SHA3-512-fingerprinted attestation. Account routes (`/account/login`, `/account/register`, `/account`) ship as designed cards on the storefront. |
66
+ | **`lib/reviews.js`** | Operator-moderated product ratings. Submission requires a signed-in customer **and** a verified purchase — `/products/:slug/review` confirms a completed order for the product (via `order.hasPurchasedProduct`) before accepting, re-checked on POST; reviews land `pending`. Author identity is hash-only (`b.crypto.namespaceHash`); the raw email is never stored. The PDP renders the average, per-star distribution, and published reviews with `AggregateRating` JSON-LD. `/admin/reviews` is the moderation queue (`listByStatus` → publish / reject). |
67
+ | **`lib/wishlist.js`** | Per-customer saved products. The PDP renders a login-gated "Save to wishlist" toggle and a "N shoppers saved this" social-proof count; `/account/wishlist` lists saved items (remove + reopen, orphan-tolerant when a product is archived). `POST /wishlist/toggle` is idempotent (`INSERT OR IGNORE`) and redirects to the canonical product slug or a safe same-origin `return_to`. UUID-shape-validated ids, `b.pagination` HMAC cursors. |
66
68
  | **`lib/subscriptions.js`** | Stripe-backed recurring billing — `subscription_plans` (interval / amount / trial) + `subscriptions` (mirrors Stripe's object byte-for-byte). `subscriptions.create` POSTs to Stripe via the payment dep, then persists the returned object locally. `handleStripeEvent` replays `customer.subscription.*` events into the local row so the shop has an authoritative view without round-tripping. |
67
69
  | **`lib/newsletter.js`** | Operator-collected email broadcast list — `signup({ email, source })` composes `b.guardEmail` for shape validation, `b.crypto.namespaceHash` for the dedup key, and `INSERT OR IGNORE` for idempotency. Storefront POST `/newsletter` route renders a designed thank-you card with separate copy for the `new` vs `dedup` branches. |
68
- | **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. |
70
+ | **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans + review moderation. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. |
69
71
  | **`lib/catalog-import.js`** | Bulk CSV import — `POST /admin/catalog/import` accepts a `text/csv` body, parses via `b.csv`, content-safety-filters every cell through `b.guardCsv` (formula-injection / bidi / control / dangerous-function denylist), validates exact header order, de-dupes rows by `product_slug`, returns per-row errors without aborting. Default 1 MiB / 10000 rows caps. |
70
72
  | **`lib/theme.js`** | File-backed templates with fallback chain. Operators register a named theme under `<themesDir>/<name>/*.html` and the storefront dispatches every renderer through it. `assetUrl(path)` resolves to `/assets/themes/<name>/<path>`. The shipped `default` theme is the fallback. |
71
73
 
@@ -80,6 +82,8 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
80
82
  - `migrations-d1/0008_inventory_thresholds.sql` — low-stock alert thresholds + alerts
81
83
  - `migrations-d1/0009_subscriptions.sql` — subscription_plans + subscriptions (Stripe-mirrored)
82
84
  - `migrations-d1/0010_newsletter_signups.sql` — email signups with hash-based dedup
85
+ - `migrations-d1/0011_reviews.sql` — operator-moderated product reviews (hash-only author identity)
86
+ - `migrations-d1/0012_wishlist.sql` — per-customer saved products (unique customer + product + variant)
83
87
 
84
88
  ### Demo seed
85
89
 
package/SECURITY.md CHANGED
@@ -92,8 +92,8 @@ node -e "
92
92
 
93
93
  | Purpose | Algorithm | Fingerprint |
94
94
  |-----------------------------------|------------|---------------------------|
95
- | Maintainer commit/tag signing key | SSH-ED25519 | *(populate on first signed tag — see below)* |
96
- | Release-signing public key | ML-DSA-65 (FIPS 204) | `0d9d241e23c333201911afd6d6d0b0ba9a2feb4f60bf5fdd976bb33e0678f7ff0ec3db816c3cb2d9caf50c681caf8b04939ffe8cf4652eae9e6c90c988bfb87e` (SHA3-512 of `keys/release-pqc-pub.json#publicKey`) |
95
+ | Maintainer commit/tag signing key | SSH-ED25519 | `SHA256:5oF/XWhFpMde9TRfEX2GAHiApAq/MXOS4vti5zQbD7g` (cross-check against `https://github.com/<maintainer>.keys` — see below) |
96
+ | Release-signing public key | ML-DSA-65 (FIPS 204) | `d40e1e2b06a2509271cc3cf76bd34c63d2fbb093f42e1e1f6b349288900ec044509ca183b3d735cda003d80eb6cf5d80fd9181ad897889e1c1f701d61b7902c9` (SHA3-512 of `keys/release-pqc-pub.json#publicKey`) |
97
97
 
98
98
  To populate the maintainer SSH fingerprint:
99
99
  ```
@@ -139,3 +139,12 @@ node -e "
139
139
  framework's at-rest key material. Operators MUST rotate the
140
140
  passphrase via `b.vault.rotate` after any container image rebuild
141
141
  that changed a vendored crypto dependency.
142
+ - **Reviews are purchase-gated and operator-moderated.** A review can
143
+ only be submitted by a signed-in customer who has a completed order
144
+ for that product; the route confirms the purchase before accepting
145
+ and re-checks it on the POST, so a direct request can't plant a
146
+ review for a product the account never bought. Submissions land in a
147
+ `pending` state and never reach the storefront until an operator
148
+ publishes them through `/admin/reviews`. The author identity is
149
+ stored hash-only (`b.crypto.namespaceHash`) — the raw email is never
150
+ persisted.
package/lib/admin.js CHANGED
@@ -162,6 +162,7 @@ function mount(router, deps) {
162
162
  var r2 = deps.r2_bridge || null; // media-upload endpoint disabled when absent
163
163
  var assetPrefix = typeof deps.asset_prefix === "string" ? deps.asset_prefix : "/assets/";
164
164
  var catalogImport = deps.catalogImport || null; // bulk-import route disabled when absent
165
+ var reviews = deps.reviews || null; // moderation endpoints disabled when absent
165
166
 
166
167
  try { _b().audit.registerNamespace(AUDIT_NAMESPACE); } catch (_e) { /* idempotent */ }
167
168
 
@@ -529,6 +530,56 @@ function mount(router, deps) {
529
530
  }));
530
531
  }
531
532
 
533
+ // ---- reviews (moderation) -------------------------------------------
534
+
535
+ // Operator-side review moderation. The queue lists reviews across all
536
+ // products in one status (defaults to `pending`); publish / reject
537
+ // drive the same transitions the storefront submit path leaves in
538
+ // `pending`. Endpoints are omitted entirely when no reviews primitive
539
+ // is wired.
540
+ if (reviews) {
541
+ router.get("/admin/reviews", R(async function (req, res) {
542
+ var url = req.url ? new URL(req.url, "http://localhost") : null;
543
+ var status = (url && url.searchParams.get("status")) || "pending";
544
+ var cursor = url && url.searchParams.get("cursor");
545
+ var limitS = url && url.searchParams.get("limit");
546
+ var limit = limitS == null ? undefined : parseInt(limitS, 10);
547
+ var page = await reviews.listByStatus(status, { cursor: cursor || undefined, limit: limit });
548
+ _json(res, 200, page);
549
+ }));
550
+
551
+ router.get("/admin/reviews/:id", R(async function (req, res) {
552
+ var rev = await reviews.get(req.params.id);
553
+ if (!rev) return _problem(res, 404, "review-not-found");
554
+ _json(res, 200, rev);
555
+ }));
556
+
557
+ router.post("/admin/reviews/:id/publish", W("review.publish", async function (req, res) {
558
+ var rev;
559
+ try {
560
+ rev = await reviews.publish(req.params.id);
561
+ } catch (e) {
562
+ if (e && e.code === "REVIEW_NOT_FOUND") return _problem(res, 404, "review-not-found");
563
+ throw e;
564
+ }
565
+ _json(res, 200, rev);
566
+ return rev;
567
+ }));
568
+
569
+ router.post("/admin/reviews/:id/reject", W("review.reject", async function (req, res) {
570
+ var body = req.body || {};
571
+ var rev;
572
+ try {
573
+ rev = await reviews.reject(req.params.id, body.reason);
574
+ } catch (e) {
575
+ if (e && e.code === "REVIEW_NOT_FOUND") return _problem(res, 404, "review-not-found");
576
+ throw e;
577
+ }
578
+ _json(res, 200, rev);
579
+ return rev;
580
+ }));
581
+ }
582
+
532
583
  // ---- config ---------------------------------------------------------
533
584
 
534
585
  var config = deps.config || null;
package/lib/order.js CHANGED
@@ -368,6 +368,29 @@ function create(opts) {
368
368
  if (r.rowCount === 0) return null;
369
369
  return await this.get(orderId);
370
370
  },
371
+
372
+ // Has this customer purchased this product? True iff an order
373
+ // line for any variant of the product sits in an order owned by
374
+ // the customer whose status is a real purchase — anything except
375
+ // pending (never captured) or cancelled (reversed before
376
+ // fulfillment). paid|fulfilling|shipped|delivered|refunded all
377
+ // count as purchased. The review gate composes this so only
378
+ // verified buyers can leave a review. Existence check, one round
379
+ // trip.
380
+ hasPurchasedProduct: async function (customerId, productId) {
381
+ _uuid(customerId, "customer id");
382
+ _uuid(productId, "product id");
383
+ var rows = (await query(
384
+ "SELECT 1 FROM order_lines ol " +
385
+ "JOIN orders o ON o.id = ol.order_id " +
386
+ "JOIN variants v ON v.id = ol.variant_id " +
387
+ "WHERE o.customer_id = ?1 AND v.product_id = ?2 " +
388
+ "AND o.status NOT IN ('pending','cancelled') " +
389
+ "LIMIT 1",
390
+ [customerId, productId],
391
+ )).rows;
392
+ return rows.length > 0;
393
+ },
371
394
  };
372
395
  }
373
396
 
package/lib/reviews.js CHANGED
@@ -349,6 +349,35 @@ function create(opts) {
349
349
  return { rows: r.rows, next_cursor: _encodeNext(r.rows, REVIEW_ORDER_KEY, limit) };
350
350
  },
351
351
 
352
+ // Cross-product moderation listing — every review in one status
353
+ // regardless of product. The moderation queue needs all `pending`
354
+ // rows globally, which listForProduct (per-product) can't serve.
355
+ // status is required here (the queue is always status-scoped). Same
356
+ // (created_at DESC, id DESC) tuple cursor as listForProduct.
357
+ listByStatus: async function (status, listOpts) {
358
+ status = _statusFilter(status);
359
+ if (status === undefined) {
360
+ throw new TypeError("reviews.listByStatus: status is required (one of " + ALLOWED_STATUSES.join(", ") + ")");
361
+ }
362
+ listOpts = listOpts || {};
363
+ var limit = _limit(listOpts.limit);
364
+ var cursorVals = _decodeCursor(listOpts.cursor, REVIEW_ORDER_KEY, "listByStatus");
365
+
366
+ var sql, params;
367
+ if (cursorVals) {
368
+ sql = "SELECT * FROM reviews WHERE status = ?1 " +
369
+ "AND (created_at < ?2 OR (created_at = ?2 AND id < ?3)) " +
370
+ "ORDER BY created_at DESC, id DESC LIMIT ?4";
371
+ params = [status, cursorVals[0], cursorVals[1], limit];
372
+ } else {
373
+ sql = "SELECT * FROM reviews WHERE status = ?1 " +
374
+ "ORDER BY created_at DESC, id DESC LIMIT ?2";
375
+ params = [status, limit];
376
+ }
377
+ var r = await query(sql, params);
378
+ return { rows: r.rows, next_cursor: _encodeNext(r.rows, REVIEW_ORDER_KEY, limit) };
379
+ },
380
+
352
381
  // Aggregate ratings — published-only by construction; counts
353
382
  // every star bucket so the storefront can draw the distribution
354
383
  // bar chart without a second query.