@blamejs/blamejs-shop 0.0.47 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +45 -4
- package/lib/storefront.js +50 -2
- 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.49 (2026-05-22) — **README refresh — scoped install command, live-demo URL, refreshed primitive table + migration list.** The README hadn't been touched since v0.0.43; the install snippet still pointed at the unscoped `blamejs-shop` name and the primitives table listed only the original commerce surface. Operators landing on the GitHub repo now see the correct `npm install @blamejs/blamejs-shop`, the live demo URL, badges for npm + license + SLSA L3, and a primitives table that includes `customers`, `subscriptions`, `newsletter`, and `theme` alongside the original commerce stack. **Changed:** *README — install snippet uses `@blamejs/blamejs-shop`* — Replaces the unscoped clone-and-smoke recipe with `npm install @blamejs/blamejs-shop` + the `require()` shape that resolves the framework + adapters. The local clone path stays as the secondary recipe for operators who want to develop against the repo. Three badges land in the header — npm version, Apache-2.0 license, SLSA Level 3. · *README — primitive table extended* — Adds rows for `customers` (passkey-only, email hash-only), `subscriptions` (Stripe-backed recurring), `newsletter` (signup primitive shipped in v0.0.41), and `theme` (file-backed templates). The `storefront` row gains the full surface description — utility bar, sticky header, dark hero with code-preview card, marquee, featured-product callout, collections grid, framework feature band, designed catalog grid, newsletter band, and the four-column footer. `admin` row mentions the subscription-plan routes. · *README — migration list updated* — Lists migrations 0001-0010 (catalog, cart, order, shop_config, webhooks, customers, inventory_thresholds, subscriptions, newsletter_signups) so operators don't have to grep the directory to know what schema lands. Adds the demo-seed recipe (`scripts/seed-sample-products.sql` + `scripts/seed-sample-product-media.sql`) for one-shot population of the live demo's four products + their SVG hero images. · *Live-demo URL pinned* — `https://blamejs-shop.coocoo.workers.dev/` lands in the README header so operators evaluating the framework can see a real deployed instance rendering the storefront design system end-to-end before committing to wiring their own Cloudflare account.
|
|
12
|
+
|
|
13
|
+
- v0.0.48 (2026-05-22) — **Home page — featured-product callout between the primitives marquee and the collections grid.** The home page rhythm went marquee → 6-tile collections grid → framework band, so a single product never got more than the four-tile catalog card's surface area to introduce itself. This release inserts a split callout (square hero image on the left, copy column with `Featured` eyebrow + title + description + accent-coloured price + `View product →` CTA on the right) between the marquee and the collections grid. The render picks the first product with attached media; operators wrapping `renderHome` can override the selection via `opts.featured`. **Added:** *`.featured-product` section* — Sits between the primitives marquee and the collections grid. Two-column grid at desktop (square hero + copy column), stacks to image-above-copy at <64rem with a 16/10 image aspect ratio. Title is a `clamp(1.75rem, 3vw, 2.5rem)` headline; price renders in the display font at `--text-2xl` with the accent colour + tabular-nums. Image is wrapped in an anchor to the PDP and has a hover-scale of `1.03` driven through the existing `--duration-slow` ease curve. The card itself has a hairline border on `var(--paper)` so it reads as a content tile, not a hero band — keeps the dark hero above and the framework band below as the page's main rhythm changes. · *`storefront.renderHome({ ..., featured? })` selection override* — When `opts.featured` is supplied the renderer uses it verbatim (shape: `{ title, description, price, slug, image_url, image_alt }`). When absent, the renderer scans the active-products list for the first row with `image_url` and uses that as the featured pick. Products without media are skipped; when no product has media the callout renders as an empty string and the page rhythm falls back to marquee → collections directly. **Changed:** *Product data shape — `description` carried through* — The renderHome `products.map(...)` projection now includes `description` (from the catalog products row) so the featured callout's lede can pull product-specific copy. Existing callers that didn't pass a description continue to render — the callout falls back to a stock 'Server-rendered, PQC-secured, shipped from origin' line.
|
|
14
|
+
|
|
11
15
|
- v0.0.47 (2026-05-22) — **Account dashboard — 4-stat row + recent-orders table with per-order product thumbnails.** The dashboard surfaced a bare three-column orders table (order id, status, total). This release adds a 4-stat row above the body (orders / lifetime spend / member since / passkeys) and a new 'Items' column on the orders table that renders the first four product thumbnails per order with a `+N` overflow chip when there are more. Visitors land on a richer surface that summarises their relationship with the shop instead of just listing opaque order ids. **Added:** *Stats row — orders, lifetime spend, member since, passkeys* — Four-up grid above the orders table. `Orders` is the count of returned orders (capped at 10 by the listForCustomer page; future patch could surface the true count via a `count()` shape on the order primitive). `Lifetime spend` sums `grand_total_minor` across orders in the dominant currency (falls back to '—' if multiple currencies appear, since silently mis-summing them is worse than declining to compute). `Member since` reads `customer.created_at` when present, otherwise the earliest order's `created_at` — both as an ISO date. `Passkeys` calls `deps.customers.listPasskeys(customer.id)` (drop-silent on `TypeError` if the primitive build doesn't expose the listing surface yet). · *`Items` column — per-order product thumbnails* — Per-order, the route walks `order.lines`, collapses to unique variant ids, and resolves each through cached `catalog.variants.get` → `catalog.products.get` + `catalog.media.listForProduct`. The variant cache is held across orders so a customer with multiple orders containing the same SKU only hits the catalog once. The table cell surfaces the first four thumbs (`.account-order__thumb`, 2.5rem rounded tiles); orders with more than four show a `+N` overflow chip. Status renders as the same `.pdp__badge`-style pill the order page uses — green-bordered `--ok` variant for `completed` / `shipped` / `delivered`. **Changed:** *`storefront.renderAccount({ customer, orders, order_product_lookup?, passkey_count?, asset_prefix? })`* — New optional opts. `order_product_lookup` is `{ order_id: [{ product, hero_media }, ...] }` — the route bundles it in. `passkey_count` is the integer count of enrolled passkeys (0 when the customers primitive can't enumerate them on this build). `asset_prefix` defaults to `/assets/` for image URL composition. All three are optional — calling with just `customer` + `orders` continues to render cleanly, the stat row just shows `0` / `—` for the missing fields.
|
|
12
16
|
|
|
13
17
|
- v0.0.46 (2026-05-22) — **Order confirmation page — line items render thumbnails + product titles (same pattern as the cart).** The post-checkout `/orders/:order_id` page used the read-only `CART_LINE` template that emitted just SKU + qty + unit + total. The order confirmation now reuses the same product-cell treatment v0.0.45 shipped on the live cart — small thumbnail (from `catalog.media.listForProduct`) + product title + SKU chip in a slug-linked anchor — so the page a customer lands on after a successful purchase shows what they bought visually, not just a column of opaque SKU codes. **Changed:** *`CART_LINE` (read-only, used by the order page) — same product cell as the editable cart line* — Markup mirrors `CART_LINE_EDITABLE` minus the qty / update / remove forms. First cell is `<a class="cart-line__product-link" href="/products/<slug>">` wrapping the thumbnail + title + SKU chip; remaining cells (qty / unit / total) are unchanged. Reuses the existing `.cart-line__thumb` + `.cart-line__product-*` CSS — no new theme rules. · *`storefront.renderOrder({ ..., product_lookup })` accepts the variant→product map* — Mirrors `renderCart`'s contract. The route handler bundles a `{ variant_id: { product, hero_media } }` cache built per-unique-variant; lines whose variant is missing fall through to SKU-as-title + the placeholder tile. The order page table header is now 'Product' instead of 'SKU'. · *`GET /orders/:order_id` walks the order's frozen lines once* — After loading the order, the route walks `o.lines` and for each unique `variant_id` pulls `catalog.variants.get` → `catalog.products.get` + `catalog.media.listForProduct`. The cache prevents a multi-quantity-same-variant order from hitting the catalog more than once per variant. The lookup is passed to `renderOrder` so the line rendering matches the cart's pattern byte-for-byte.
|
package/README.md
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
# blamejs.shop
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@blamejs/blamejs-shop)
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
[](https://slsa.dev)
|
|
8
|
+
|
|
9
|
+
Open-source ecommerce framework built on [blamejs](https://github.com/blamejs/blamejs). Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.
|
|
10
|
+
|
|
11
|
+
Live demo: **https://blamejs-shop.coocoo.workers.dev/**
|
|
4
12
|
|
|
5
13
|
## Requirements
|
|
6
14
|
|
|
7
15
|
- Node.js LTS (>= 24.14.1)
|
|
16
|
+
- For a deployable shop: a Cloudflare account (Workers, Containers, D1, R2, KV, Durable Objects). Local development works without it via `node:sqlite`.
|
|
8
17
|
|
|
9
18
|
## Install
|
|
10
19
|
|
|
20
|
+
```bash
|
|
21
|
+
npm install @blamejs/blamejs-shop
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
var bShop = require("@blamejs/blamejs-shop");
|
|
26
|
+
var b = bShop.framework; // the vendored blamejs
|
|
27
|
+
var d1 = bShop.externaldbD1; // Cloudflare D1 adapter
|
|
28
|
+
var cat = bShop.catalog.create({ query }); // catalog primitive
|
|
11
29
|
```
|
|
30
|
+
|
|
31
|
+
Or clone for development:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
12
34
|
git clone https://github.com/blamejs/blamejs.shop.git
|
|
13
35
|
cd blamejs.shop
|
|
14
36
|
bash scripts/vendor-update.sh blamejs latest
|
|
@@ -39,19 +61,38 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
|
|
|
39
61
|
| **`lib/order.js`** | FSM-driven post-checkout record via upstream `b.fsm`. States: pending → paid → fulfilling → shipped → delivered (+ refunded / cancelled). Every transition appends to `order_transitions`. |
|
|
40
62
|
| **`lib/checkout.js`** | Orchestrator. `quote()` returns priced quote; `confirm()` creates PaymentIntent + persists order in pending; `handleStripeEvent()` verifies webhook + fires the FSM transition (idempotent on re-delivery). |
|
|
41
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). |
|
|
42
|
-
| **`lib/storefront.js`** | Server-rendered HTML —
|
|
43
|
-
| **`lib/
|
|
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
|
+
| **`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/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
|
+
| **`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. |
|
|
44
69
|
| **`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
|
+
| **`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. |
|
|
45
71
|
|
|
46
72
|
### Migrations applied to D1
|
|
47
73
|
|
|
48
74
|
- `migrations-d1/0001_catalog.sql` — products, variants, prices, inventory, media
|
|
49
75
|
- `migrations-d1/0002_cart.sql` — carts, cart_lines (partial-unique active-cart-per-session)
|
|
50
76
|
- `migrations-d1/0003_order.sql` — orders, order_lines, order_transitions (FSM audit log)
|
|
77
|
+
- `migrations-d1/0004_shop_config.sql` — shop_config (operator-tunable runtime config)
|
|
78
|
+
- `migrations-d1/0005_webhooks.sql` — webhooks subscriptions + deliveries (signed fan-out)
|
|
79
|
+
- `migrations-d1/0006_customers.sql` — customers + passkey_credentials
|
|
80
|
+
- `migrations-d1/0008_inventory_thresholds.sql` — low-stock alert thresholds + alerts
|
|
81
|
+
- `migrations-d1/0009_subscriptions.sql` — subscription_plans + subscriptions (Stripe-mirrored)
|
|
82
|
+
- `migrations-d1/0010_newsletter_signups.sql` — email signups with hash-based dedup
|
|
83
|
+
|
|
84
|
+
### Demo seed
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
wrangler d1 execute blamejs-shop --remote --file=scripts/seed-sample-products.sql
|
|
88
|
+
wrangler d1 execute blamejs-shop --remote --file=scripts/seed-sample-product-media.sql
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Lands four demo products (Operator Tee, Edge Reader v1, Operator License, Starter Bundle) with brand-coloured SVG hero images under `scripts/sample-product-images/`. Idempotent — re-running is a no-op via `INSERT OR IGNORE`.
|
|
51
92
|
|
|
52
93
|
### Tests
|
|
53
94
|
|
|
54
|
-
|
|
95
|
+
19+ layer-1 test suites running against in-memory `node:sqlite` loaded from the live migrations. Layer-2 integration test boots the full HTTP server on a 127.0.0.1 ephemeral port and exercises the complete browse → PDP → add-to-cart → cart-update → cart-remove flow. Schema CHECK / UNIQUE / FK constraints exercised end-to-end.
|
|
55
96
|
|
|
56
97
|
## Operator quick-start
|
|
57
98
|
|
package/lib/storefront.js
CHANGED
|
@@ -318,6 +318,8 @@ var HOME_HERO =
|
|
|
318
318
|
" </div>\n" +
|
|
319
319
|
"</section>\n" +
|
|
320
320
|
"\n" +
|
|
321
|
+
"RAW_FEATURED_CALLOUT\n" +
|
|
322
|
+
"\n" +
|
|
321
323
|
"<section class=\"collections\" aria-labelledby=\"collections-title\">\n" +
|
|
322
324
|
" <header class=\"section-head\">\n" +
|
|
323
325
|
" <p class=\"eyebrow\">Featured collections</p>\n" +
|
|
@@ -453,7 +455,14 @@ function renderHome(opts) {
|
|
|
453
455
|
// products render the text-only PRODUCT_CARD fallback below.
|
|
454
456
|
var imageUrl = p.hero_media ? assetPrefix + p.hero_media.r2_key : null;
|
|
455
457
|
var imageAlt = p.hero_media ? (p.hero_media.alt_text || p.title) : null;
|
|
456
|
-
return {
|
|
458
|
+
return {
|
|
459
|
+
title: p.title,
|
|
460
|
+
description: p.description || "",
|
|
461
|
+
price: priceStr,
|
|
462
|
+
slug: p.slug,
|
|
463
|
+
image_url: imageUrl,
|
|
464
|
+
image_alt: imageAlt,
|
|
465
|
+
};
|
|
457
466
|
});
|
|
458
467
|
if (opts.theme) {
|
|
459
468
|
return opts.theme.render("home", {
|
|
@@ -473,7 +482,46 @@ function renderHome(opts) {
|
|
|
473
482
|
// actual catalog size, not a stale hardcoded number. Falls back
|
|
474
483
|
// to a typographic em-dash when the catalog hasn't been seeded.
|
|
475
484
|
var heroProductCount = products.length === 0 ? "—" : String(products.length);
|
|
476
|
-
|
|
485
|
+
|
|
486
|
+
// Featured-product callout — pick the first product that has
|
|
487
|
+
// attached media. Surfaces a single product in a wider treatment
|
|
488
|
+
// than the dense 6-tile collections grid below. Operators that
|
|
489
|
+
// want a different selection rule (top-seller, newest, manually
|
|
490
|
+
// pinned) wrap renderHome and override `opts.featured`.
|
|
491
|
+
function _esc(s) {
|
|
492
|
+
return String(s == null ? "" : s)
|
|
493
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
494
|
+
}
|
|
495
|
+
var featuredProduct = null;
|
|
496
|
+
if (opts.featured) {
|
|
497
|
+
featuredProduct = opts.featured;
|
|
498
|
+
} else {
|
|
499
|
+
for (var fi = 0; fi < products.length; fi += 1) {
|
|
500
|
+
if (products[fi].image_url) { featuredProduct = products[fi]; break; }
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
var featuredHtml = "";
|
|
504
|
+
if (featuredProduct) {
|
|
505
|
+
var fpDesc = featuredProduct.description || "Server-rendered, PQC-secured, shipped from origin. Composed on the vendored blamejs framework.";
|
|
506
|
+
featuredHtml =
|
|
507
|
+
"<section class=\"featured-product\" aria-labelledby=\"featured-title\">\n" +
|
|
508
|
+
" <div class=\"featured-product__inner\">\n" +
|
|
509
|
+
" <a class=\"featured-product__media\" href=\"/products/" + _esc(featuredProduct.slug) + "\">\n" +
|
|
510
|
+
" <img src=\"" + _esc(featuredProduct.image_url) + "\" alt=\"" + _esc(featuredProduct.image_alt || featuredProduct.title) + "\" loading=\"lazy\">\n" +
|
|
511
|
+
" </a>\n" +
|
|
512
|
+
" <div class=\"featured-product__copy\">\n" +
|
|
513
|
+
" <p class=\"eyebrow\">Featured</p>\n" +
|
|
514
|
+
" <h2 id=\"featured-title\" class=\"featured-product__title\">" + _esc(featuredProduct.title) + "</h2>\n" +
|
|
515
|
+
" <p class=\"featured-product__lede\">" + _esc(fpDesc) + "</p>\n" +
|
|
516
|
+
" <p class=\"featured-product__price\">" + _esc(featuredProduct.price) + "</p>\n" +
|
|
517
|
+
" <a class=\"btn-primary\" href=\"/products/" + _esc(featuredProduct.slug) + "\">View product <span aria-hidden=\"true\">→</span></a>\n" +
|
|
518
|
+
" </div>\n" +
|
|
519
|
+
" </div>\n" +
|
|
520
|
+
"</section>";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
var hero = _render(HOME_HERO, { product_count: heroProductCount })
|
|
524
|
+
.replace("RAW_FEATURED_CALLOUT", featuredHtml);
|
|
477
525
|
// The hero + value band + catalog section give the home page a
|
|
478
526
|
// designed surface even when no products are loaded yet —
|
|
479
527
|
// visitors land on the storefront shell, not a tech demo.
|
package/package.json
CHANGED