@blamejs/blamejs-shop 0.1.19 → 0.1.22

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 (34) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +8 -2
  3. package/SECURITY.md +12 -0
  4. package/lib/admin.js +944 -1
  5. package/lib/checkout.js +192 -22
  6. package/lib/collections.js +4 -3
  7. package/lib/customers.js +102 -0
  8. package/lib/giftcards.js +40 -0
  9. package/lib/order.js +22 -0
  10. package/lib/storefront.js +462 -70
  11. package/lib/vendor/MANIFEST.json +3 -3
  12. package/lib/vendor/blamejs/CHANGELOG.md +10 -0
  13. package/lib/vendor/blamejs/README.md +4 -0
  14. package/lib/vendor/blamejs/api-snapshot.json +129 -2
  15. package/lib/vendor/blamejs/index.js +12 -0
  16. package/lib/vendor/blamejs/lib/json-merge-patch.js +93 -0
  17. package/lib/vendor/blamejs/lib/json-patch.js +206 -0
  18. package/lib/vendor/blamejs/lib/json-path.js +638 -0
  19. package/lib/vendor/blamejs/lib/json-pointer.js +109 -0
  20. package/lib/vendor/blamejs/lib/jtd.js +234 -0
  21. package/lib/vendor/blamejs/lib/link-header.js +169 -0
  22. package/lib/vendor/blamejs/package.json +1 -1
  23. package/lib/vendor/blamejs/release-notes/v0.12.57.json +18 -0
  24. package/lib/vendor/blamejs/release-notes/v0.12.58.json +22 -0
  25. package/lib/vendor/blamejs/release-notes/v0.12.60.json +18 -0
  26. package/lib/vendor/blamejs/release-notes/v0.12.61.json +18 -0
  27. package/lib/vendor/blamejs/release-notes/v0.12.62.json +18 -0
  28. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +18 -0
  29. package/lib/vendor/blamejs/test/layer-0-primitives/json-merge-patch.test.js +81 -0
  30. package/lib/vendor/blamejs/test/layer-0-primitives/json-patch.test.js +184 -0
  31. package/lib/vendor/blamejs/test/layer-0-primitives/json-path.test.js +113 -0
  32. package/lib/vendor/blamejs/test/layer-0-primitives/jtd.test.js +52 -0
  33. package/lib/vendor/blamejs/test/layer-0-primitives/link-header.test.js +96 -0
  34. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.1.x
10
10
 
