@blamejs/blamejs-shop 0.1.10 → 0.1.11

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.1.x
10
10
 
11
+ - v0.1.11 (2026-05-25) — **Sign in with Apple.** Customers can now sign in with Apple, alongside passkeys and Google. A “Continue with Apple” button appears on the account login page once the operator wires the Apple credentials; the callback turns the verified Apple identity into a shop session, adopts the guest cart, and claims prior guest orders placed under the same verified email — the same way Google sign-in does. Apple's OAuth client secret is itself an ES256 JWT, which the shop mints from the team's Sign-in-with-Apple key. Sign in with Apple is off until the credentials are set, like every other integration. **Added:** *Sign in with Apple (OIDC)* — `GET /account/login/apple` starts the flow (sealed in-flight state cookie, PKCE, nonce); Apple posts the result back to `POST /account/auth/apple/callback` (response_mode=form_post). The callback verifies the state, exchanges the code, signs the customer in on `(provider=apple, subject)`, adopts the guest cart, and — when Apple reports the email as verified — claims prior guest orders under that email. The display name is captured from Apple's first-authorization `user` field (Apple sends it only once and never in the ID token). The button appears on `/account/login` only when the credentials are configured, and is listed on `/admin/integrations`. · *customers.mintAppleClientSecret* — Mints Apple's required ES256 client-secret JWT from a Services-ID `.p8` key (team id, key id, client id). This is the one signature the protocol forces to be classical ECDSA P-256 rather than the framework's post-quantum default — an external identity provider's wire format, not an application default. The secret is minted at boot with a 150-day life (inside Apple's six-month ceiling) and re-minted on each deploy. **Changed:** *Account login offers Apple when configured* — The login page shows Continue-with-Google and Continue-with-Apple buttons independently, each gated on its own credentials. Set `APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_CLIENT_ID` (your Services ID), `APPLE_PRIVATE_KEY` (the `.p8` contents), and `SHOP_ORIGIN`, and add `<SHOP_ORIGIN>/account/auth/apple/callback` as a Return URL on the Services ID. Requires an Apple Developer Program membership. See the README “Optional integrations” section.
12
+
11
13
  - v0.1.10 (2026-05-25) — **Admin console — a review moderation screen.** Reviews join the admin console, completing the set of management screens. `/admin/reviews` is the moderation queue: filter by status (pending, published, rejected) and act on each submission inline — publish it, reject it with a reason, or take a published one back down. Reviews are short, so the queue shows each one in full (rating, title, body, verified-purchase flag) with its actions, no separate detail page. As with the other screens, every path content-negotiates: a bearer-token client gets the JSON API unchanged, a signed-in browser gets the HTML console. **Added:** *Review moderation screen* — `/admin/reviews` renders the review queue as inline cards (rating stars, title, body, verified-purchase flag, product, date) with status-filter chips, when opened in a signed-in browser; the same path serves the existing JSON list to a bearer-token client. Each card offers the actions that fit its status — a pending review can be published or rejected, a published one taken down, a rejected one published — with Reject taking a reason. Publish and reject post to their endpoints and redirect (PRG); a missing id is a no-op notice, never a 500. **Changed:** *Console nav gains Reviews; the reviews API content-negotiates* — The signed-in admin nav now includes Reviews — shown, like Returns, only when the reviews primitive is wired. The `/admin/reviews` list and the publish / reject endpoints serve the HTML console to a signed-in browser while continuing to serve the JSON API to a bearer-token client unchanged. A request without the bearer token is no longer answered with a 401 on the list path — a browser GET receives the sign-in form — matching the other console screens.
12
14
 
13
15
  - v0.1.9 (2026-05-25) — **Admin console — a returns moderation screen.** Returns join the admin console. `/admin/returns` is the RMA moderation queue: filter by status (pending, approved, received, refunded, rejected), and open a request to see its items, reason, customer notes, and linked order. From the detail page an operator works the return through its lifecycle — approve with a refund amount, mark the goods received, record the refund, or reject with a reason shown to the customer — with only the moves legal from the current status offered. An illegal move or a bad id is refused with a notice rather than an error. As with Products and Orders, every path content-negotiates: a bearer-token client gets the JSON API unchanged, a signed-in browser gets the HTML console. **Added:** *Returns moderation screen* — `/admin/returns` renders the RMA queue as a table (RMA code, order, reason, status, item count, refund amount, requested date) with status-filter chips, when opened in a signed-in browser; the same path serves the existing JSON list to a bearer-token client. Each return links to a detail page showing its line items, reason and customer notes, linked order, and refund details, with the legal next actions as forms — Approve takes a refund amount and currency, Reject takes a reason, Mark received and Refund are single-click. Each posts to its own endpoint and redirects (PRG); an unknown id renders a 404 page and an illegal or refused action redirects back with a notice, never a 500. · *returns.transitionsFrom* — `returns.transitionsFrom(status)` returns the moderation events legal from a given status as `{ on, to }`, derived from the same transition table the setters use — so the console's action buttons stay in lockstep with the return state machine. **Changed:** *Console nav gains Returns; the returns API content-negotiates* — The signed-in admin nav now includes Returns alongside Home, Dashboard, Products, Orders, Integrations, and Setup — shown only when the returns primitive is wired, so the link never points at an unmounted route. The `/admin/returns` list, detail, and approve / received / refund / reject endpoints now serve the HTML console to a signed-in browser while continuing to serve the JSON API to a bearer-token client unchanged. A request without the bearer token is no longer answered with a 401 on these paths — a browser GET receives the sign-in form and a write redirects to the console, matching the other console screens.
package/README.md CHANGED
@@ -62,7 +62,7 @@ Every primitive is composed on the vendored blamejs surface — no npm runtime d
62
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). |
63
63
  | **`lib/email.js`** | Transactional templates — order receipt, ship notification, refund confirmation. Strict `{{var}}` renderer with HTML escape + refusal of unknown / unused placeholders. Composed on `b.mail` (DKIM/SPF/DMARC/BIMI upstream). |
