@blamejs/blamejs-shop 0.1.4 → 0.1.6

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.1.x
10
10
 
11
+ - v0.1.6 (2026-05-25) — **Claim past guest orders when a shopper signs in.** A shopper who checked out as a guest and later signs in with a provider-verified email now finds those past orders attached to their account. Checkout records a hash of the buyer's email on the order; on Google sign-in, orders placed under that same email — and not yet owned by anyone — are claimed into the account. Linking happens only on an email the identity provider verified, never on an unverified one, so it can't be used to steal another shopper's order history. **Added:** *Guest-order reconciliation on sign-in* — Orders now carry a `customer_email_hash` (migration 0206), written at checkout from the buyer's email with the same key the customers table uses. On a verified Google sign-in, `order.linkGuestOrdersByEmailHash` claims every ownerless order under that email into the account — so prior guest purchases appear in /account and satisfy customer-scoped checks (e.g. verified-buyer reviews). Only ownerless orders are touched (an order already attached to a customer is never reassigned), and only a provider-verified email triggers a link.
12
+
13
+ - v0.1.5 (2026-05-25) — **Tell operators how to turn each integration on.** Every third-party integration (Stripe card checkout, Apple/Google Pay, Sign in with Google) is off by default and only activates when you supply its credentials. This release documents exactly what to set for each — in the README, in a new .env.example, and in the admin console itself. A signed-in operator opens /admin/integrations to see, at a glance, which integrations are live and the precise environment variables (or one-time action) needed to enable the rest. Nothing is enabled without your keys. **Added:** *Admin integrations status page* — `/admin/integrations` lists each integration with a live Enabled / Not configured status and the exact variables or action to turn it on — Stripe keys, the payment-method-domain registration for wallets, the Google OAuth client + redirect URI. Read-only; secrets are never rendered. Linked from the admin landing. · *Operator setup docs* — A README "Optional integrations" section and a top-level `.env.example` enumerate every environment variable, what capability it unlocks, and the external setup (e.g. the Google OAuth redirect URI, the Stripe webhook path). Both note that Apple sign-in + PayPal are planned and that Shop Pay / "Sign in with Shop" isn't available to a self-hosted store.
14
+
11
15
  - v0.1.4 (2026-05-25) — **Sign in with Google.** Customers can sign in with Google alongside passkeys. The account login page gains a Continue with Google button; the OIDC authorization-code flow (PKCE, state, nonce, ID-token verification) runs through the framework's OAuth adapter, and the verified identity becomes a shop session. Accounts are keyed on the provider's stable subject, and an existing account is only ever linked on an email the provider has verified — an unverified email that collides with an existing account is refused rather than linked. A cart built before signing in is adopted into the account, so checkout attaches the order to the customer. **Added:** *Google sign-in* — Mounts `/account/login/google` + `/account/auth/google/callback` when the operator sets `GOOGLE_OAUTH_CLIENT_ID`, `GOOGLE_OAUTH_CLIENT_SECRET`, and `SHOP_ORIGIN`. The in-flight state (CSRF state + nonce + PKCE verifier) rides a sealed, /account-scoped, SameSite=Lax cookie; the callback verifies the state before exchanging the code. A forged or stale callback is dropped to the login page. On success the guest session cart is adopted into the account (`cart.setCustomer`), matching the passkey path. · *Federated identity model + safe account linking* — `customers.signInWithOIDC` resolves a verified sign-in to a customer: an existing `(provider, subject)` link, else — only on a provider-verified email — an existing account with that email, else a new account. It never links to an existing account on an unverified email (account-takeover defense). New `customer_oauth_identities` table (migration 0205) + `customers.byOAuthIdentity`.
12
16
 