11
+ - v0.1.22 (2026-05-25) — **Fix the container boot crash-loop, plus gift cards, subscription self-management, and a customers roster.** The deployable container could crash-loop on startup, taking every write route (add-to-cart, checkout, account, admin) down while edge-rendered reads kept working — this release fixes that boot path. It also adds gift cards (issue / balance / redeem-at-checkout with an operator ledger), customer subscription self-management at `/account/subscriptions`, and a read-only customers roster in the admin console. **Added:** *Gift cards — redeem at checkout, balance check, operator ledger* — A gift-card code can be applied at checkout as a credit against the order total: the amount due drops by the available balance (never below zero), the order still records the full total it owed, and the debit is recorded once per order behind an atomic `balance >= amount` guard so concurrent spends can't overdraw — a card that fully covers the order is marked paid with no card charge. Customers check a balance at `GET /gift-cards` (which is not a code-existence oracle — unknown / malformed / expired all return the same generic result). Operators work `/admin/gift-cards`: list issued cards (masked code, original + remaining balance, status, issued date) filtered by lifecycle status, issue a card (the bearer code shown once, right after creation), and open a card to read its full credit / debit / expire ledger. Codes are stored as a `namespaceHash` digest plus a 4-char hint — never in the clear. · *Customer subscription self-management* — Signed-in customers view and cancel their own subscriptions at `/account/subscriptions` — each row shows the plan summary, status, and renew / end date, with a Cancel control on cancelable subscriptions. Every action is ownership-checked: a subscription that doesn't belong to the signed-in customer is a 404, never another account's row. Cancellation runs through the payment provider, so the cancel control mounts only when the payment handle is wired; without it the list renders read-only. Customer-facing subscription creation — a subscription-checkout flow — remains a separate follow-up; this surface is for managing existing subscriptions. · *Customers admin roster* — `/admin/customers` renders a read-only account roster (newest first): display name, a short id, the join date, the sign-in method (passkey count plus any linked OAuth providers), and the order count, with cursor pagination in the browser and a JSON list (with `next_cursor`) for a bearer-token client. Account creation and changes stay with the storefront's passkey / OIDC ceremonies. Order counts and sign-in methods are resolved with bounded aggregate queries over each page's ids — no per-row database trips. Raw email is never stored, so it is never shown. **Changed:** *Console nav gains Customers and Gift cards* — The signed-in admin nav now includes Customers and Gift cards alongside the existing screens, each shown when its primitive is wired. All new endpoints content-negotiate like the rest of the console: a bearer-token client gets the JSON API, a signed-in browser gets HTML; a request without the bearer token returns the sign-in form on a GET and redirects on a write. The customers roster, the subscription cancel, and the gift-card balance page all degrade gracefully when an optional dependency (payment, or the primitive itself) isn't configured. · *Deploy now syncs theme assets to R2* — `npm run deploy` chains `wrangler deploy` with a new `scripts/sync-r2-assets.js` that uploads the repo's theme stylesheets to the R2 bucket the Worker serves `/assets/*` from. `wrangler deploy` alone ships the Worker + container but never touches R2, so a CSS change would silently drift from the deployed HTML — new markup against a stale stylesheet. Run `npm run sync-assets` to push the stylesheets without a full redeploy. · *Theme stylesheet and island scripts served with Subresource Integrity* — The default theme's stylesheet `<link>` and the externalized passkey island `<script>` tags now carry a `sha384` `integrity` attribute computed from the on-disk asset, so a browser refuses any `/assets/*` response whose bytes don't match — closing the window where a tampered or stale CDN object could execute against the page. The digests are computed once at boot and cached; a missing or unreadable asset degrades to no integrity attribute rather than blocking the render. **Fixed:** *Passkey sign-in / registration no longer blocked by CSP* — The WebAuthn passkey ceremonies on `/account/login` and `/account/register` were inline `<script>` blocks, which the storefront's strict `script-src 'self'` CSP refuses to execute — so on the deploy the sign-in and registration buttons did nothing (accounts are passkey-only, so this blocked all customer auth). The ceremonies now ship as external assets (`/assets/themes/default/js/passkey-login.js` + `passkey-register.js`) that `'self'` allows, served from R2 and uploaded by the deploy's asset sync. Same effect, CSP-clean — and a clearer split between the server-rendered form and its opt-in enhancement. · *Container no longer crash-loops on startup* — The container boot had three ways to fail before it could serve a request, each of which left edge-rendered reads working while every container-served route (add-to-cart, cart update / remove, checkout, `/account`, `/admin`) was unreachable. (1) The secure-default boot unlocks a wrapped vault **and** a wrapped audit-signing keypair, which the framework reads from `BLAMEJS_VAULT_PASSPHRASE` / `BLAMEJS_AUDIT_SIGNING_PASSPHRASE`, but the deploy contract documents a single `VAULT_PASSPHRASE` the framework never read — so the container had no passphrase source, no TTY to prompt, and exited. The entry point now bridges the documented secret onto the vault passphrase and derives a domain-separated audit-signing passphrase from it. (2) Encrypted-at-rest requires a tmpfs; the image now points it at `/dev/shm` rather than failing to find one. (3) The Docker liveness probe was `wget`, which bot-guard blocks as automation (403), so a healthy container was marked unhealthy and stopped — the probe is now a browser-shaped Node request (`scripts/healthcheck.js`) that passes bot-guard like real traffic, without exempting the health path from the security middleware. A boot gate (`test/layer-2-integration/server-boot.test.js`) spawns the real entry point and asserts it listens, answers the probe, and still blocks header-less clients. Operators must redeploy the container to pick this up.
12
+
13
+ - v0.1.21 (2026-05-25) — **Admin console — a collections screen.** Collections join the admin console. `/admin/collections` lists manual and smart product collections, creates either kind, edits title / description / sort strategy, archives one, manages a manual collection's members (add / remove / reorder), and builds a smart collection's rules with a live preview of the products they match — all from a signed-in browser. As with the other console screens, the same paths serve the existing JSON API to a bearer-token client unchanged. **Added:** *Collections management screen* — `/admin/collections` lists the shop's collections (title, slug, type, status, member or match count) with an all / active / archived filter; `/admin/collections/:slug` opens one. For a manual collection it manages members — add a product by id, remove, and reorder. For a smart collection it edits the rule set (field / operator / value rows over the supported fields and operators) and renders a live preview of the products the rules currently match. A form creates either kind; editing updates the title, description, and sort strategy (and rules for smart collections); and a collection can be archived. A bad-shape submit re-renders with the validator's message rather than a 500, and an unknown slug or product is a no-op notice. **Changed:** *Console nav gains Collections* — The signed-in admin nav now includes Collections, shown when the collections primitive is wired. The list, detail, create, edit, archive, and member endpoints content-negotiate like the other screens: a bearer-token client gets the JSON API, a signed-in browser gets HTML. A request without the bearer token returns the sign-in form on a GET and redirects on a write.
14
+
15
+ - v0.1.20 (2026-05-25) — **A "Customers also bought" rail on the order confirmation.** The order confirmation page now shows a recommendation rail. After a purchase, `/orders/:id` surfaces up to four products drawn from operator-curated pins first, then co-purchase signals from the order's own items (what other shoppers bought alongside them), then category-popular and in-stock fallbacks — always excluding what was just bought. The rail is best-effort: if the engine isn't wired it simply doesn't render. **Added:** *Order-confirmation recommendation rail* — The post-purchase page renders a "Customers also bought" rail beneath the order summary, anchored on the order's items. Picks come from the recommendations engine: operator-curated overrides first, then a co-purchase signal (products bought in the same orders), then category-popular and in-stock fallbacks to fill the rail; the order's own products are always excluded. Each pick reuses the storefront product-card markup (image, title, price) and links to the product page. The rail renders in both the default and file-backed theme layouts; a store without the recommendations primitive wired renders no rail (and a read failure degrades to none rather than erroring the confirmation page).
16
+
11
17
  - v0.1.19 (2026-05-25) — **Admin console — a webhooks screen, and order events now fan out.** Webhooks join the admin console. `/admin/webhooks` registers an endpoint (with a one-time signing-secret reveal), lists endpoints, enables / disables and deletes one, and opens an endpoint's delivery feed to retry a failed delivery. Order lifecycle transitions now fan out to registered endpoints — the order primitive is wired to the webhook dispatcher so a paid / fulfilled / shipped / delivered / cancelled / refunded transition produces a signed delivery. **Added:** *Webhooks management screen* — `/admin/webhooks` registers an outbound endpoint — an https:// URL plus the events to subscribe (or all) — and shows the HMAC-SHA3-512 signing secret once on creation; the secret is never rendered in the endpoint list afterward. The list shows each endpoint's URL, events, status, and per-minute rate, with actions to enable / disable, delete, and open its deliveries. The delivery feed lists each attempt (event, status, response code, attempt count, last error, time) and retries a failed delivery. The JSON API a bearer-token client already used — create / list / update / delete / deliveries / retry — is unchanged; the browser screen content-negotiates on the same paths. · *Order events fan out to webhooks* — The order primitive is now wired to the webhook dispatcher, so an order transition (`order.mark_paid`, `order.start_fulfillment`, `order.mark_shipped`, `order.mark_delivered`, `order.cancel`, `order.refund`) produces a signed delivery to every subscribed endpoint. Dispatch is post-persist — a delivery failure can never roll back the transition that landed — and a failed delivery is recorded for retry from the console rather than surfaced to the request that triggered it. **Changed:** *Console nav gains Webhooks* — The signed-in admin nav now includes Webhooks, shown when the webhooks primitive is wired. Endpoint create, list, enable / disable, delete, the delivery feed, and retry content-negotiate like the other console screens: a bearer-token client gets the JSON API, a signed-in browser gets HTML. A request without the bearer token returns the sign-in form on a GET and redirects on a write. An https-only URL or empty event set re-renders the form with the validator's message, and an unknown endpoint or delivery is a no-op notice rather than a 500.