64
64
  | **`lib/storefront.js`** | Server-rendered HTML — utility bar + sticky header + dark hero with code-preview card + primitives marquee + featured-product callout + collections grid + framework feature band + designed catalog grid + newsletter band + four-column footer. Designed surfaces also for PDP, cart, checkout, pay, order, account login / register / dashboard, search results, `/admin` API landing, 404. Image-bearing cards on the home + search grids pull from `catalog.media`. The default theme stylesheet is external (R2-served `themes/default/assets/css/main.css`) and CSP-compliant — operators override by uploading a replacement at the same key, by passing `opts.theme_css` to renderers, or by registering a named theme through the `theme` primitive. |
65
- | **`lib/customers.js`** | Customer accounts — passkey (WebAuthn) + **Sign in with Google** (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). Account routes (`/account/login`, `/account/register`, `/account`, `/account/login/google`) ship as designed cards on the storefront. |
65
+ | **`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. |
66
66
  | **`lib/reviews.js`** | Operator-moderated product ratings. Submission requires a signed-in customer **and** a verified purchase — `/products/:slug/review` confirms a completed order for the product (via `order.hasPurchasedProduct`) before accepting, re-checked on POST; reviews land `pending`. Author identity is hash-only (`b.crypto.namespaceHash`); the raw email is never stored. The PDP renders the average, per-star distribution, and published reviews with `AggregateRating` JSON-LD. `/admin/reviews` is the moderation queue (`listByStatus` → publish / reject). |
67
67
  | **`lib/wishlist.js`** | Per-customer saved products. The PDP renders a login-gated "Save to wishlist" toggle and a "N shoppers saved this" social-proof count; `/account/wishlist` lists saved items (remove + reopen, orphan-tolerant when a product is archived). `POST /wishlist/toggle` is idempotent (`INSERT OR IGNORE`) and redirects to the canonical product slug or a safe same-origin `return_to`. UUID-shape-validated ids, `b.pagination` HMAC cursors. |
68
68
  | **`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`. |
@@ -162,12 +162,10 @@ variables. A signed-in operator can see the live on/off status of each at
162
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
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
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
+ | **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. |
165
166
 
166
167
  **Planned / not available:**
167
168
 
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
169
  - **PayPal** — a separate adapter (Orders v2 + its own webhook); planned.
172
170
  - **Shop Pay / "Sign in with Shop"** — **not available** to a self-hosted,
173
171
  non-Shopify store: the credentials only issue from a Shopify Admin and payment
package/lib/admin.js CHANGED
@@ -1787,6 +1787,8 @@ var INTEGRATIONS_CATALOG = [
1787
1787
  set: "Configure Stripe (above), then register each domain: POST /admin/payment-method-domains {\"domain_name\":\"shop.example.com\"}. No Apple Developer account needed." },
1788
1788
  { key: "google_signin", name: "Sign in with Google", enables: "A “Continue with Google” button on the account login page.",
1789
1789
  set: "GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, SHOP_ORIGIN. Add <SHOP_ORIGIN>/account/auth/google/callback as a Google OAuth redirect URI." },
1790
+ { key: "apple_signin", name: "Sign in with Apple", enables: "A “Continue with Apple” button on the account login page.",
1791
+ set: "APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_CLIENT_ID (your Services ID), APPLE_PRIVATE_KEY (the .p8 key contents), SHOP_ORIGIN. Add <SHOP_ORIGIN>/account/auth/apple/callback as a Return URL on the Services ID. Requires an Apple Developer Program membership." },
1790
1792
  ];
1791
1793
 
1792
1794
  function renderAdminIntegrations(opts) {
package/lib/customers.js CHANGED
@@ -29,6 +29,12 @@
29
29
  * <= 4 KiB) but never tries to parse them.
30
30
  */
31
31
 
32
+ // Apple's OAuth client secret must be an ES256 (ECDSA P-256, IEEE-P1363)
33
+ // JWT; the PQC-first framework ships no classical ES256 signer and
34
+ // b.crypto.sign can't select P1363 encoding. node:crypto is the only way to
35
+ // mint it (used solely by mintAppleClientSecret below).
36
+ var nodeCrypto = require("node:crypto"); // allow:non-shop-require — Apple mandates ES256/P1363; framework has no classical ES256 signer
37
+
32
38
  var bShop;
33
39
  function _b() {
34
40
  if (!bShop) bShop = require("./index");
@@ -413,8 +419,56 @@ function create(opts) {
413
419
  return api;
414
420
  }
415
421
 
422
+ // "Sign in with Apple" requires the OAuth client secret to be an
423
+ // ES256-signed JWT — Apple's protocol mandates ECDSA P-256, the one
424
+ // signature in this codebase that is NOT the PQC default (the framework
425
+ // ships no ES256 signer by design). It's an external-protocol constraint,
426
+ // not an application default, exactly like a Stripe/PayPal webhook
427
+ // signature: the remote party dictates the algorithm. Minted from the
428
+ // team's `.p8` EC private key:
429
+ // header { alg:"ES256", kid:<key_id> }
430
+ // payload { iss:<team_id>, iat, exp, aud:"https://appleid.apple.com", sub:<client_id> }
431
+ // Returns the compact JWS to hand to `b.auth.oauth.create({ clientSecret })`.
432
+ // `ttl_seconds` defaults to 150 days (inside Apple's 180-day ceiling); the
433
+ // secret is re-minted on the next process start, so a normal deploy
434
+ // cadence refreshes it well within the window. A config-time / entry-point
435
+ // helper — THROWS on bad input so a misconfigured `.p8` fails at boot.
436
+ function mintAppleClientSecret(opts) {
437
+ opts = opts || {};
438
+ var teamId = opts.team_id;
439
+ var keyId = opts.key_id;
440
+ var clientId = opts.client_id;
441
+ var privateKeyPem = opts.private_key; // .p8 contents (PKCS#8 PEM)
442
+ if (!teamId || !keyId || !clientId || !privateKeyPem) {
443
+ throw new TypeError("mintAppleClientSecret: team_id, key_id, client_id, private_key are all required");
444
+ }
445
+ // Apple's exp/iat are unix SECONDS, so derive the default from C.TIME
446
+ // (the single duration source of truth, in ms) rather than a hand-rolled
447
+ // literal: 150 days, inside Apple's 180-day ceiling.
448
+ var ttl = opts.ttl_seconds == null ? _b().constants.TIME.days(150) / 1000 : opts.ttl_seconds;
449
+ if (typeof ttl !== "number" || !isFinite(ttl) || ttl <= 0 || ttl > 15777000) {
450
+ throw new TypeError("mintAppleClientSecret: ttl_seconds must be 1..15777000 (Apple's 6-month maximum)");
451
+ }
452
+ var key;
453
+ try { key = nodeCrypto.createPrivateKey(privateKeyPem); }
454
+ catch (e) { throw new TypeError("mintAppleClientSecret: private_key is not a valid PEM key — " + (e && e.message || e)); }
455
+ if (key.asymmetricKeyType !== "ec") {
456
+ throw new TypeError("mintAppleClientSecret: private_key must be an EC P-256 (.p8) key, got " + key.asymmetricKeyType);
457
+ }
458
+ var now = Math.floor(Date.now() / 1000);
459
+ var header = { alg: "ES256", kid: keyId };
460
+ var payload = { iss: teamId, iat: now, exp: now + ttl, aud: "https://appleid.apple.com", sub: clientId };
461
+ var b64u = _b().crypto.toBase64Url;
462
+ var signingInput = b64u(JSON.stringify(header)) + "." + b64u(JSON.stringify(payload));
463
+ // ES256 = ECDSA P-256 over SHA-256, JOSE-encoded as raw r||s (IEEE
464
+ // P1363), not the DER node emits by default — `dsaEncoding` selects it.
465
+ var sig = nodeCrypto.sign("sha256", Buffer.from(signingInput), { key: key, dsaEncoding: "ieee-p1363" });
466
+ return signingInput + "." + b64u(sig);
467
+ }
468
+
416
469
  module.exports = {
417
470
  create: create,
471
+ mintAppleClientSecret: mintAppleClientSecret,
418
472
  EMAIL_NAMESPACE: EMAIL_NAMESPACE,
419
473
  MAX_DISPLAY_NAME_LEN: MAX_DISPLAY_NAME_LEN,
420
474
  MAX_CRED_FIELD_BYTES: MAX_CRED_FIELD_BYTES,
package/lib/storefront.js CHANGED
@@ -2245,10 +2245,17 @@ var LOGIN_ERROR_MESSAGES = {
2245
2245
 
2246
2246
  function renderAccountLogin(opts) {
2247
2247
  opts = opts || {};
2248
- var oauthHtml = opts.google_enabled
2248
+ var oauthButtons = "";
2249
+ if (opts.google_enabled) {
2250
+ oauthButtons += "<a class=\"btn-secondary auth-oauth__btn\" href=\"/account/login/google\">Continue with Google</a>";
2251
+ }
2252
+ if (opts.apple_enabled) {
2253
+ oauthButtons += "<a class=\"btn-secondary auth-oauth__btn\" href=\"/account/login/apple\">Continue with Apple</a>";
2254
+ }
2255
+ var oauthHtml = oauthButtons
2249
2256
  ? "<div class=\"auth-oauth\">" +
2250
2257
  "<div class=\"auth-oauth__divider\"><span>or</span></div>" +
2251
- "<a class=\"btn-secondary auth-oauth__btn\" href=\"/account/login/google\">Continue with Google</a>" +
2258
+ oauthButtons +
2252
2259
  "</div>"
2253
2260
  : "";
2254
2261
  var errHtml = (opts.error && LOGIN_ERROR_MESSAGES[opts.error])
@@ -2937,6 +2944,7 @@ function mount(router, deps) {
2937
2944
  shop_name: shopName,
2938
2945
  cart_count: cartCount,
2939
2946
  google_enabled: !!deps.oauthGoogle,
2947
+ apple_enabled: !!deps.oauthApple,
2940
2948
  error: url && url.searchParams.get("error"),
2941
2949
  }));
2942
2950
  });
@@ -3309,6 +3317,112 @@ function mount(router, deps) {
3309
3317
  });
3310
3318
  }
3311
3319
 
3320
+ // Sign in with Apple (OIDC). Mounts when the operator wires an
3321
+ // `oauthApple` adapter (b.auth.oauth, apple preset). Two differences
3322
+ // from Google: (1) Apple uses response_mode=form_post, so the callback
3323
+ // is a POST whose `code`/`state` arrive in the form body, not the query
3324
+ // string; (2) Apple returns the user's name ONLY on the first
3325
+ // authorization, in a `user` form field (JSON) — never in the ID token
3326
+ // — so we read it from there and fall back to the (usually absent)
3327
+ // token name. Everything after the verified identity (sealed-state
3328
+ // check, sign-in, cart merge, guest-order reconciliation, auth cookie)
3329
+ // mirrors the Google path.
3330
+ if (deps.oauthApple) {
3331
+ router.get("/account/login/apple", async function (req, res) {
3332
+ try {
3333
+ var a = await deps.oauthApple.authorizationUrl({});
3334
+ // Apple returns via response_mode=form_post — a CROSS-SITE POST
3335
+ // from appleid.apple.com back to our callback. A SameSite=Lax
3336
+ // cookie is NOT sent on a cross-site POST navigation (Lax only
3337
+ // covers top-level GETs), so the sealed state would be lost and
3338
+ // every sign-in would fail. It must be SameSite=None; Secure.
3339
+ // (Google's callback is a GET, so Lax is fine there.)
3340
+ _cookieJar().writeSealed(res, OAUTH_COOKIE_NAME, JSON.stringify({
3341
+ provider: "apple", state: a.state, nonce: a.nonce, verifier: a.verifier,
3342
+ }), { expires: new Date(Date.now() + _b().constants.TIME.minutes(10)), path: "/account", sameSite: "None", secure: true });
3343
+ res.status(302);
3344
+ res.setHeader && res.setHeader("location", a.url);
3345
+ return res.end ? res.end() : res.send("");
3346
+ } catch (e) {
3347
+ if (e && e.code === "vault/not-initialized") { _serviceUnavailable(res, "auth not configured"); return; }
3348
+ res.status(303); res.setHeader && res.setHeader("location", "/account/login?error=oauth");
3349
+ return res.end ? res.end() : res.send("");
3350
+ }
3351
+ });
3352
+
3353
+ router.post("/account/auth/apple/callback", async function (req, res) {
3354
+ function _toLogin(err) {
3355
+ res.status(303);
3356
+ res.setHeader && res.setHeader("location", "/account/login" + (err ? "?error=" + err : ""));
3357
+ return res.end ? res.end() : res.send("");
3358
+ }
3359
+ // form_post: code + state (+ the first-auth `user` blob) are in the
3360
+ // request body, not the query string.
3361
+ var body = req.body || {};
3362
+ var code = body.code;
3363
+ var state = body.state;
3364
+ if (!code || !state) return _toLogin("oauth");
3365
+
3366
+ var saved;
3367
+ try { var raw = _cookieJar().readSealed(req, OAUTH_COOKIE_NAME); saved = raw ? JSON.parse(raw) : null; }
3368
+ catch (_e) { saved = null; }
3369
+ _cookieJar().clear(res, OAUTH_COOKIE_NAME, { path: "/account" });
3370
+ if (!saved || saved.provider !== "apple" || saved.state !== state) return _toLogin("oauth");
3371
+
3372
+ var claims;
3373
+ try {
3374
+ var tokens = await deps.oauthApple.exchangeCode({ code: code, verifier: saved.verifier, nonce: saved.nonce });
3375
+ claims = tokens && tokens.claims;
3376
+ } catch (_e) { return _toLogin("oauth"); }
3377
+ if (!claims || !claims.sub) return _toLogin("oauth");
3378
+
3379
+ // Apple sends the display name only on first consent, as a JSON
3380
+ // `user` form field: { name: { firstName, lastName }, email }.
3381
+ var displayName = claims.name || null;
3382
+ if (typeof body.user === "string" && body.user.length) {
3383
+ try {
3384
+ var u = JSON.parse(body.user);
3385
+ if (u && u.name) {
3386
+ displayName = [u.name.firstName, u.name.lastName].filter(Boolean).join(" ") || displayName;
3387
+ }
3388
+ } catch (_e) { /* malformed user blob — fall back to the token name */ }
3389
+ }
3390
+ // Apple's email_verified arrives as a boolean OR the string "true".
3391
+ var emailVerified = claims.email_verified === true || claims.email_verified === "true";
3392
+
3393
+ var rv;
3394
+ try {
3395
+ rv = await deps.customers.signInWithOIDC({
3396
+ provider: "apple",
3397
+ subject: String(claims.sub),
3398
+ email: claims.email,
3399
+ email_verified: emailVerified,
3400
+ display_name: displayName,
3401
+ });
3402
+ } catch (e) {
3403
+ if (e && e.code === "OAUTH_EMAIL_UNVERIFIED_CONFLICT") return _toLogin("email-conflict");
3404
+ if (e instanceof TypeError) return _toLogin("oauth");
3405
+ throw e;
3406
+ }
3407
+ var sid = _readSidCookie(req);
3408
+ if (sid) {
3409
+ try {
3410
+ var anonCart = await deps.cart.bySession(sid);
3411
+ if (anonCart) await deps.cart.setCustomer(anonCart.id, rv.customer.id);
3412
+ } catch (_e) { /* best-effort merge; sign-in itself succeeds */ }
3413
+ }
3414
+ if (emailVerified && claims.email && deps.order &&
3415
+ typeof deps.order.linkGuestOrdersByEmailHash === "function") {
3416
+ try {
3417
+ await deps.order.linkGuestOrdersByEmailHash(rv.customer.id, deps.customers.hashEmail(claims.email));
3418
+ } catch (_e) { /* best-effort reconciliation; sign-in succeeds regardless */ }
3419
+ }
3420
+ _setAuthCookie(res, { customer_id: rv.customer.id, exp: Date.now() + _b().constants.TIME.days(14) });
3421
+ res.status(303); res.setHeader && res.setHeader("location", "/account");
3422
+ return res.end ? res.end() : res.send("");
3423
+ });
3424
+ }
3425
+
3312
3426
  // Wishlist — saved products scoped to the logged-in customer.
3313
3427
  // Mounts when the wishlist primitive is wired.
3314
3428
  if (deps.wishlist) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
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": {