Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .agents/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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._
<!-- skills:end -->
41 changes: 41 additions & 0 deletions .agents/skills/apps-nuvemshop/SKILL.md
Original file line number Diff line number Diff line change
@@ -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/<id>`) | `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/<id>`) | `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.
26 changes: 26 additions & 0 deletions .agents/skills/apps-nuvemshop/references/account.md
Original file line number Diff line number Diff line change
@@ -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 = <id>`; Admin API `GET /customers/<id>` | `null` when there's no `store_*` cookie or `/account/` redirects to login. |

## Cookies

The session is the store's `store_session_payload_<storeId>` (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.
43 changes: 43 additions & 0 deletions .agents/skills/apps-nuvemshop/references/api.md
Original file line number Diff line number Diff line change
@@ -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"`.
37 changes: 37 additions & 0 deletions .agents/skills/apps-nuvemshop/references/cart-and-checkout.md
Original file line number Diff line number Diff line change
@@ -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=<productId>&variation[i]=<value>&quantity=N`
(variant *values*, not the variant id) → `{success, item, cart}`;
- change/remove: `POST /cart/update/` `quantity[<lineId>]=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_<id>` + `store_login_session`
(`store_cart_session_<id>` 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).
41 changes: 41 additions & 0 deletions .agents/skills/apps-nuvemshop/references/listing.md
Original file line number Diff line number Diff line change
@@ -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: `?<Attribute>=<value>`, 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/<handle>` (`?variant=<id>` selects a variant) and nested
categories `/<parent>/<child>`, 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".
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
Loading
Loading