12
18
 
13
19
  - v0.1.18 (2026-05-25) — **Admin console — a subscription-plans screen.** Subscription plans join the admin console. `/admin/subscription-plans` lists the recurring-offer catalog — price, interval, trial, and Stripe price id — with an active / archived filter, creates a plan from a signed-in browser, and archives one. As with the other console screens, the same path serves the existing JSON API to a bearer-token client unchanged. **Added:** *Subscription-plans management screen* — `/admin/subscription-plans` renders the plan catalog (price and interval, trial length, Stripe price id, linked variant, status) with an active / archived filter, when opened in a signed-in browser; the same path serves the JSON list to a bearer-token client. A form creates a plan — Stripe price id, interval, interval count, currency, amount, trial days, and an optional variant link — and each active row archives in one submit. A bad-shape create re-renders the form with the validator's message rather than a 500, and archiving an unknown plan is a no-op notice. Archiving is terminal from the console: because a plan mirrors a Stripe price id that can go stale, a retired plan is re-offered by creating a new one against a fresh price id. **Changed:** *Console nav gains Subscriptions* — The signed-in admin nav now includes Subscriptions alongside Products, Inventory, Orders, Returns, and Reviews, shown when the subscriptions primitive is wired. The `/admin/subscription-plans` list, create, and archive endpoints content-negotiate like the other screens: a bearer-token client gets the JSON API, a signed-in browser gets HTML. A request without the bearer token returns the sign-in form on a GET and redirects on a write.