13
17
  - v0.1.2 (2026-05-25) — **Apple Pay and Google Pay express checkout.** The pay page now offers one-tap wallet checkout. Stripe's Express Checkout Element renders Apple Pay and Google Pay buttons above the card form on eligible devices, confirming the same PaymentIntent as the card path — so the webhook and order flow are unchanged. To turn the wallets on, the operator registers the shop's web domain with Stripe once via the admin API; Stripe performs Apple merchant validation and hosts the domain-association file, so no Apple Developer account is needed. **Added:** *Wallet buttons on the pay page* — The Stripe Express Checkout Element mounts above the card form and auto-renders Apple Pay / Google Pay (and Link) when the device and the shop's registered domain make them available. It stays hidden until Stripe reports an available wallet, and confirms the existing per-order PaymentIntent — the payment-completion path (webhook → order FSM) is identical to the card flow. · *Payment-method domain registration* — `POST /admin/payment-method-domains` (with `{ "domain_name": "shop.example.com" }`) registers a domain with Stripe to enable the wallets; `GET /admin/payment-method-domains` lists registered domains and their per-method status. The payment adapter gains `registerPaymentMethodDomain` + `listPaymentMethodDomains`. Apex, www, and each subdomain register separately; a live-mode registration also covers sandbox.
package/README.md CHANGED
@@ -93,6 +93,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
93
93
  - `migrations-d1/0026_customer_addresses.sql` — per-customer address book (default shipping/billing flags)
94
94
  - `migrations-d1/0023_returns.sql` — return authorizations + lines (RMA lifecycle FSM)
95
95
  - `migrations-d1/0205_customer_oauth_identities.sql` — federated sign-in identities (provider + subject, verified-email gating)
96
+ - `migrations-d1/0206_orders_email_hash.sql` — queryable buyer-email hash on orders (guest-order reconciliation key)
96
97
  - `migrations-d1/0043_collections.sql` — manual + smart product collections (members + rules + sort strategy)
97
98
  - `migrations-d1/0050_recently_viewed.sql` — per-customer / per-session product browse history (dedup + per-subject cap)
98
99
 
@@ -146,6 +147,32 @@ curl -X POST https://your-shop.example.com/admin/products \
146
147
 
147
148
  See [`docs/deploy-cloudflare.md`](docs/deploy-cloudflare.md) for the full deploy recipe.
148
149
 
150
+ ## Optional integrations — what to set to enable each
151
+
152
+ Every third-party integration is **off by default** and lights up only when you
153
+ supply its credentials. Nothing here phones home or is enabled without your
154
+ keys; the storefront runs fully (browse, cart, accounts) with none of them. Set
155
+ the values as deployment secrets (`wrangler secret put …`) or environment
156
+ variables. A signed-in operator can see the live on/off status of each at
157
+ **`/admin/integrations`**. See [`.env.example`](.env.example) for the full list.
158
+
159
+ | Integration | What it enables | Set this | Notes |
160
+ |-------------|-----------------|----------|-------|
161
+ | **Admin console** | The bearer-token JSON API + the `/admin` browser console (sign-in, setup wizard, dashboard). | `ADMIN_API_KEY` (≥ 16 chars — use 32 random bytes) | Sign in at `/admin` by pasting the key. Without it the admin surface doesn't mount. |
162
+ | **Card checkout (Stripe)** | Checkout + the Payment Element on the pay page; refunds; subscription billing. | `STRIPE_API_KEY` (`sk_…`), `STRIPE_WEBHOOK_SECRET` (`whsec_…`), `STRIPE_PUBLISHABLE_KEY` (`pk_…`) | Point your Stripe webhook at `/api/webhooks/stripe`. Without these the shop stays browsable but checkout doesn't mount. |
163
+ | **Apple Pay & Google Pay** | One-tap wallet buttons (Express Checkout Element) on the pay page. | Stripe (above) **+** register each web domain: `POST /admin/payment-method-domains {"domain_name":"shop.example.com"}` | Stripe performs Apple merchant validation and hosts the association file — **no Apple Developer account needed**. Apex, `www`, and each subdomain register separately; a live-mode registration also covers sandbox. |
164
+ | **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. |
165
+
166
+ **Planned / not available:**
167
+
168
+ - **Sign in with Apple** — the flow is wired in the framework, but it needs an
169
+ Apple Developer Program membership ($99/yr) and an ES256 client-secret minted
170
+ from your `.p8` key. Config-optional; shipping behind those credentials.
171
+ - **PayPal** — a separate adapter (Orders v2 + its own webhook); planned.
172
+ - **Shop Pay / "Sign in with Shop"** — **not available** to a self-hosted,
173
+ non-Shopify store: the credentials only issue from a Shopify Admin and payment
174
+ flows through Shopify Payments. There is no path to enable it here.
175
+
149
176
  ## Vendoring blamejs
