@blamejs/blamejs-shop 0.3.55 → 0.3.57
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 +39 -2
- package/SECURITY.md +14 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/email.js +169 -0
- package/lib/storefront.js +368 -0
- package/lib/vendor/MANIFEST.json +84 -72
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/README.md +3 -3
- package/lib/vendor/blamejs/SECURITY.md +5 -0
- package/lib/vendor/blamejs/api-snapshot.json +15 -3
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +10 -4
- package/lib/vendor/blamejs/lib/ai-prompt.js +1 -1
- package/lib/vendor/blamejs/lib/app-shutdown.js +28 -0
- package/lib/vendor/blamejs/lib/archive-read.js +215 -16
- package/lib/vendor/blamejs/lib/archive.js +206 -52
- package/lib/vendor/blamejs/lib/auth/oauth.js +58 -0
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +84 -27
- package/lib/vendor/blamejs/lib/breach-deadline.js +166 -1
- package/lib/vendor/blamejs/lib/cloud-events.js +3 -1
- package/lib/vendor/blamejs/lib/codepoint-class.js +21 -0
- package/lib/vendor/blamejs/lib/db-schema.js +120 -3
- package/lib/vendor/blamejs/lib/db.js +10 -3
- package/lib/vendor/blamejs/lib/error-page.js +93 -9
- package/lib/vendor/blamejs/lib/external-db.js +164 -13
- package/lib/vendor/blamejs/lib/guard-email.js +36 -3
- package/lib/vendor/blamejs/lib/http-client.js +37 -7
- package/lib/vendor/blamejs/lib/mail-auth.js +554 -55
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +15 -5
- package/lib/vendor/blamejs/lib/mail-sieve.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +88 -19
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +58 -11
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +56 -4
- package/lib/vendor/blamejs/lib/middleware/attach-user.js +45 -10
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +70 -14
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +30 -2
- package/lib/vendor/blamejs/lib/middleware/deny-response.js +29 -9
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +56 -4
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +301 -14
- package/lib/vendor/blamejs/lib/openapi-paths-builder.js +105 -29
- package/lib/vendor/blamejs/lib/openapi.js +225 -100
- package/lib/vendor/blamejs/lib/problem-details.js +15 -3
- package/lib/vendor/blamejs/lib/queue-local.js +148 -38
- package/lib/vendor/blamejs/lib/queue.js +41 -11
- package/lib/vendor/blamejs/lib/render.js +21 -3
- package/lib/vendor/blamejs/lib/router.js +13 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +55 -0
- package/lib/vendor/blamejs/lib/sse.js +7 -5
- package/lib/vendor/blamejs/lib/static.js +46 -17
- package/lib/vendor/blamejs/lib/uri-template.js +3 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +57 -0
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +127 -0
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +61 -0
- package/lib/vendor/blamejs/test/00-primitives.js +151 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +201 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive.test.js +179 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asyncapi.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/attach-user-bearer-scheme.test.js +154 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-chunked-malformed.test.js +10 -8
- package/lib/vendor/blamejs/test/layer-0-primitives/body-parser-smuggling.test.js +99 -20
- package/lib/vendor/blamejs/test/layer-0-primitives/breach-deadline.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +50 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +63 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +107 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +145 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/deny-response.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +119 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +121 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-email.test.js +14 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +53 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +179 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/openapi.test.js +177 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +312 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +165 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sse.test.js +33 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +59 -0
- package/lib/wishlist-alerts.js +22 -0
- package/lib/wishlist-digest.js +18 -0
- 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.3.x
|
|
10
10
|
|
|
11
|
+
- v0.3.57 (2026-06-02) — **Vendored blamejs framework refreshed from v0.14.16 to v0.14.19.** The storefront runs on a vendored copy of the blamejs framework. This refreshes it across three upstream patch releases (v0.14.16 to v0.14.19), picking up framework fixes and hardening. The most operator-relevant is a PKCE-downgrade defense in the OAuth/OIDC client, which the storefront composes for Sign in with Google and Apple, so the federated sign-in flow inherits it. The remaining upstream changes are in framework areas the storefront does not expose (archive writing, SCIM, verifiable-credential issuance, SPF/DMARC tooling, OpenAPI), and the storefront's own behavior is unchanged — verified by the full test suite and the vendored-tree integrity gate against the new tree. **Changed:** *Updated the vendored framework to blamejs v0.14.19* — The vendored framework moves from v0.14.16 to v0.14.19 (three upstream patch releases of fixes and additive, opt-in changes). Notably, the OAuth/OIDC client now refuses an identity provider that advertises PKCE methods without S256 — a stripped-S256 downgrade defense — which hardens the storefront's Sign in with Google and Apple. The integrity manifest over the vendored tree was re-stamped as part of the refresh.
|
|
12
|
+
|
|
13
|
+
- v0.3.56 (2026-06-02) — **Opt-in wishlist alert and digest emails, passwordless magic-link sign-in, and a documented list of planned integrations.** Two opt-in capabilities are wired, and the operator docs now state plainly which integrations are planned-but-not-yet-available so nothing reads as silently missing. A customer can opt in to wishlist price-drop and back-in-stock alerts and a periodic wishlist digest; and a passwordless email magic-link offers an account sign-in path alongside passkeys and OAuth. Both stay inert until an operator configures email sending, so a store with no mailer behaves exactly as before. **Added:** *Opt-in wishlist alert and digest emails* — A customer can opt in, from their wishlist, to price-drop and back-in-stock alerts and to a periodic wishlist digest. Scheduled sweeps send them. The feature is off until an operator configures email sending and an email resolver; with no mailer it is a no-op. Digest emails are rendered field-by-field with output escaping. · *Passwordless magic-link sign-in* — When a mailer is configured, a customer can request a single-use, time-limited sign-in link by email, in addition to passkeys and OAuth. The request response is identical whether or not the email matches an account (no account enumeration), and the link establishes a sealed session on use. **Changed:** *Documented planned integrations* — The README and SECURITY docs now list the integrations that are intentionally not yet available — address validation, live chat, web-push notifications, in-console payment-key configuration, an age gate, and per-page promo banners — each with the condition under which it would be built, so operators know what is and isn't supported rather than discovering a gap.
|
|
14
|
+
|
|
11
15
|
- v0.3.55 (2026-06-02) — **Accessibility: field-level form-error messages and horizontally scrollable admin tables.** Two accessibility improvements. When a storefront form is rejected, the specific field at fault is now marked for assistive technology with an inline error message tied to that input, in addition to the existing summary at the top of the form. And wide tables in the admin console now scroll horizontally inside their own container instead of overflowing the page on a narrow or zoomed viewport. **Fixed:** *Field-level error messages on storefront forms* — When the address, review, product-question, or support-ticket form is rejected, the offending field is now marked aria-invalid with an inline error message associated to it (aria-describedby), so a screen-reader user is taken to the exact field to fix rather than only hearing a top-of-form summary. The server remains the only validator; this only changes how a rejected field is presented. Forms that fail on a whole-form condition continue to show the summary message. · *Admin tables reflow on narrow screens* — Wide data tables in the admin console (orders, products, inventory, and the rest) are now wrapped in a horizontal-scroll container, so columns stay reachable on a smaller laptop window, a split screen, or at high zoom instead of overflowing the layout. Column header semantics are preserved.
|
|
12
16
|
|
|
13
17
|
- v0.3.54 (2026-06-02) — **In-store pickup, saved payment methods, and gift wrapping at checkout.** Three more built-but-unreachable capabilities are now wired end to end. Operators can offer in-store pickup with managed pickup locations and a fulfilment queue; signed-in customers can save a card and reuse it at checkout; and operators can offer gift wrapping with a per-order gift message. The gift-wrap fee is added as a real cart line so it is included in the charged total. **Added:** *In-store pickup (click and collect)* — Operators can define pickup locations and work a pickup queue in the admin console (filtered by location), and shoppers can choose 'Pick up in store' at checkout. The order page and a new account pickups view show the pickup status. A not-found pickup or location returns a clear not-found rather than a generic error. · *Saved payment methods* — When Stripe is configured, a signed-in customer can save a card via a secure setup flow (loaded through a route-scoped policy and a same-origin script, with no inline script), manage their saved cards under their account, set a default, and reuse a saved card at checkout. A customer can only ever see or change their own saved cards. · *Gift wrapping at checkout* — Operators can define gift-wrap options (with a fee) in the admin console, and shoppers can add a wrap plus a gift message and recipient at checkout. The wrap fee is added as a real cart line, so it is included in the order total and the amount charged — never applied after the charge.
|
package/README.md
CHANGED
|
@@ -69,12 +69,12 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
|
|
|
69
69
|
| **`lib/payment.js`** | Payment adapters — **Stripe** (verify webhook HMAC-SHA256 via upstream `b.webhook.verify`, create / retrieve / confirm / cancel PaymentIntent, refund, register / list payment-method domains for Apple/Google Pay) and **PayPal** (`adapter: "paypal"` — OAuth2 client-credentials token, create / capture / get / refund Orders v2, webhook verify via PayPal's verify-webhook-signature API). No `stripe` / `paypal` npm dep — outbound through `b.httpClient` (SSRF-gated, retried, circuit-broken). |
|
|
70
70
|
| **`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`. |
|
|
71
71
|
| **`lib/checkout.js`** | Orchestrator. `quote()` returns priced quote; `confirm()` creates a Stripe PaymentIntent + persists order pending; `handleStripeEvent()` verifies webhook + fires the FSM transition. PayPal path: `createPaypalOrder()` opens a PayPal order + persists pending, `capturePaypalOrder()` captures → paid, `handlePaypalEvent()` is the webhook backstop. All idempotent on re-delivery. |
|
|
72
|
-
| **`lib/email.js`** | Transactional templates — order receipt, ship notification, refund confirmation
|
|
72
|
+
| **`lib/email.js`** | Transactional templates — order receipt, ship notification, refund confirmation, wishlist price-drop, abandoned-cart, review request, back-in-stock, **wishlist digest** (the periodic saved-items rollup, rendered per-line from the structured digest so every title / price is independently escaped), and **email magic-link sign-in**. Strict `{{var}}` renderer with HTML escape + refusal of unknown / unused placeholders. Composed on `b.mail` (DKIM/SPF/DMARC/BIMI upstream). |
|
|
73
73
|
| **`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; the typeface (Inter / Inter Tight) is self-hosted from `themes/default/assets/fonts`, so no page loads a cross-origin font. 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. |
|
|
74
74
|
| **`lib/customers.js`** | Customer accounts — passkey (WebAuthn) + **Sign in with Google / Apple** (OIDC). 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. `signInWithOIDC` keys federated accounts on the provider `(provider, subject)` and links an existing account only on a provider-verified email (never on an unverified one). `mintAppleClientSecret` produces Apple's required ES256 client-secret JWT from a Services-ID `.p8` key (the one classical signature the protocol mandates; the PQC default doesn't apply to an external IdP's wire format). Account routes (`/account/login`, `/account/register`, `/account`, `/account/login/google`, `/account/login/apple`) ship as designed cards on the storefront; signed-in customers manage their own passkeys (`/account/passkeys` — list, add another, confirm-gated revoke scoped to the account with a last-sign-in-method guard) and edit their profile (`/account/profile`). |
|
|
75
75
|
| **`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). |
|
|
76
76
|
| **`lib/product-qa.js`** | Customer questions and operator/customer answers per product, operator-moderated, distinct from the rating-based reviews. A signed-in shopper asks at `/products/:slug/question`; questions land `pending` and surface only after approval. Author identity is the customer id (verified against the customers primitive) or a hash-only email — the raw address is never stored. The product page renders approved questions with their approved answers (seller / customer / system badge, pinned 'top answer' first) in both the edge and container paths. `/admin/questions` is the moderation console: the cross-product queue (`listQuestionsByStatus`), and a per-question detail to approve / reject the question, post the seller answer (`submitAnswer`), approve / reject / pin answers. |
|
|
77
|
-
| **`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. |
|
|
77
|
+
| **`lib/wishlist.js`** + **`lib/wishlist-alerts.js`** + **`lib/wishlist-digest.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) and carries a per-customer opt-in panel for **sale + restock alerts** (price-drop / back-in-stock, event-driven) and the **periodic digest** (the saved-items rollup on a weekly / monthly schedule). `POST /wishlist/toggle` is idempotent (`INSERT OR IGNORE`) and redirects to the canonical product slug or a safe same-origin `return_to`. Both notification paths are off by default and require a configured mailer plus an email-address resolver to actually send (the customer store keeps only an email hash) — see *Optional integrations*. UUID-shape-validated ids, `b.pagination` HMAC cursors; prices rendered through `pricing.format` (locale + zero-decimal-currency correct). |
|
|
78
78
|
| **`lib/save-for-later.js`** | Per-customer cart holding list. Each cart line gets a login-gated "Save for later" control (`POST /cart/lines/:id/save` → `moveFromCart`); `/account/saved` lists items with Move-to-cart / Remove. `moveToCart` reprices to the current catalog price and stock-gates (out-of-stock + non-backorderable is refused). Composes `catalog.inventory` + `catalog.prices` + `catalog.variants`. |
|
|
79
79
|
| **`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. |
|
|
80
80
|
| **`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. |
|
|
@@ -198,12 +198,49 @@ variables. A signed-in operator can see the live on/off status of each at
|
|
|
198
198
|
| **Sign in with Google** | A *Continue with Google* button on `/account/login` (OIDC). | `GOOGLE_OAUTH_CLIENT_ID`, `GOOGLE_OAUTH_CLIENT_SECRET`, `SHOP_ORIGIN` (e.g. `https://shop.example.com`) | Create a Google Cloud **OAuth 2.0 Web** client; add `<SHOP_ORIGIN>/account/auth/google/callback` as an Authorized redirect URI; consent-screen scopes `openid email profile`. The button appears only when all three are set. |
|
|
199
199
|
| **Sign in with Apple** | A *Continue with Apple* button on `/account/login` (OIDC). | `APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_CLIENT_ID` (your **Services ID**), `APPLE_PRIVATE_KEY` (the `.p8` key contents), `SHOP_ORIGIN` | Needs an **Apple Developer Program** membership. Create a Services ID, enable Sign in with Apple, add `<SHOP_ORIGIN>/account/auth/apple/callback` as a Return URL, and create a Sign-in-with-Apple key (`.p8`). The shop mints Apple's ES256 client secret from the key at boot (re-minted each deploy, inside Apple's 6-month window). The button appears only when all five are set. |
|
|
200
200
|
| **PayPal checkout** | A native PayPal button on `/checkout` (PayPal Orders v2 — create / approve / capture), distinct from PayPal-through-Stripe. | `PAYPAL_CLIENT_ID`, `PAYPAL_SECRET` (a PayPal REST app), `PAYPAL_WEBHOOK_ID`, `PAYPAL_ENV` (`sandbox`\|`live`); Stripe checkout must also be live | The shop exchanges the OAuth2 token and creates / captures orders server-side; the button drives `/checkout/paypal/create` + `/checkout/paypal/capture`. Point a PayPal webhook at `/api/webhooks/paypal` (verified through PayPal's API). Allow `www.paypal.com` in your CSP `script-src` / `frame-src` (as you would `js.stripe.com`). |
|
|
201
|
+
| **Transactional email (SMTP)** | Order/ship/refund mail, abandoned-cart recovery, back-in-stock alerts, **wishlist sale + restock alerts and the periodic wishlist digest** (opt-in per customer on `/account/wishlist`), and **email magic-link sign-in** (a *Email me a sign-in link* option on `/account/login` for shoppers without a passkey or social login). | `SMTP_HOST`, `MAIL_FROM` (plus optional `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS`) | Without a mailer these surfaces stay inert — the wishlist crons scan nothing, the magic-link page reports email sign-in unavailable, and passkey / social login are unchanged. **Wishlist alerts + digests are additionally gated on an email-address resolver:** the customer store keeps only a salted email *hash* (never the plaintext), so out of the box there is no deliverable address and the crons send nothing even with SMTP set. They begin sending once you supply a resolver that maps a customer id to a deliverable address from your own plaintext-address store — the same hook abandoned-cart recovery uses. |
|
|
201
202
|
|
|
202
203
|
**Planned / not available:**
|
|
203
204
|
|
|
204
205
|
- **Shop Pay / "Sign in with Shop"** — **not available** to a self-hosted,
|
|
205
206
|
non-Shopify store: the credentials only issue from a Shopify Admin and payment
|
|
206
207
|
flows through Shopify Payments. There is no path to enable it here.
|
|
208
|
+
- **Address verification at checkout** — the address cache/record layer ships,
|
|
209
|
+
but no verification provider is bundled (verifying a postal address requires a
|
|
210
|
+
paid third-party lookup). *Re-opens* when you supply a provider adapter (USPS,
|
|
211
|
+
Smarty, Lob, Google, Melissa, …); the checkout address step then validates
|
|
212
|
+
against it. No provider, no behavior change.
|
|
213
|
+
- **Live chat** — a synchronous chat widget is not wired. The strict storefront
|
|
214
|
+
CSP (`script-src 'self'`, no inline script) means a chat island must be served
|
|
215
|
+
first-party from the container, and a realtime transport is a deployment-cost
|
|
216
|
+
decision. *Re-opens* on operator demand: a container-served polling island
|
|
217
|
+
(poll endpoint, admin queue, idle-session cleanup) is the minimal path;
|
|
218
|
+
WebSocket is a later upgrade.
|
|
219
|
+
- **Web push notifications** — the device registry + consent matrix + dispatcher
|
|
220
|
+
ship, and a PWA manifest + service worker are served, but no push subscription
|
|
221
|
+
endpoint or VAPID send is wired. *Re-opens* once you generate a VAPID keypair
|
|
222
|
+
(stored as a deployment secret, like the Stripe keys) and enable a subscribe
|
|
223
|
+
endpoint; a registered service worker is the prerequisite.
|
|
224
|
+
- **Payment keys are deployment secrets, by design** — Stripe / PayPal keys are
|
|
225
|
+
set as deployment secrets (`STRIPE_API_KEY`, …), **not** entered through the
|
|
226
|
+
admin console. A console wizard that wrote payment secrets into the database
|
|
227
|
+
would widen the blast radius (a database dump or a leaked bridge secret would
|
|
228
|
+
then expose payment keys; deployment secret bindings never sit in the
|
|
229
|
+
database). This is deliberate and is not planned to change for a single-store
|
|
230
|
+
deploy; a multi-tenant model with per-tenant operator-self-service keys would
|
|
231
|
+
be a separate build.
|
|
232
|
+
- **Age-restricted catalogs / age gate** — **not supported.** The catalog has no
|
|
233
|
+
age-restricted product class and the storefront enforces no age interstitial.
|
|
234
|
+
*Re-opens* as a real build (a `requires_age_check` product attribute + a
|
|
235
|
+
server-enforced edge + container interstitial) if an age-restricted category
|
|
236
|
+
enters the catalog.
|
|
237
|
+
- **Placement-specific promo banners** — the sitewide promo/notice strip is the
|
|
238
|
+
**announcement bar** (managed at `/admin/announcements`); it is the single
|
|
239
|
+
source for the top-of-page strip. Additional marketing *placements*
|
|
240
|
+
(homepage hero, PDP-side, cart-side, empty-search, footer) are a future
|
|
241
|
+
additive surface. *Re-opens* when you want placement-specific marketing beyond
|
|
242
|
+
the sitewide strip; the top strip stays the announcement bar's (running two
|
|
243
|
+
competing sitewide strips is avoided by design).
|
|
207
244
|
|
|
208
245
|
## Vendoring blamejs
|
|
209
246
|
|
package/SECURITY.md
CHANGED
|
@@ -198,6 +198,20 @@ node -e "
|
|
|
198
198
|
emails are stored hash-only (`b.crypto.namespaceHash`), and the
|
|
199
199
|
account leaderboard exposes rank plus initials only, never an email or
|
|
200
200
|
account id.
|
|
201
|
+
- **Email magic-link sign-in is single-use and hashed-at-rest.** The
|
|
202
|
+
optional *Email me a sign-in link* path (mounts only when a mailer is
|
|
203
|
+
configured) mints a 256-bit token whose plaintext is emailed once and
|
|
204
|
+
never persisted — only its `namespaceHash` digest is stored. Redemption
|
|
205
|
+
re-hashes the submitted token, compares in constant time, and flips the
|
|
206
|
+
row to `consumed` under a `status = 'issued'` guard, so a token works
|
|
207
|
+
exactly once (a replayed link bounces to login). Tokens carry a short
|
|
208
|
+
default lifetime (15 minutes) re-checked at redeem time, and a
|
|
209
|
+
background tick durably expires stale rows. The request form is not an
|
|
210
|
+
account-existence oracle: a known and an unknown address return the
|
|
211
|
+
identical "if an account exists we've emailed a link" confirmation, and
|
|
212
|
+
an unknown / expired / reused token bounces to login identically. On
|
|
213
|
+
success the sealed `shop_auth` session cookie is set the same way the
|
|
214
|
+
passkey and OAuth paths set it; passkey and social login are unchanged.
|
|
201
215
|
- **Privileged actions are recorded and reviewable.** Every mutating
|
|
202
216
|
admin action and every catalog-API error is appended to a
|
|
203
217
|
tamper-evident, hash-chained audit log (the framework's `b.audit`
|
package/lib/asset-manifest.json
CHANGED
package/lib/email.js
CHANGED
|
@@ -187,6 +187,75 @@ var ABANDONED_CART_TEXT_AFTER_LINES =
|
|
|
187
187
|
"{{notes}}\n\n" +
|
|
188
188
|
"Return to cart: {{cart_url}}\n";
|
|
189
189
|
|
|
190
|
+
// Wishlist digest — the periodic rollup of the customer's saved items
|
|
191
|
+
// (price + in/out-of-stock marker per line). The line set is rendered
|
|
192
|
+
// through the strict renderer once per line so each line's `title` /
|
|
193
|
+
// `price` / `stock` is HTML-escaped independently, then concatenated
|
|
194
|
+
// between the BEFORE/AFTER halves. The structured `lines[]` from
|
|
195
|
+
// composeDigest is the source — the pre-built `digest.html` is NEVER
|
|
196
|
+
// spliced in (that would double-handle escaping); rendering from the
|
|
197
|
+
// structured array keeps the escape-by-default path owning escaping
|
|
198
|
+
// end-to-end (the <script>-in-title XSS guard).
|
|
199
|
+
|
|
200
|
+
var WISHLIST_DIGEST_HTML_BEFORE_LINES =
|
|
201
|
+
"<!DOCTYPE html>\n" +
|
|
202
|
+
"<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Your wishlist update</title></head>" +
|
|
203
|
+
"<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
|
|
204
|
+
"<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
|
|
205
|
+
" <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">Your wishlist this period</h1>\n" +
|
|
206
|
+
" <p style=\"margin:0 0 16px;\">Here's where your saved items stand right now:</p>\n" +
|
|
207
|
+
" <ul style=\"padding-left:20px;margin:0 0 16px;color:#0d0d0d;\">\n";
|
|
208
|
+
|
|
209
|
+
var WISHLIST_DIGEST_HTML_LINE =
|
|
210
|
+
" <li><a href=\"{{product_url}}\" style=\"color:#fa4f09;\">{{title}}</a> — {{price}}{{stock}}</li>\n";
|
|
211
|
+
|
|
212
|
+
var WISHLIST_DIGEST_HTML_EMPTY =
|
|
213
|
+
" <li>No items in your wishlist right now.</li>\n";
|
|
214
|
+
|
|
215
|
+
var WISHLIST_DIGEST_HTML_AFTER_LINES =
|
|
216
|
+
" </ul>\n" +
|
|
217
|
+
" <p style=\"margin:16px 0;color:#0d0d0d;font-size:13px;\">You're receiving this because you opted into the wishlist digest.</p>\n" +
|
|
218
|
+
"</div>\n" +
|
|
219
|
+
"</body></html>\n";
|
|
220
|
+
|
|
221
|
+
var WISHLIST_DIGEST_TEXT_BEFORE_LINES =
|
|
222
|
+
"Your wishlist this period\n=========================\n\n" +
|
|
223
|
+
"Here's where your saved items stand right now:\n\n";
|
|
224
|
+
|
|
225
|
+
var WISHLIST_DIGEST_TEXT_LINE =
|
|
226
|
+
" {{title}} — {{price}}{{stock}}\n {{product_url}}\n";
|
|
227
|
+
|
|
228
|
+
var WISHLIST_DIGEST_TEXT_EMPTY =
|
|
229
|
+
" No items in your wishlist right now.\n";
|
|
230
|
+
|
|
231
|
+
var WISHLIST_DIGEST_TEXT_AFTER_LINES =
|
|
232
|
+
"\nYou're receiving this because you opted into the wishlist digest.\n";
|
|
233
|
+
|
|
234
|
+
// Magic-link sign-in — a single-use, time-limited link a customer
|
|
235
|
+
// follows to sign in without a passkey or password. The `link_url`
|
|
236
|
+
// carries the plaintext portal token; it's HTML-escaped through the
|
|
237
|
+
// strict renderer like every other value (the URL is built server-side
|
|
238
|
+
// but escape-by-default still applies).
|
|
239
|
+
|
|
240
|
+
var MAGIC_LINK_HTML =
|
|
241
|
+
"<!DOCTYPE html>\n" +
|
|
242
|
+
"<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Your sign-in link</title></head>" +
|
|
243
|
+
"<body style=\"margin:0;background:#ffffff;color:#0d0d0d;font-family:system-ui,sans-serif;\">\n" +
|
|
244
|
+
"<div style=\"max-width:560px;margin:0 auto;padding:24px;\">\n" +
|
|
245
|
+
" <h1 style=\"color:#0d0d0d;margin:0 0 12px;\">Sign in to your account</h1>\n" +
|
|
246
|
+
" <p style=\"margin:0 0 16px;\">Use the button below to sign in. This link works once and expires shortly.</p>\n" +
|
|
247
|
+
" <p style=\"margin:24px 0;\"><a href=\"{{link_url}}\" style=\"background:#fa4f09;color:#ffffff;padding:12px 20px;text-decoration:none;display:inline-block;font-weight:bold;\">Sign in</a></p>\n" +
|
|
248
|
+
" <p style=\"margin:0;color:#0d0d0d;font-size:13px;\">If the button doesn't work, paste this link into your browser: {{link_url}}</p>\n" +
|
|
249
|
+
" <p style=\"margin:16px 0 0;color:#0d0d0d;font-size:13px;\">If you didn't request this, you can ignore this email — no one can sign in without the link.</p>\n" +
|
|
250
|
+
"</div>\n" +
|
|
251
|
+
"</body></html>\n";
|
|
252
|
+
|
|
253
|
+
var MAGIC_LINK_TEXT =
|
|
254
|
+
"Sign in to your account\n\n" +
|
|
255
|
+
"Use this single-use link to sign in (it expires shortly):\n" +
|
|
256
|
+
"{{link_url}}\n\n" +
|
|
257
|
+
"If you didn't request this, you can ignore this email.\n";
|
|
258
|
+
|
|
190
259
|
// Review request — sent ~7 days after ship. Per-product review
|
|
191
260
|
// links use the same before/lines/after split as the abandoned
|
|
192
261
|
// cart so the strict renderer keeps its escape-everything property.
|
|
@@ -443,6 +512,96 @@ function create(opts) {
|
|
|
443
512
|
);
|
|
444
513
|
},
|
|
445
514
|
|
|
515
|
+
// Wishlist digest — the periodic rollup of the customer's saved
|
|
516
|
+
// items. The caller (the wishlistDigest dispatcher) passes the
|
|
517
|
+
// structured `lines[]` composeDigest built; this renders each line
|
|
518
|
+
// through the strict renderer once so every `title` / `price` is
|
|
519
|
+
// HTML-escaped independently, then concatenates between the
|
|
520
|
+
// BEFORE/AFTER halves. The pre-built `digest.html` / `digest.text`
|
|
521
|
+
// are accepted by the caller's shape but NEVER spliced in raw —
|
|
522
|
+
// rendering from the structured array keeps the escape-by-default
|
|
523
|
+
// path owning escaping end-to-end (the <script>-in-title XSS guard).
|
|
524
|
+
// `lines` MAY be empty (the "no items" digest); validation is
|
|
525
|
+
// entry-point tier (throw TypeError so the cron caller's try/catch
|
|
526
|
+
// drops the row silently).
|
|
527
|
+
sendWishlistDigest: async function (input) {
|
|
528
|
+
if (!input) throw new TypeError("email.sendWishlistDigest: input object required");
|
|
529
|
+
if (typeof input.customer_email !== "string" || !input.customer_email) {
|
|
530
|
+
throw new TypeError("email.sendWishlistDigest: customer_email required");
|
|
531
|
+
}
|
|
532
|
+
if (!Array.isArray(input.lines)) {
|
|
533
|
+
throw new TypeError("email.sendWishlistDigest: lines array required (may be empty)");
|
|
534
|
+
}
|
|
535
|
+
var i;
|
|
536
|
+
for (i = 0; i < input.lines.length; i += 1) {
|
|
537
|
+
var ln = input.lines[i];
|
|
538
|
+
if (!ln || typeof ln !== "object") {
|
|
539
|
+
throw new TypeError("email.sendWishlistDigest: lines[" + i + "] must be an object");
|
|
540
|
+
}
|
|
541
|
+
if (typeof ln.title !== "string" || !ln.title) {
|
|
542
|
+
throw new TypeError("email.sendWishlistDigest: lines[" + i + "].title required");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
var htmlLines = "";
|
|
546
|
+
var textLines = "";
|
|
547
|
+
if (input.lines.length === 0) {
|
|
548
|
+
htmlLines = WISHLIST_DIGEST_HTML_EMPTY;
|
|
549
|
+
textLines = WISHLIST_DIGEST_TEXT_EMPTY;
|
|
550
|
+
} else {
|
|
551
|
+
for (i = 0; i < input.lines.length; i += 1) {
|
|
552
|
+
var line = input.lines[i];
|
|
553
|
+
// The in/out-of-stock marker is derived here (not trusted from
|
|
554
|
+
// the caller as raw HTML) so it rides the strict renderer like
|
|
555
|
+
// every other field.
|
|
556
|
+
var stockMarker = line.in_stock === true ? " (in stock)"
|
|
557
|
+
: line.in_stock === false ? " (out of stock)"
|
|
558
|
+
: "";
|
|
559
|
+
var lineVars = {
|
|
560
|
+
title: line.title,
|
|
561
|
+
price: line.price == null ? "—" : String(line.price),
|
|
562
|
+
stock: stockMarker,
|
|
563
|
+
product_url: typeof line.product_url === "string" && line.product_url
|
|
564
|
+
? line.product_url
|
|
565
|
+
: "#",
|
|
566
|
+
};
|
|
567
|
+
htmlLines += _render(WISHLIST_DIGEST_HTML_LINE, lineVars);
|
|
568
|
+
textLines += _render(WISHLIST_DIGEST_TEXT_LINE, lineVars);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
var html =
|
|
572
|
+
WISHLIST_DIGEST_HTML_BEFORE_LINES +
|
|
573
|
+
htmlLines +
|
|
574
|
+
WISHLIST_DIGEST_HTML_AFTER_LINES;
|
|
575
|
+
var text =
|
|
576
|
+
WISHLIST_DIGEST_TEXT_BEFORE_LINES +
|
|
577
|
+
textLines +
|
|
578
|
+
WISHLIST_DIGEST_TEXT_AFTER_LINES;
|
|
579
|
+
return await _send(
|
|
580
|
+
input.customer_email,
|
|
581
|
+
"Your wishlist update",
|
|
582
|
+
html, text, input.replyTo
|
|
583
|
+
);
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
// Magic-link sign-in — emails a single-use portal link. The
|
|
587
|
+
// `link_url` carries the plaintext token; it's escaped through the
|
|
588
|
+
// strict renderer like every other value. Entry-point tier: throw
|
|
589
|
+
// TypeError on a bad shape so the caller's try/catch keeps the
|
|
590
|
+
// surface enumeration-safe (the generic confirmation shows either way).
|
|
591
|
+
sendMagicLink: async function (input) {
|
|
592
|
+
if (!input) throw new TypeError("email.sendMagicLink: input object required");
|
|
593
|
+
if (typeof input.customer_email !== "string" || !input.customer_email) {
|
|
594
|
+
throw new TypeError("email.sendMagicLink: customer_email required");
|
|
595
|
+
}
|
|
596
|
+
if (typeof input.link_url !== "string" || !input.link_url) {
|
|
597
|
+
throw new TypeError("email.sendMagicLink: link_url required");
|
|
598
|
+
}
|
|
599
|
+
var vars = { link_url: input.link_url };
|
|
600
|
+
var html = _render(MAGIC_LINK_HTML, vars);
|
|
601
|
+
var text = _render(MAGIC_LINK_TEXT, vars);
|
|
602
|
+
return await _send(input.customer_email, "Your sign-in link", html, text, input.replyTo);
|
|
603
|
+
},
|
|
604
|
+
|
|
446
605
|
// Review request — sent ~7 days after ship. Per-product review
|
|
447
606
|
// links are derived from review_base_url + "/" + slug + "/review",
|
|
448
607
|
// then HTML-escaped through the strict renderer with the rest of
|
|
@@ -575,6 +734,16 @@ module.exports = {
|
|
|
575
734
|
ABANDONED_CART_TEXT_BEFORE_LINES: ABANDONED_CART_TEXT_BEFORE_LINES,
|
|
576
735
|
ABANDONED_CART_TEXT_LINE: ABANDONED_CART_TEXT_LINE,
|
|
577
736
|
ABANDONED_CART_TEXT_AFTER_LINES: ABANDONED_CART_TEXT_AFTER_LINES,
|
|
737
|
+
WISHLIST_DIGEST_HTML_BEFORE_LINES: WISHLIST_DIGEST_HTML_BEFORE_LINES,
|
|
738
|
+
WISHLIST_DIGEST_HTML_LINE: WISHLIST_DIGEST_HTML_LINE,
|
|
739
|
+
WISHLIST_DIGEST_HTML_EMPTY: WISHLIST_DIGEST_HTML_EMPTY,
|
|
740
|
+
WISHLIST_DIGEST_HTML_AFTER_LINES: WISHLIST_DIGEST_HTML_AFTER_LINES,
|
|
741
|
+
WISHLIST_DIGEST_TEXT_BEFORE_LINES: WISHLIST_DIGEST_TEXT_BEFORE_LINES,
|
|
742
|
+
WISHLIST_DIGEST_TEXT_LINE: WISHLIST_DIGEST_TEXT_LINE,
|
|
743
|
+
WISHLIST_DIGEST_TEXT_EMPTY: WISHLIST_DIGEST_TEXT_EMPTY,
|
|
744
|
+
WISHLIST_DIGEST_TEXT_AFTER_LINES: WISHLIST_DIGEST_TEXT_AFTER_LINES,
|
|
745
|
+
MAGIC_LINK_HTML: MAGIC_LINK_HTML,
|
|
746
|
+
MAGIC_LINK_TEXT: MAGIC_LINK_TEXT,
|
|
578
747
|
REVIEW_REQUEST_HTML_BEFORE_LINES: REVIEW_REQUEST_HTML_BEFORE_LINES,
|
|
579
748
|
REVIEW_REQUEST_HTML_LINE: REVIEW_REQUEST_HTML_LINE,
|
|
580
749
|
REVIEW_REQUEST_HTML_AFTER_LINES: REVIEW_REQUEST_HTML_AFTER_LINES,
|