package/README.md CHANGED
@@ -69,10 +69,13 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
69
69
  | **`lib/addresses.js`** | Per-customer address book at `/account/addresses` — add / edit / set default shipping or billing / remove. One-default-per-role invariant (promoting clears the prior). Every by-id route confirms the address belongs to the signed-in customer before acting (a guessed id returns 404). `b.guardUuid` ids, 2-char ISO country. |
70
70
  | **`lib/returns.js`** | Self-serve RMAs. Customer requests a return against their own order at `/account/orders/:id/return` (items + reason, ownership-checked, lines built from the order's own records) and tracks status at `/account/returns`. Operators work `/admin/returns` — approve (refund amount) / mark received / refund / reject — over the pending → approved → received → refunded FSM; illegal transitions are 409, bad ids 404. |
71
71
  | **`lib/recently-viewed.js`** | Signed-in customer browse history. A product-page visit records the view server-side against the customer's account (drop-silent — never blocks the page); `/account/recently-viewed` lists them newest-first as a grid with a Clear-history control. De-duped + capped per customer, archived products drop out, login-gated. Guest/session history is opt-in (a client beacon) and not shipped — the lib's `forSession` + `merge` support it. |
72
+ | **`lib/recommendations.js`** | Product-recommendation engine. Operator-curated override pins first (`setOverride` — "when viewing A, show B", kind-scoped + weight-ordered), then a signal-based fallback: co-purchase (products bought in the same orders), category-popular, and in-stock-random filler. `recommendForProduct` / `recommendForCart` / `recommendForCustomer` / `recommendForCategory` each return renderable picks (active + in-stock, source product excluded). The order confirmation page (`/orders/:id`) renders a "Customers also bought" rail from it — best-effort, anchored on the order's items, excluding what was just bought. |
72
73
  | **`lib/collections.js`** | Curated + smart product groupings. `GET /collections` lists the shop's active collections; `GET /collections/:slug` renders the grid — manual collections list hand-picked members, smart collections evaluate stored rules against the active catalog and apply the collection's sort strategy. Each product resolves fresh, so archived products drop out. Public, no sign-in; a bad or unknown slug is a 404 (never a 500). Linked from the footer on every page. |
73
- | **`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. |
74
+ | **`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. Customers view + cancel their own subscriptions at `/account/subscriptions` (ownership-checked; cancel mounts when the payment handle is wired). |
75
+ | **`lib/giftcards.js`** | Prepaid bearer gift cards. `issue({ amount_minor, currency })` generates a 16-char code (32-glyph alphabet, no ambiguous letters) via `b.crypto.generateBytes`, stores only its `namespaceHash` digest + a 4-char hint, and returns the plaintext code once. `balance(code)` / `lookup(code)` resolve a code to its live balance (constant-time hash compare); `redeem({ code, order_id, amount_minor })` decrements the balance with an atomic `balance >= amount` SQL guard so concurrent spends can't overdraw. Redeemed at checkout as a credit against the order grand total: the amount due drops by the applied balance (never below zero), the order still records the full total it owed, and the debit is recorded once per order — a card that fully covers the order is marked paid with no Stripe charge. Customers check a balance at `GET /gift-cards`; the page is not a code-existence oracle (unknown / malformed / expired all return the same generic not-found). |
76
+ | **`lib/gift-card-ledger.js`** | Append-only credit / debit / expire history per gift card, with a denormalized `balance_after_minor` snapshot for O(1) balance reads. `credit` / `debit` / `expire` write one row each; `history(id)` paginates a card's transactions; `transactionsForOrder(id)` lists a card's movements for one order. The audit trail behind the admin gift-card ledger console; overdraft is refused at the primitive layer. |
74
77
  | **`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. |
75
- | **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans + review moderation + return moderation. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. Also serves a **browser admin console**: sign in at `/admin` by pasting the API key (sealed `shop_admin` session cookie, SameSite=Strict, /admin-scoped), with a persistent nav across every signed-in page. A guided **setup wizard** at `/admin/setup` writes shop identity to config; **Products** (`/admin/products`) browses the catalog and creates / archives / restores; **Inventory** (`/admin/inventory`) lists stock per SKU (on-hand / held / available) with a low-stock filter, restocks, sets per-SKU thresholds, and tracks new SKUs; **Orders** (`/admin/orders`) lists recent orders with status filters, opens an order's items, totals, and shipping address, and drives the lifecycle (mark paid → fulfil → ship → deliver, cancel — Refund goes through the payment provider) through the order FSM; **Returns** (`/admin/returns`) is the RMA moderation queue — filter by status, open a request's items and reason, and approve (with refund amount) → mark received → refund, or reject with a reason, over the return FSM; **Reviews** (`/admin/reviews`) is the review moderation queue — filter by status and publish, reject (with a reason), or take down each submission inline; **Subscriptions** (`/admin/subscription-plans`) is the recurring-offer catalog — filter active / archived, create a plan (Stripe price id, interval, amount, trial), and archive one, with archiving terminal because the mirrored Stripe price can go stale; **Webhooks** (`/admin/webhooks`) registers outbound endpoints (https:// only) with a one-time signing-secret reveal, enables / disables / deletes them, and opens an endpoint's delivery feed to retry a failed delivery — the signing secret is shown once on create and never in the list, and order transitions fan out signed deliveries to subscribed endpoints. The Returns, Reviews, Subscriptions, and Webhooks links appear only when those primitives are wired. Each console path content-negotiates: a bearer-token client still gets the JSON API unchanged, a signed-in browser gets HTML. Reachable by the cookie or the bearer token. |
78
+ | **`lib/admin.js`** | Bearer-token-gated CRUD over catalog + orders + refunds + bulk CSV import + subscription plans + review moderation + return moderation. Token compared via `b.crypto.timingSafeEqual`. Errors as RFC 9457 problem documents via `b.problemDetails`. Audit emission on every mutation. Also serves a **browser admin console**: sign in at `/admin` by pasting the API key (sealed `shop_admin` session cookie, SameSite=Strict, /admin-scoped), with a persistent nav across every signed-in page. A guided **setup wizard** at `/admin/setup` writes shop identity to config; **Products** (`/admin/products`) browses the catalog and creates / archives / restores; **Inventory** (`/admin/inventory`) lists stock per SKU (on-hand / held / available) with a low-stock filter, restocks, sets per-SKU thresholds, and tracks new SKUs; **Orders** (`/admin/orders`) lists recent orders with status filters, opens an order's items, totals, and shipping address, and drives the lifecycle (mark paid → fulfil → ship → deliver, cancel — Refund goes through the payment provider) through the order FSM; **Customers** (`/admin/customers`) is a read-only roster, newest first — display name, short id, join date, sign-in method (passkey count + linked OAuth providers), and order count, with the count and sign-in methods resolved by bounded aggregate queries so a page of customers costs no per-row trips (email addresses aren't stored in the clear, so they're not shown); **Returns** (`/admin/returns`) is the RMA moderation queue — filter by status, open a request's items and reason, and approve (with refund amount) → mark received → refund, or reject with a reason, over the return FSM; **Reviews** (`/admin/reviews`) is the review moderation queue — filter by status and publish, reject (with a reason), or take down each submission inline; **Subscriptions** (`/admin/subscription-plans`) is the recurring-offer catalog — filter active / archived, create a plan (Stripe price id, interval, amount, trial), and archive one, with archiving terminal because the mirrored Stripe price can go stale; **Collections** (`/admin/collections`) manages manual + smart product collections — filter active / archived, create a collection (manual or smart with a starter rule), and per collection edit title / description / sort strategy, manage manual members (add by product id, remove, reorder) or edit a smart collection's rule set with a live preview of the products the rules currently match, and archive; **Gift cards** (`/admin/gift-cards`) is the gift-card ledger — list issued cards (masked code, original + remaining balance, status, issued date) filtered by lifecycle status, issue a new card (the bearer code shown once, right after creation), and open a card to see its full credit / debit / expire ledger; **Webhooks** (`/admin/webhooks`) registers outbound endpoints (https:// only) with a one-time signing-secret reveal, enables / disables / deletes them, and opens an endpoint's delivery feed to retry a failed delivery — the signing secret is shown once on create and never in the list, and order transitions fan out signed deliveries to subscribed endpoints. The Customers, Returns, Reviews, Subscriptions, Collections, Gift cards, and Webhooks links appear only when those primitives are wired. Each console path content-negotiates: a bearer-token client still gets the JSON API unchanged, a signed-in browser gets HTML. Reachable by the cookie or the bearer token. |
76
79
  | **`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. |
77
80
  | **`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. |
78
81
 
@@ -86,6 +89,8 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
86
89
  - `migrations-d1/0006_customers.sql` — customers + passkey_credentials
87
90
  - `migrations-d1/0008_inventory_thresholds.sql` — low-stock alert thresholds + alerts
88
91
  - `migrations-d1/0009_subscriptions.sql` — subscription_plans + subscriptions (Stripe-mirrored)
92
+ - `migrations-d1/0013_giftcards.sql` — gift cards (hashed bearer code + balance snapshot) + redemptions
93
+ - `migrations-d1/0081_gift_card_ledger.sql` — append-only gift-card credit/debit/expire ledger
89
94
  - `migrations-d1/0010_newsletter_signups.sql` — email signups with hash-based dedup
90
95
  - `migrations-d1/0011_reviews.sql` — operator-moderated product reviews (hash-only author identity)
91
96
  - `migrations-d1/0012_wishlist.sql` — per-customer saved products (unique customer + product + variant)
@@ -96,6 +101,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
96
101
  - `migrations-d1/0206_orders_email_hash.sql` — queryable buyer-email hash on orders (guest-order reconciliation key)
97
102
  - `migrations-d1/0043_collections.sql` — manual + smart product collections (members + rules + sort strategy)
98
103
  - `migrations-d1/0050_recently_viewed.sql` — per-customer / per-session product browse history (dedup + per-subject cap)
104
+ - `migrations-d1/0105_recommendations.sql` — operator-curated recommendation overrides (kind-scoped, weight-ordered)
99
105
 
100
106
  ### Demo seed
101
107
 
package/SECURITY.md CHANGED
@@ -148,3 +148,15 @@ node -e "
148
148
  publishes them through `/admin/reviews`. The author identity is
149
149
  stored hash-only (`b.crypto.namespaceHash`) — the raw email is never
150
150
  persisted.
151
+ - **Gift-card codes are bearer secrets, stored hash-only.** A gift
152
+ card's plaintext code is shown exactly once at issuance and never
153
+ persisted — only its `namespaceHash` digest plus a 4-character hint
154
+ for operator support lookups. Balance, lookup, and redeem hash the
155
+ submitted code and compare in constant time, so the store can't leak
156
+ a code even from a full database dump. The customer balance page
157
+ (`/gift-cards`) is not a code-existence oracle: unknown, malformed,
158
+ expired, voided, and fully-redeemed codes all return the same generic
159
+ "couldn't find a balance" so it can't be used to probe valid codes.
160
+ Redemption decrements with an atomic `balance >= amount` SQL guard
161
+ keyed on the order id, so concurrent or replayed checkouts can never
162
+ overdraw a card or apply more than the remaining balance.