150
177
 
151
178
  `blamejs.shop` vendors blamejs as a shallow git clone of the release tag
package/lib/admin.js CHANGED
@@ -1037,6 +1037,19 @@ function mount(router, deps) {
1037
1037
  _redirect(res, "/admin");
1038
1038
  });
1039
1039
 
1040
+ // Integrations status — what's live + what to set to enable the rest.
1041
+ // `deps.integrations` is the live on/off map computed at the entry
1042
+ // point from the environment (admin.js never reads process.env).
1043
+ router.get("/admin/integrations", async function (req, res) {
1044
+ if (!_htmlAuthed(req, expectedToken)) {
1045
+ return _sendHtml(res, 200, renderAdminLogin({ shop_name: deps.shop_name }));
1046
+ }
1047
+ _sendHtml(res, 200, renderAdminIntegrations({
1048
+ shop_name: deps.shop_name,
1049
+ status: deps.integrations || {},
1050
+ }));
1051
+ });
1052
+
1040
1053
  if (config) {
1041
1054
  router.get("/admin/setup", async function (req, res) {
1042
1055
  if (!_htmlAuthed(req, expectedToken)) return _redirect(res, "/admin");
@@ -1365,6 +1378,7 @@ function renderAdminLanding(opts) {
1365
1378
  "<h2>Admin</h2>" +
1366
1379
  "<div class=\"nav-cards\">" +
1367
1380
  "<a class=\"nav-card\" href=\"/admin/setup\"><h3>Setup wizard</h3><p>Shop identity, currency, and contact details.</p></a>" +
1381
+ "<a class=\"nav-card\" href=\"/admin/integrations\"><h3>Integrations</h3><p>Payments, wallets, and sign-in — what's live and what to set.</p></a>" +
1368
1382
  "<a class=\"nav-card\" href=\"/admin/dashboard\"><h3>Dashboard</h3><p>Sales, revenue, and recent orders at a glance.</p></a>" +
1369
1383
  "</div>" +
1370
1384
  "<div class=\"actions-row\"><form method=\"post\" action=\"/admin/logout\"><button type=\"submit\" class=\"btn btn--ghost\">Sign out</button></form></div>" +
@@ -1401,11 +1415,64 @@ function renderAdminSetup(opts) {
1401
1415
  return _renderAdminShell(opts.shop_name, "Setup", body);
1402
1416
  }
1403
1417
 
1418
+ // Each integration is off until the operator supplies its credentials.
1419
+ // `opts.status` carries the live booleans (computed at the entry point
1420
+ // from the environment); this page shows what's on and exactly what to
1421
+ // set to turn the rest on. Read-only — secrets are never rendered.
1422
+ var INTEGRATIONS_CATALOG = [
1423
+ { key: "stripe", name: "Card checkout (Stripe)", enables: "Checkout, the Payment Element, refunds, and subscription billing.",
1424
+ set: "STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEY (point the Stripe webhook at /api/webhooks/stripe)." },
1425
+ { key: "express_checkout", name: "Apple Pay & Google Pay", enables: "One-tap wallet buttons on the pay page.",
1426
+ set: "Configure Stripe (above), then register each domain: POST /admin/payment-method-domains {\"domain_name\":\"shop.example.com\"}. No Apple Developer account needed." },
1427
+ { key: "google_signin", name: "Sign in with Google", enables: "A “Continue with Google” button on the account login page.",
1428
+ set: "GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, SHOP_ORIGIN. Add <SHOP_ORIGIN>/account/auth/google/callback as a Google OAuth redirect URI." },
1429
+ ];
1430
+
1431
+ function renderAdminIntegrations(opts) {
1432
+ opts = opts || {};
1433
+ var status = opts.status || {};
1434
+ var rows = INTEGRATIONS_CATALOG.map(function (it) {
1435
+ // Three states: "enabled" (live), "action" (credentials present but a
1436
+ // one-time operator action — e.g. registering a domain with Stripe —
1437
+ // is still required before it's actually live), "off" (not configured).
1438
+ var st = status[it.key] || "off";
1439
+ var pill, detail;
1440
+ if (st === "enabled") {
1441
+ pill = "<span class=\"status-pill paid\">Enabled</span>";
1442
+ detail = "<span class=\"meta\">Live.</span>";
1443
+ } else if (st === "action") {
1444
+ pill = "<span class=\"status-pill pending\">Action needed</span>";
1445
+ detail = "<span class=\"meta\">" + _htmlEscape(it.set) + "</span>";
1446
+ } else {
1447
+ pill = "<span class=\"status-pill cancelled\">Not configured</span>";
1448
+ detail = "<span class=\"meta\">" + _htmlEscape(it.set) + "</span>";
1449
+ }
1450
+ return "<tr>" +
1451
+ "<td><strong>" + _htmlEscape(it.name) + "</strong><br><span class=\"meta\">" + _htmlEscape(it.enables) + "</span></td>" +
1452
+ "<td>" + pill + "</td>" +
1453
+ "<td>" + detail + "</td>" +
1454
+ "</tr>";
1455
+ }).join("");
1456
+ var body =
1457
+ "<section>" +
1458
+ "<h2>Integrations</h2>" +
1459
+ "<p class=\"meta\">Every integration is off until you supply its credentials — set them as deployment secrets, then redeploy. Nothing is enabled without your keys.</p>" +
1460
+ "<div class=\"panel\"><table>" +
1461
+ "<thead><tr><th>Integration</th><th>Status</th><th>To enable</th></tr></thead>" +
1462
+ "<tbody>" + rows + "</tbody>" +
1463
+ "</table></div>" +
1464
+ "<p class=\"meta\" style=\"margin-top:1.25rem;\">Sign in with Apple and PayPal are planned. “Sign in with Shop” / Shop Pay isn't available to a self-hosted store. See the README “Optional integrations” section for full setup steps.</p>" +
1465
+ "<div class=\"actions-row\"><a class=\"btn btn--ghost\" href=\"/admin\">Back</a></div>" +
1466
+ "</section>";
1467
+ return _renderAdminShell(opts.shop_name, "Integrations", body);
1468
+ }
1469
+
1404
1470
  module.exports = {
1405
1471
  mount: mount,
1406
1472
  AUDIT_NAMESPACE: AUDIT_NAMESPACE,
1407
1473
  renderDashboard: renderDashboard,
1408
- renderAdminLogin: renderAdminLogin,
1409
- renderAdminLanding: renderAdminLanding,
1410
- renderAdminSetup: renderAdminSetup,
1474
+ renderAdminLogin: renderAdminLogin,
1475
+ renderAdminLanding: renderAdminLanding,
1476
+ renderAdminSetup: renderAdminSetup,
1477
+ renderAdminIntegrations: renderAdminIntegrations,
1411
1478
  };
package/lib/checkout.js CHANGED
@@ -96,6 +96,10 @@ function create(deps) {
96
96
  var payment = deps.payment;
97
97
  var order = deps.order;
98
98
  var subscriptions = deps.subscriptions || null;
99
+ // Optional — when wired, the buyer email is hashed (via the same
100
+ // customers.hashEmail keying the customers table) and stored on the
101
+ // order so a later verified-email sign-in can claim the guest order.
102
+ var customers = deps.customers || null;
99
103
 
100
104
  // Compose a quote from a cart + ship-to + (optional) selected
101
105
  // shipping service. Pure read — no DB writes.
@@ -219,6 +223,9 @@ function create(deps) {
219
223
  grand_total_minor: quote.totals.grand_total_minor,
220
224
  payment_intent_id: pi.id,
221
225
  ship_to: input.ship_to,
226
+ // Hash of the buyer email (same key as the customers table) so a
227
+ // later verified-email sign-in can claim this guest order.
228
+ customer_email_hash: customers ? customers.hashEmail(email) : null,
222
229
  lines: quote.lines.map(function (l) {
223
230
  return {
224
231
  variant_id: l.variant_id,
package/lib/order.js CHANGED
@@ -174,13 +174,14 @@ function create(opts) {
174
174
  await query(
175
175
  "INSERT INTO orders (id, cart_id, customer_id, session_id, status, currency, " +
176
176
  "subtotal_minor, discount_minor, tax_minor, shipping_minor, grand_total_minor, " +
177
- "payment_intent_id, ship_to_json, created_at, updated_at) " +
178
- "VALUES (?1, ?2, ?3, ?4, 'pending', ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?13)",
177
+ "payment_intent_id, ship_to_json, customer_email_hash, created_at, updated_at) " +
178
+ "VALUES (?1, ?2, ?3, ?4, 'pending', ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?14)",
179
179
  [
180
180
  id, input.cart_id, input.customer_id || null, input.session_id,
181
181
  input.currency, input.subtotal_minor, input.discount_minor,
182
182
  input.tax_minor, input.shipping_minor, input.grand_total_minor,
183
- input.payment_intent_id || null, JSON.stringify(input.ship_to), ts,
183
+ input.payment_intent_id || null, JSON.stringify(input.ship_to),
184
+ input.customer_email_hash || null, ts,
184
185
  ],
185
186
  );
186
187
  for (var i = 0; i < input.lines.length; i += 1) {
@@ -369,6 +370,26 @@ function create(opts) {
369
370
  return await this.get(orderId);
370
371
  },
371
372
 
373
+ // Claim guest orders into a customer account by matching the
374
+ // recorded buyer-email hash. The CALLER must only pass a hash for an
375
+ // email the identity provider VERIFIED — this method does not (and
376
+ // cannot) re-verify; linking an unverified email would be account
377
+ // takeover. Only touches orders with no owner yet (customer_id IS
378
+ // NULL), so it never re-assigns another customer's order. Returns
379
+ // the count linked.
380
+ linkGuestOrdersByEmailHash: async function (customerId, emailHash) {
381
+ _uuid(customerId, "customer id");
382
+ if (typeof emailHash !== "string" || !emailHash.length) {
383
+ throw new TypeError("order.linkGuestOrdersByEmailHash: emailHash must be a non-empty string");
384
+ }
385
+ var r = await query(
386
+ "UPDATE orders SET customer_id = ?1, updated_at = ?2 " +
387
+ "WHERE customer_id IS NULL AND customer_email_hash = ?3",
388
+ [customerId, _now(), emailHash],
389
+ );
390
+ return Number(r.rowCount || 0);
391
+ },
392
+
372
393
  // Has this customer purchased this product? True iff an order
373
394
  // line for any variant of the product sits in an order owned by
374
395
  // the customer whose status is a real purchase — anything except
package/lib/storefront.js CHANGED
@@ -3294,6 +3294,15 @@ function mount(router, deps) {
3294
3294
  if (anonCart) await deps.cart.setCustomer(anonCart.id, rv.customer.id);
3295
3295
  } catch (_e) { /* best-effort merge; sign-in itself succeeds */ }
3296
3296
  }
3297
+ // Claim prior guest orders placed under this email — ONLY because
3298
+ // the provider verified it (claims.email_verified). Links orders
3299
+ // with no owner yet whose recorded email hash matches; best-effort.
3300
+ if (claims.email_verified === true && claims.email && deps.order &&
3301
+ typeof deps.order.linkGuestOrdersByEmailHash === "function") {
3302
+ try {
3303
+ await deps.order.linkGuestOrdersByEmailHash(rv.customer.id, deps.customers.hashEmail(claims.email));
3304
+ } catch (_e) { /* best-effort reconciliation; sign-in succeeds regardless */ }
3305
+ }
3297
3306
  _setAuthCookie(res, { customer_id: rv.customer.id, exp: Date.now() + _b().constants.TIME.days(14) });
3298
3307
  res.status(303); res.setHeader && res.setHeader("location", "/account");
3299
3308
  return res.end ? res.end() : res.send("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {