From e0fff4272c6eb8430698c996cfe19d54c1ff6eac Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Oct 2026 22:08:09 -0300 Subject: [PATCH] feat(apps-nuvemshop): customer accounts, sitemap and the apps-nuvemshop skill The Storefront API has no customer endpoints. Validated against a live store: - register: Admin API POST /customers (custom-app `adminToken`). The store's own form needs a domain-bound reCAPTCHA and silently drops posts without it; the Admin API has no captcha, so the action requires Cloudflare Turnstile (`turnstileSecret`) unless `allowUnverifiedRegistration`. Duplicate email maps to `email_taken`. - login/logout: the store's /account/login/ and /account/logout/, called server-side. Failure reasons come from the login page markup (`js-login-general-error`, `js-account-validation-pending`). - user loader: customer id from the store's /account/ page, profile from the Admin API; null when logged out. - store.ts bridges the session: only `store_*` cookies go upstream, and only `store_*` Set-Cookie come back, re-emitted on our domain. - nuvemshopSitemap(): products + categories from the API with our URLs; matches the store's own sitemap (50/50 products, same categories). - Config/admin form: storeUrl, adminToken and turnstileSecret (secrets), allowUnverifiedRegistration. Operation names for store/admin/turnstile calls. - New skill .agents/skills/apps-nuvemshop (API fields/limits, listing parity, accounts, cart/checkout findings incl. the /comprar/ IP block). Co-Authored-By: Claude Opus 5.5 (1M context) --- .agents/skills/README.md | 3 +- .agents/skills/apps-nuvemshop/SKILL.md | 41 +++ .../apps-nuvemshop/references/account.md | 26 ++ .../skills/apps-nuvemshop/references/api.md | 43 +++ .../references/cart-and-checkout.md | 37 +++ .../apps-nuvemshop/references/listing.md | 41 +++ CLAUDE.md | 1 + .../src/__tests__/account.test.ts | 250 ++++++++++++++++++ .../apps-nuvemshop/src/__tests__/mod.test.ts | 42 ++- .../src/actions/account/login.ts | 30 +++ .../src/actions/account/logout.ts | 10 + .../src/actions/account/register.ts | 83 ++++++ packages/apps-nuvemshop/src/admin.ts | 23 ++ packages/apps-nuvemshop/src/client.ts | 18 ++ packages/apps-nuvemshop/src/index.ts | 5 + packages/apps-nuvemshop/src/loaders/user.ts | 30 +++ packages/apps-nuvemshop/src/manifest.gen.ts | 8 + packages/apps-nuvemshop/src/mod.ts | 31 +++ packages/apps-nuvemshop/src/store.ts | 88 ++++++ .../src/utils/__tests__/sitemap.test.ts | 57 ++++ .../src/utils/operationRouter.ts | 8 +- packages/apps-nuvemshop/src/utils/sitemap.ts | 54 ++++ 22 files changed, 923 insertions(+), 6 deletions(-) create mode 100644 .agents/skills/apps-nuvemshop/SKILL.md create mode 100644 .agents/skills/apps-nuvemshop/references/account.md create mode 100644 .agents/skills/apps-nuvemshop/references/api.md create mode 100644 .agents/skills/apps-nuvemshop/references/cart-and-checkout.md create mode 100644 .agents/skills/apps-nuvemshop/references/listing.md create mode 100644 packages/apps-nuvemshop/src/__tests__/account.test.ts create mode 100644 packages/apps-nuvemshop/src/actions/account/login.ts create mode 100644 packages/apps-nuvemshop/src/actions/account/logout.ts create mode 100644 packages/apps-nuvemshop/src/actions/account/register.ts create mode 100644 packages/apps-nuvemshop/src/admin.ts create mode 100644 packages/apps-nuvemshop/src/loaders/user.ts create mode 100644 packages/apps-nuvemshop/src/store.ts create mode 100644 packages/apps-nuvemshop/src/utils/__tests__/sitemap.test.ts create mode 100644 packages/apps-nuvemshop/src/utils/sitemap.ts diff --git a/.agents/skills/README.md b/.agents/skills/README.md index 259dbf895..982be2019 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -46,6 +46,7 @@ skill, not the row. | Skill | What it covers | |---|---| +| [`apps-nuvemshop/`](apps-nuvemshop/SKILL.md) | @decocms/apps-nuvemshop (packages/apps-nuvemshop): the Nuvemshop/Tiendanube commerce app over the headless Storefront API (storefront-api.tiendanube.com v2026-11) plus the store's own session endpoints and the Admin API. Use when touching nuvemshopGet/nuvemshopPost, PRODUCT_FIELDS/CATEGORY_FIELDS, productListingPage facets/sort (applyListing, sort_by, ?Cor=A\|B), toProduct/variant prices, relatedProducts, the account actions (login/logout/register, user loader, store_session_payload cookie, adminToken, Turnstile), nuvemshopSitemap, createCheckout, or the deco-nuvemshop block; or when products come back with only id/name/handle, a category tree is flat, filters/sorts don't match the theme, links redirect (trailing slash), registration fails silently (reCAPTCHA), login says 'Valide seu e-mail', or add-to-cart (/comprar/) returns 403. | | [`apps-vtex/`](apps-vtex/SKILL.md) | @decocms/apps-vtex (packages/apps-vtex): the VTEX commerce app — client (vtexFetch, vtexFetchWithCookies, vtexCachedFetch, intelligentSearch), checkout/session/auth actions, catalog/IS/legacy/cart loaders, useCart/useUser/useWishlist and the Cart v2 hooks (createCart, createCartQuery). Use when code mentions orderForm, expectedOrderFormSections, *V2 cart actions, invoke.vtex.*, VtexIdclientAutCookie/buildAuthCookieHeader, salesChannel/sc, vtex_segment, fetchWithCache; or when the cart empties after add-to-cart, logged-in calls 401/403, prices are wrong per sales channel, orderForm sections are missing, a minicart/add-to-cart makes too many VTEX cart API calls, or VTEX responses look stale. Also for apps-vtex PR reviews. Repeated calls across navigations or edge/route/layout caching are deco-caching, not this. | | [`deco-caching/`](deco-caching/SKILL.md) | Every cache layer of a deco storefront (@decocms/blocks, @decocms/tanstack): worker-entry edge cache and profiles, route staleTime/gcTime, page in-flight dedup, layout caches, loader cache, SWR upstream cache. Use when touching createDecoWorkerEntry, cacheHeaders, detectCacheProfile, routeCacheDefaults, staleTime, gcTime, createCachedLoader, createFetchCache, registerLayoutSections, X-Cache, Cache-Control or /_cache/purge; or when a page is a cache MISS/never HITs, a TTL is wrong, private data is cached, the same upstream call (e.g. a Header/Footer shelf hitting VTEX intelligent search) fires on every navigation, staleTime/gcTime values are asked about, a layout shows another visitor's variant, you need to purge, or the loader cache eats memory. | | [`deco-cms-route-config/`](deco-cms-route-config/SKILL.md) | @decocms/tanstack route files for a TanStack Start site: cmsRouteConfig ($.tsx catch-all), cmsHomeRouteConfig (index.tsx), __root.tsx, the admin protocol routes (decoMetaRouteConfig/decoRenderRouteConfig/decoInvokeRouteConfig), ignoreSearchParams, routeCacheDefaults (staleTime/gcTime), head/SEO from page.seo. Use when creating or editing src/routes/*, when a variant click or hydration triggers an extra server fetch, a route 500s with 'cannot have both an 'id' and a 'path'', SEO tags are missing from SSR HTML, or an import from @decocms/tanstack does not resolve. | @@ -54,5 +55,5 @@ skill, not the row. | [`deco-storefront-egress-guardrails/`](deco-storefront-egress-guardrails/SKILL.md) | Audit for storefront patterns that burn origin egress and edge cache hit rate on deco sites (@decocms/tanstack; legacy Fresh/Deno too), with a zero-dependency HTML anatomy script. Use when origin egress or bandwidth cost spikes, HTML of a PLP/PDP/home is megabytes (oversized SSR payload), a PLP/PDP never gets a cache HIT, gclid/utm tracking params leak into cached pages, the cache is split per region, or when reviewing a PR or migration for these regressions. | | [`run-migration/`](run-migration/SKILL.md) | Dev loop for the migrator in this repo: reset a target site workspace to its Fresh/Deno state (origin/main) and run the local packages/blocks-cli migrate script against it. Use when testing a migrator change on a real site, or asked to 'run the migration' on a site from here. Not for sites already on TanStack that only need the @decocms/start 6.x → 7.x upgrade (use upgrades-decocms-v6-to-v7 in decocms/migrations). | -_7 skill(s). This table is generated by `scripts/skills-readme.ts` — edit the skill, not the row._ +_8 skill(s). This table is generated by `scripts/skills-readme.ts` — edit the skill, not the row._ diff --git a/.agents/skills/apps-nuvemshop/SKILL.md b/.agents/skills/apps-nuvemshop/SKILL.md new file mode 100644 index 000000000..3d58512f5 --- /dev/null +++ b/.agents/skills/apps-nuvemshop/SKILL.md @@ -0,0 +1,41 @@ +--- +name: apps-nuvemshop +description: "@decocms/apps-nuvemshop (packages/apps-nuvemshop): the Nuvemshop/Tiendanube commerce app over the headless Storefront API (storefront-api.tiendanube.com v2026-11) plus the store's own session endpoints and the Admin API. Use when touching nuvemshopGet/nuvemshopPost, PRODUCT_FIELDS/CATEGORY_FIELDS, productListingPage facets/sort (applyListing, sort_by, ?Cor=A|B), toProduct/variant prices, relatedProducts, the account actions (login/logout/register, user loader, store_session_payload cookie, adminToken, Turnstile), nuvemshopSitemap, createCheckout, or the deco-nuvemshop block; or when products come back with only id/name/handle, a category tree is flat, filters/sorts don't match the theme, links redirect (trailing slash), registration fails silently (reCAPTCHA), login says 'Valide seu e-mail', or add-to-cart (/comprar/) returns 403." +--- + +# @decocms/apps-nuvemshop + +Nuvemshop as a framework-agnostic package. Source: `packages/apps-nuvemshop/src/`. +Three upstreams, each covering what the others can't: + +| Upstream | Client | Used for | +|---|---|---| +| Storefront API (`storefront-api.tiendanube.com/v2026-11/stores/`) | `client.ts` (`nuvemshopGet` cached, `nuvemshopPost`) | catalog, search, categories, single-variant shipping, `POST /checkouts` | +| The store's own domain (`storeUrl`) | `store.ts` (`storeFetch`, cookie bridge) | login, logout, logged-in account page | +| Admin API (`api.nuvemshop.com.br/v1/`) | `admin.ts` (`nuvemshopAdmin`, `adminToken`) | customer registration (no captcha), customer profile | + +Everything egresses through `createNuvemshopFetch` by default (no `setXFetch` +wiring needed); Storefront GETs go through `createFetchCache`. + +## When to load what + +| Symptom / task | Load | +|---|---| +| Products/categories missing fields, 401 with a token, rate limits, what the API can't do | [`api.md`](./references/api.md) | +| PLP filters, sort, pagination, related products, colors, URLs that redirect | [`listing.md`](./references/listing.md) | +| Login/logout/register/user, cookies, `adminToken`, Turnstile | [`account.md`](./references/account.md) | +| Cart, add-to-cart 403, checkout (API vs classic), checkout proxy | [`cart-and-checkout.md`](./references/cart-and-checkout.md) | + +## Rules that the code alone doesn't show + +- **Always send `fields=`.** The API's default selection is `id,name,handle` + for products *and* categories; without `parent` the category tree goes flat. + The client adds `PRODUCT_FIELDS`/`CATEGORY_FIELDS` per route — don't bypass it. +- **URLs have no trailing slash.** `createDecoRouter` strips it, so + `/produtos/x/` would answer with a redirect and a canonical that never 200s. + Loaders still accept slashed paths (old Nuvemshop links). +- **Tests are written against captured API responses** (`src/__fixtures__`) + typed with the API types: a response-shape drift fails `tsc`. Re-capture + instead of hand-editing fixtures; anonymize third-party stores. +- **No customer data or real tokens in this repo** — fixtures come from the + demo store; tokens live in site secrets. diff --git a/.agents/skills/apps-nuvemshop/references/account.md b/.agents/skills/apps-nuvemshop/references/account.md new file mode 100644 index 000000000..b4d10818e --- /dev/null +++ b/.agents/skills/apps-nuvemshop/references/account.md @@ -0,0 +1,26 @@ +# Accounts: register, login, logout, user + +No API covers customer sessions. What works, validated against a live store: + +| Action | Upstream | Notes | +|---|---|---| +| `actions/account/register` | Admin API `POST /customers` `{name,email,password,send_email_invite:false}` | The store's form needs a reCAPTCHA bound to its domain — posting it without the token is **silently rejected** (302 back, no account). The Admin API has no captcha, so the action requires Cloudflare Turnstile (`turnstileSecret`) unless `allowUnverifiedRegistration` (demos). Duplicate email → 422 → `email_taken`. | +| `actions/account/login` | store `POST /account/login/` (email, password) | Success = 302 to `/account/`. Failure = 302 back to the login page, whose HTML carries the reason: `js-login-general-error` (wrong credentials) or `js-account-validation-pending` (email not validated — every new account must click the email link first). The page also contains hidden copies of every message, so match the class, not the text. | +| `actions/account/logout` | store `GET /account/logout/` | | +| `loaders/user` | store `GET /account/` → `LS.customer = `; Admin API `GET /customers/` | `null` when there's no `store_*` cookie or `/account/` redirects to login. | + +## Cookies + +The session is the store's `store_session_payload_` (rewritten on +login) + `store_login_session`. `store.ts`: +- sends upstream only the request's `store_*` cookies (never the site's own); +- re-emits only `store_*` `Set-Cookie` into `RequestContext.responseHeaders` + with `Domain` rewritten to our host (the store's Cloudflare `__cf_bm`/`_cfuvid` + are dropped). Same cookie names as the store, so a checkout proxy would carry + the same session. + +## Admin token + +`adminToken` is a custom-app token, server-only, often created with full +access. Never return it or Admin API payloads wholesale to the browser — +`user` returns only id/name/email/phone of the session's own customer. diff --git a/.agents/skills/apps-nuvemshop/references/api.md b/.agents/skills/apps-nuvemshop/references/api.md new file mode 100644 index 000000000..f26793f79 --- /dev/null +++ b/.agents/skills/apps-nuvemshop/references/api.md @@ -0,0 +1,43 @@ +# Storefront API — what it gives and what it doesn't + +OpenAPI 0.5.0, `v2026-11`. Routes: `GET /products` (browse), `GET /search/products`, +`GET /products/{id|handle}`, `GET /categories[/{id|handle}]`, +`POST /shipping-options`, `POST /checkouts`. That's all — no cart, customer, +facets, promotions, installments or sitemap. + +## Fields + +The default selection is `id,name,handle`. `client.ts` adds, per route: +- products: `PRODUCT_FIELDS` (images, variants, attributes, categories, brand, + tags, seo_*, free_shipping, video_url, created_at). `cost` is never requested. +- categories: `CATEGORY_FIELDS` (adds `parent`, `subcategories`, seo_*). + +An invalid field answers `400 invalid_fields` listing the bad ones — that's +how the valid set was discovered. `custom_fields` is store-gated +(`custom_fields_unavailable`). + +## Prices and stock (per variant, decimal strings) + +- sale price = `promotional_price ?? price`; list price = `max(compare_at_price, price)` + (`compare_at_price` equals `price` when there's no discount). +- `stock: null` means unlimited; available = `!stock_management || stock == null || stock > 0`. +- `values[i]` pairs with the product's `attributes[i]` (e.g. `["P","Azul"]` ↔ `["Tamanho","Cor"]`). +- No currency in the payload — it's config (`currency`, default BRL). + +## Query rules + +- `ids`/`handles` (≤30) can't be combined with `sort_by`; results keep request order. +- `sort_by` accepts only `best-selling | price-ascending | price-descending` (else 400). +- `category_id` includes subcategories. Unknown params are silently ignored. +- `/search/products` returns only the first page (≤200), no `sort_by`. +- Handles are per language; there's no `lang` param. + +## Auth and limits + +- Tokenless: 120 req/min per store + client IP. From a Worker every buyer shares + egress IPs, so production should use a token. +- Storefront token (from Nuvemshop's account team): 1,200/min per store; + `X-LinkedStore-Buyer-IP` gives each buyer their own 120 bucket. The client sends + it only on POSTs, only with a token. +- A custom-app token (Configurações → Aplicativos sob medida) is an **Admin API** + token: the Storefront API rejects it with `401 "A valid Storefront token is required"`. diff --git a/.agents/skills/apps-nuvemshop/references/cart-and-checkout.md b/.agents/skills/apps-nuvemshop/references/cart-and-checkout.md new file mode 100644 index 000000000..f3a04ca5a --- /dev/null +++ b/.agents/skills/apps-nuvemshop/references/cart-and-checkout.md @@ -0,0 +1,37 @@ +# Cart and checkout + +## What exists + +- **API**: `POST /checkouts {line_items, coupon_code?}` → `checkout_url` + (`actions/createCheckout`). No cart, no totals/coupon preview before it. +- **Classic (theme) endpoints**, form-urlencoded + `X-Requested-With`, no CSRF: + - add: `POST /comprar/` `add_to_cart=&variation[i]=&quantity=N` + (variant *values*, not the variant id) → `{success, item, cart}`; + - change/remove: `POST /cart/update/` `quantity[]=N` (0 removes); + - read: `POST /cart/update/` with no body is `400 empty_quantities`; a + non-existent line (`quantity[1]=1`) returns the cart unchanged; no cart → + `400 cart_not_found`; + - coupon: `POST /cart_coupon/` `coupon_code` (`400 coupon_not_found`), + `/cart_coupon_remove/`; shipping: `/cart/save_shipping/`. + - Session = `store_session_payload_` + `store_login_session` + (`store_cart_session_` alone isn't enough). + +## The `/comprar/` IP block + +~15 carts created from one IP in a few minutes got that IP a `403 +{"success":false}` on every add-to-cart — application-level, not a Cloudflare +challenge, including a real browser clicking the theme's button, for 35+ min. +No merchant setting changes it. A server-side proxy sends every buyer through +the Worker's egress IPs, so this can block a whole store's add-to-cart. The +API path (`/checkouts` with a Storefront token + buyer IP) doesn't have this. +Ask Nuvemshop before shipping a classic-cart proxy. + +## Checkout proxy (spike) + +Proxying `/checkout/*` (and the cart endpoints) to the store with `Set-Cookie` +`Domain` and `Location` rewritten renders the hosted checkout on our domain +identically to the store's own (contact → CEP → address → shipping → payment); +its API (`checkout-api.ms.tiendanube.com`) echoes any CORS origin with +credentials. Console warnings (checkout-security postMessage, report-only CSP, +Turnstile) appear on the store's own domain too. Payment itself was not +exercised (demo store without an active gateway). diff --git a/.agents/skills/apps-nuvemshop/references/listing.md b/.agents/skills/apps-nuvemshop/references/listing.md new file mode 100644 index 000000000..26ecc4ba7 --- /dev/null +++ b/.agents/skills/apps-nuvemshop/references/listing.md @@ -0,0 +1,41 @@ +# PLP: facets, sort, pagination, related products + +The API has no facets and only three sorts, but the Nuvemshop core (the theme) +has both. `utils/listing.ts` (`applyListing`) reproduces the theme in memory over +a window of ≤200 products (`LISTING_WINDOW`) — verified equal to the theme's +counts and order on live search/category cases. Past 200 products per +category/search it is wrong; switch to native API facets when they exist. + +## URL contract (same as the theme, so old links keep working) + +- Filters: `?=`, several values OR-ed with `|` + (`?Cor=Vermelho|Preto`); AND across attributes. Repeated params are NOT the + theme's format (it keeps only the last). Price: `min_price`/`max_price`. +- Sort: `?sort_by=` with the theme's values (`alpha-*`, `created-*`, + `best-selling`, `price-*`) plus our `discount-descending`. Only `best-selling` + is delegated to the API; the rest sort in memory with newest-first ties. +- Default category order: the API returns oldest-first and doesn't expose the + category's configured sort, so `defaultSort` (block prop, default + `created-descending`) applies; `?sort_by=user` (manual order) falls back to it. +- Search keeps API relevance and drops `best-selling`. +- Facet counts ignore their own attribute's selection; zero-count values are hidden. + +## Paths + +`/produtos/` (`?variant=` selects a variant) and nested +categories `//`, no trailing slash. The PLP resolves the category +from the last path segment. + +## Related products + +`relatedProducts` follows the theme's fallback rule: same category, without the +product, in-stock first, refilled with out-of-stock, max 8. No shuffle (keeps it +cacheable). The theme first prefers ids from a related-products app metafield, +which the API doesn't expose. + +## Colors + +The theme paints swatches from a hex the core attaches to palette colors +(`custom_data`); the API returns only the name. Color → hex is site config (a +CMS loader); free-text colors ("Vermelho e Preto") never had a hex in the theme +either — it shows "+N" / "N cores". diff --git a/CLAUDE.md b/CLAUDE.md index cdd7faf49..6bb0e561c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,6 +133,7 @@ One folder, `.agents/skills/`, and only skills that describe the framework and i | Touching / debugging | Skill | |---|---| | `packages/apps-vtex/**`, cart (`useCart`, `createCart`, orderForm), VTEX cookies/auth, sales channel | `apps-vtex` | +| `packages/apps-nuvemshop/**`, Nuvemshop Storefront API fields/facets/sort, store login/register/`store_session_payload`, `/comprar/` 403 | `apps-nuvemshop` | | Any cache layer: `workerEntry.ts`, `sdk/cacheHeaders.ts`, `sdk/cachedLoader.ts`, `sdk/fetchCache.ts`, layout caches in `cms/resolve.ts`/`cms/sectionLoaders.ts`, `staleTime`/`gcTime`, `X-Cache` | `deco-caching` | | A site's `src/routes/*`, `cmsRouteConfig`, admin route configs, SEO/head | `deco-cms-route-config` | | `invoke.gen.ts`/`invoke.ts`, `createServerFn`, CORS on cart/checkout | `deco-server-functions-invoke` | diff --git a/packages/apps-nuvemshop/src/__tests__/account.test.ts b/packages/apps-nuvemshop/src/__tests__/account.test.ts new file mode 100644 index 000000000..05f8bb93b --- /dev/null +++ b/packages/apps-nuvemshop/src/__tests__/account.test.ts @@ -0,0 +1,250 @@ +import { RequestContext } from "@decocms/blocks/sdk/requestContext"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import login from "../actions/account/login"; +import logout from "../actions/account/logout"; +import register from "../actions/account/register"; +import { configureNuvemshop, setNuvemshopFetch } from "../client"; +import user from "../loaders/user"; + +const STORE = "https://demodeco.lojavirtualnuvem.com.br"; +const SITE = "https://www.loja.example"; +const ADMIN = "https://api.nuvemshop.com.br/v1/8336778"; + +type Route = (url: URL, init: RequestInit) => Response | Promise; +let routes: Record = {}; +const fetchMock = vi.fn(async (input: string | URL | Request, init: RequestInit = {}) => { + const url = new URL(String(input)); + const key = `${init.method ?? "GET"} ${url.origin}${url.pathname}`; + const route = routes[key]; + if (!route) throw new Error(`unexpected ${key}`); + return route(url, init); +}); +const calls = (key: string) => + fetchMock.mock.calls.filter( + ([u, i]) => + `${i?.method ?? "GET"} ${new URL(String(u)).origin}${new URL(String(u)).pathname}` === key, + ); +const html = (body: string, init: ResponseInit = {}) => + new Response(body, { ...init, headers: { "content-type": "text/html", ...init.headers } }); +const redirect = (location: string, cookies: string[] = []) => { + const headers = new Headers({ location }); + for (const c of cookies) headers.append("set-cookie", c); + return new Response(null, { status: 302, headers }); +}; +const SESSION_SET = `store_session_payload_8336778=NEW; expires=Thu, 05-Nov-2026 23:56:42 GMT; path=/; domain=demodeco.lojavirtualnuvem.com.br; secure; HttpOnly; SameSite=Lax`; +const inRequest = (fn: () => Promise, cookie = "") => + RequestContext.run(new Request(`${SITE}/deco/invoke/x`, { headers: { cookie } }), async () => { + const result = await fn(); + return { result, setCookies: RequestContext.current!.responseHeaders.getSetCookie() }; + }); + +beforeEach(() => { + fetchMock.mockClear(); + routes = {}; + configureNuvemshop({ storeId: "8336778", storeUrl: STORE, adminToken: "admintoken" }); + setNuvemshopFetch(fetchMock as unknown as typeof fetch); +}); + +describe("login", () => { + it("posts the classic form and forwards the store session cookie onto our domain", async () => { + const CF = + "__cf_bm=abc; HttpOnly; SameSite=None; Secure; Path=/; Domain=lojavirtualnuvem.com.br"; + routes[`POST ${STORE}/account/login/`] = () => redirect(`${STORE}/account/`, [SESSION_SET, CF]); + const { result, setCookies } = await inRequest( + () => login({ email: "a@b.com", password: "pw" }), + "store_session_payload_8336778=OLD; _ga=GA1.1; other=x", + ); + expect(result).toEqual({ ok: true }); + + const [, init] = calls(`POST ${STORE}/account/login/`)[0]; + expect(String(init!.body)).toBe("email=a%40b.com&password=pw"); + const headers = new Headers(init!.headers); + // only the store's own cookies go upstream — never the site's (_ga, other) + expect(headers.get("cookie")).toBe("store_session_payload_8336778=OLD"); + expect(headers.get("origin")).toBe(STORE); + expect(init!.redirect).toBe("manual"); + + // only the store session is re-emitted on our domain (not the store's Cloudflare cookies) + expect(setCookies).toEqual([ + SESSION_SET.replace("domain=demodeco.lojavirtualnuvem.com.br", "Domain=www.loja.example"), + ]); + }); + + it.each([ + [ + '