chore: sync next with main#77
Closed
nicacioliveira wants to merge 40 commits into
Closed
Conversation
@decocms/start@5.3.0 was promoted to @latest in decocms/deco-start#184 (o11y RC stack rebased onto the post-revert src/-exports baseline). Bump the peer/dev pins from 5.3.0-rc.2 to 5.3.0 so semantic-release on main cuts @decocms/apps@1.15.0 against the stable framework. Verified locally: - npm install resolves @decocms/start@5.3.0 (registry override required; global .npmrc still points at the bawclothing GH Packages registry). - npm run typecheck clean. - npm test 413/413 passing. Co-authored-by: Cursor <cursoragent@cursor.com>
chore(deps): pin @decocms/start to 5.3.0 stable + promote next to 1.15.0
When a forwarder builds `init` with `headers: new Headers(...)` and
that init flows through `getVtexFetch()` (as `createVtexCheckoutProxy`
does since 1.15.0), the framework's own header-merge logic silently
dropped the cookies. The naive `{ ...authHeaders, ...init?.headers }`
spread collapses a `Headers` instance to `{}` (Headers has no own
enumerable entries), wiping the browser's full Cookie header on its
way to VTEX.
Fix: funnel all per-call header merges through a `mergeHeaders` helper
backed by the `Headers` constructor, which correctly absorbs every
`HeadersInit` shape (Headers / string[][] / Record). Apply the same
treatment to `vtexCachedFetch` and `intelligentSearch` so their
`_fetch` callsites get vtex_segment forwarding too — covering the
last gaps that previously forced sites to wrap `setVtexFetch` with
their own (often Headers-unsafe) cookie injectors.
Regression test asserts an existing `Cookie` header on a `Headers`-
typed init survives the full vtexFetchResponse path verbatim. Two
new describe blocks exercise the cached-GET and IS paths.
418/418 tests pass; typecheck clean.
Co-authored-by: Cursor <cursoragent@cursor.com>
fix(vtex): merge headers Headers-aware so init.headers Cookie survives
Recent releases have failed silently — `npm publish` does no OIDC token exchange, posts anonymously, and the registry returns a misleading 404. The publish step's log shows zero OIDC/provenance/trusted-publisher traces, meaning the npm CLI isn't even attempting the handshake. Adding `--provenance` makes the OIDC code path mandatory: if the runtime is missing the required env vars (e.g. `id-token: write` not being honored by the runner), npm fails fast with a precise error instead of silently degrading to anonymous publish. No-op when OIDC is working — the previous behavior already auto-signed provenance on successful runs; this just makes the requirement explicit and the failure mode visible. Co-authored-by: Cursor <cursoragent@cursor.com>
fix(ci): publish with explicit --provenance to surface OIDC errors
The previous --provenance addition confirmed OIDC signing works end-to-end, but the registry PUT still returns the masked "is not in this registry" 404. Silly logging dumps the actual HTTP request + response so the next failure exposes whatever npm is comparing the OIDC subject against on the server side. Co-authored-by: Cursor <cursoragent@cursor.com>
Provenance signing confirms the OIDC token is accepted by sigstore. The masked 404 npm returns to the PUT publish call provides no additional context. This step replicates the token-exchange step that npm CLI does internally before each publish, against both @decocms/apps (failing) and @decocms/start (working) — so we can compare npm's responses side-by-side from the exact same OIDC token in the same workflow run. Nothing about the publish itself is changed; this only adds a diagnostic step. The signature half of the OIDC JWT is never logged. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…working publish 3e0ee95 (1.15.0-next.1 publish, May 18) is the last successful CI publish on this repo. Every change I made today since — provenance flag, loglevel=silly, OIDC-claims probe, token-exchange probe — was diagnostic noise on top of a config that was already working. Strip all of it; this commit makes .releaserc.json and release.yml on main byte-identical to 3e0ee95, leaving the runtime exactly as it was the last time CI successfully published @decocms/apps. If this still 404s, the failure is not in our config and the next move is npm support. Co-authored-by: Cursor <cursoragent@cursor.com>
The publish-failure chain that started May 19 produced two orphan git tags (v1.15.2 and v1.15.3) without matching npm versions or GitHub Releases, so subsequent runs saw them as already-released and skipped the publish. The orphan tags are now deleted; this empty fix commit exists to land a Conventional Commit type that triggers semantic-release to attempt the publish again on the byte-identical-to-3e0ee95 config. The actual fix being released is the cookie-merge fix from PR #53. Co-authored-by: Cursor <cursoragent@cursor.com>
Root cause of the May 19 publish-404 chain is the npm registry's deprecation of the npm 10 OIDC handshake, not a config drift on our side and not the residual state left by manual publishes (those were just coincident in time). Two interacting issues, both documented in npm/cli#8976 and npm/cli#8730 within the last week: 1. Node 22 ships with npm 10, whose OIDC handshake the registry has deprecated. npm 11 (Node 24) implements the supported handshake. 2. `registry-url` on actions/setup-node writes an .npmrc with `_authToken=${NODE_AUTH_TOKEN}` and exports a placeholder NODE_AUTH_TOKEN. npm CLI prefers that (anonymous-stub) token over the OIDC token exchange, surfacing as the same masked 404. Together these turn a perfectly valid OIDC trusted-publisher setup into a "404 Not Found - PUT … is not in this registry" error, while sigstore still happily signs provenance against the OIDC token. This commit aligns the release job with the working pattern shared in npm/cli#8976 (May 15 2026): Node 24, no registry-url, semantic- release publishes via plain `npm publish`. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…nance + verbose Co-authored-by: Cursor <cursoragent@cursor.com>
deco-start publishes successfully via OIDC trusted publishing while apps-start has been failing with a masked PUT 404 since May 19. Side- by-side diff revealed apps-start was diverging from the working pattern in several places that touch OIDC validation. Bring the release pipeline back into byte-for-byte alignment with deco-start: - Drop workflow_dispatch and the `next` branch from the on: triggers. npm docs explicitly call out workflow_dispatch as a source of OIDC claim/validation mismatches. - Drop `next` channel + prerelease config from semantic-release branches. Single-branch release flow matches the working repo. - Revert publishCmd to the plain `npm publish --access public` deco-start uses; no --provenance flag (npm auto-attaches provenance for OIDC publishes from public repos), no dynamic --tag. - Drop `npm ci` + `npm test` in favour of setup-bun + `bun install --frozen-lockfile` + `bunx semantic-release`, again matching deco-start. Replace package-lock.json with bun.lock. - Revert Node back to 22 to fully match. The Node 22→24 + npm 10→11 lead from npm/cli#8976 reproduced the same 404, so it wasn't the actual root cause; the divergence from the working pipeline was. If this still 404s, the cause is package-scoped server-side state on @decocms/apps (the manual publishes since May 19 left @latest owned by crazydevil rather than the OIDC GitHub Actions identity) and the next move is npm support, not more config tweaks. Co-authored-by: Cursor <cursoragent@cursor.com>
Closes the framework-level VTEX Set-Cookie propagation gap for sites
consuming `@decocms/apps` via `createServerFn` action handlers. VTEX's
`checkout.vtex.com` and `CheckoutOrderFormOwnership` cookies were
silently dropped on the action path, letting the storefront's local
orderForm reference drift away from VTEX's server-side orderForm.
Three fixes in one PR:
1. Cart-adjacent actions switched to `vtexFetchWithCookies`. Seven
`/api/checkout/...` actions previously used the plain `vtexFetch`
helper, which does not capture VTEX's `Set-Cookie` headers onto
`RequestContext.responseHeaders`. The bridge in generated
`invoke.gen.ts` had nothing to forward. Updated:
`simulateCart`, `setShippingPostalCode` (per VTEX docs, can rotate
the ownership cookie), `getInstallments`, `updateItemPrice`,
`changeToAnonymousUser`, `clearOrderFormMessages`,
`getSellersByRegion`.
2. Headers-aware merge in `vtexFetchWithCookies`. The previous
implementation cast `init.headers` to `Record<string, string>` and
spread it into a new object to inject the auto-forwarded `Cookie`
header. When the caller passed a `Headers` instance, the spread
collapsed to `{}` and silently wiped every other header the caller
had set (auth, content-type, trace context). Replaced with a
`withCookieHeader(headers, cookieValue)` helper backed by the
`Headers` constructor — the same Headers-aware fix that landed in
#53 for `vtexFetchResponse`, applied here for completeness.
3. Restore `vtex/invoke.ts` as the generator contract. The file was
deleted on 2026-03-30 (commit 0303cbb) on the assumption that
`setupApps()` would auto-register handlers from the manifest. But
the framework's `@decocms/start/scripts/generate-invoke.ts` still
scans this file as its source of truth, so sites running
`npm run generate:invoke` against any apps version >= 1.15.0 hit a
hard "invoke.ts not found" error. Restored at the current action
shapes (single-props-object call convention, no `VtexFetchResult`
unwrapping). All 17 actions previously exposed are re-exposed here.
Paired with a matching `@decocms/start` major that emits a
`forwardResponseCookies()` bridge in `invoke.gen.ts`. Sites get the
full fix after bumping both packages and re-running
`npm run generate:invoke`.
BREAKING CHANGE: requires the matching `@decocms/start` major
containing the `forwardResponseCookies()` emit. Sites bumping
`@decocms/apps` alone gain the per-action cookie capture but won't
propagate Set-Cookies to the browser until they also bump
`@decocms/start` and regenerate `src/server/invoke.gen.ts`.
Co-authored-by: Cursor <cursoragent@cursor.com>
feat(vtex)!: forward Set-Cookie through cart-adjacent actions
…ault (#57) * fix(vtex): include image and inventoryLevel on variant entries by default Variant selectors on PDPs read hasVariant[i].image[0].url and hasVariant[i].offers.offers[0].inventoryLevel.value to render thumbnails and gate per-SKU stock state. The lean variant transform was dropping image[] entirely and hard-zeroing inventoryLevel, so every migrated site showed empty squares and "Restam 0 uni" on every variant — and the buy button stayed disabled. Changes: - PDP inline-loader now defaults leanVariants to false (PDPs typically want full variants; payload size only matters on PLP/shelf). - toProductVariant honors two new flags: variantIncludeImage (default true) → image[0] variantIncludeInventory (default true) → real offer.inventoryLevel Both can be opted out to recover the old strict-lean behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ci): switch CI install/run from npm to bun The repo's lockfile is bun.lock; release.yml already uses bun install --frozen-lockfile. ci.yml was still on npm ci, which fails immediately because there's no package-lock.json. Aligning both workflows on the same toolchain. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
`checkout.vtex.com` was written at two different scopes: the checkout proxy domain-scopes it to the storefront host (rewriteSetCookieDomain), while vtexFetchWithCookies stripped the Domain attribute, producing a HOST-ONLY cookie. Browsers treat host-only and domain-scoped cookies as distinct keys, so the two writers could not overwrite each other: a returning user accumulated two `checkout.vtex.com` cookies holding different orderForm ids, and VTEX read whichever was sent last (RFC 6265 ordering, by creation time) — a nondeterministic empty-cart at checkout. Part 1 (client.ts): vtexFetchWithCookies now rewrites the Domain to the active request host instead of stripping it, matching the proxy and native VTEX. Both writers land on the same cookie key, so the newest write always wins. Falls back to stripping only outside a request scope. Part 2 (proxy.ts): on GET checkout-UI navigations, canonicalize from the storefront's source-of-truth `checkout.vtex.com__orderFormId` mirror — re-assert the domain-scoped cookie and expire any stale host-only variant left by older releases. Gated to GET so it never fights a deliberate server-side orderForm change (login merge, changeToAnonymousUser). No-op once a browser jar has cycled. This heals dirty jars without per-site shims. Co-authored-by: Cursor <cursoragent@cursor.com>
Harden two cookie regexes flagged in review: - rewriteCookieDomain: scope the Domain rewrite to an attribute boundary (`;\s*domain=`) so a `domain=` substring inside the cookie value is not corrupted. - proxy mirror match: anchor `checkout.vtex.com__orderFormId` to a cookie-name boundary (start or `;\s*`) so a decoy embedding the string inside another cookie's value cannot be captured. Adds regression tests for both (value-embedded `domain=`, decoy mirror). Co-authored-by: Cursor <cursoragent@cursor.com>
…tion fix(vtex): unify checkout cookie scope to eliminate orderForm drift
…t Part 2) The checkout-UI canonicalization added in #58 is destructive in production and is the cause of the orderForm divergence it was meant to prevent. Two empirical findings (curl against a live preview): 1. Cloudflare Workers collapses two same-name `Set-Cookie` headers in one response, keeping the LAST. The block emitted, in order, a canonical `checkout.vtex.com=__ofid=<mirror>` re-assert followed by a `checkout.vtex.com=; Max-Age=0` expiry — so the re-assert was dropped on the wire and only the expiry survived. Net effect on every GET /checkout that carried the mirror cookie: `checkout.vtex.com` AND `CheckoutOrderFormOwnership` were force-expired. VTEX then minted a fresh orderForm, diverging from the (stale) JS mirror. 2. The premise was wrong anyway: the `checkout.vtex.com__orderFormId` mirror is NOT a reliable source of truth — it can be stale relative to VTEX's actual orderForm, so forcing it would make VTEX reject and re-mint regardless. This is why incognito worked (no mirror cookie yet) but returning users broke (mirror present → cookies wiped on checkout entry). Part 1 (unifying the cookie SCOPE via domain-rewrite in `vtexFetchWithCookies`, #58) is correct and is retained. Legacy host-only residue cleanup, if needed, must use a separately-verified mechanism — a server-side guess cannot distinguish stale residue from an authoritative host-only cookie, nor reliably emit a same-name set+expire. Co-authored-by: Cursor <cursoragent@cursor.com>
…ookie-canonicalization fix(vtex): remove destructive checkout cookie canonicalization (revert Part 2)
…61) * feat(magento): initial scaffold port from deco-cx/apps to apps-start Starts the Magento app port to @decocms/apps following the same shape as vtex/ and shopify/. This first commit lands the configure surface and 2 reference loaders (features, cart) — enough to unblock real consumer sites; the remaining ~20 loaders/actions are itemized in magento/README.md and will follow in separate commits. Contents: - magento/client.ts — configureMagento({ baseUrl, apiKey, storeId, ... }) with module-global state, mirroring configureVtex. magentoFetch() helper attaches Bearer auth + Referer + optional x-origin-header on every call. initMagentoFromBlocks(blocks) resolves the CMS block's `__resolveType: "website/loaders/secret.ts"` references via env vars, same fallback granadobr-tanstack already uses in production. - magento/types.ts — MagentoCart + MagentoCartItem shapes used by the ported loaders. - magento/loaders/features.ts — returns the resolved Features block from the config. The simplest loader, used as the smoke test. - magento/loaders/cart.ts — fetches the customer's active cart by reading `dataservices_cart_id` cookie via @decocms/start/sdk/cookie and calling the Magento admin REST endpoint. Returns null for anonymous sessions (expected) and on 404 (expired cookie). - magento/middleware.ts — passthrough today; the legacy changeCardIdAfterCheckout reconciliation flow is deferred to a follow-up PR. - magento/index.ts — re-export entry, parallel to vtex/index.ts. - package.json — adds @decocms/apps/magento/* exports. Motivation: deco-sites/granadobr-tanstack (Granado, Brazilian cosmetics) just migrated from Fresh/Deno to TanStack Start. Its parity audit reported a HIGH severity finding: invoke(magento/loaders/features) failed: handler not found …because no @decocms/apps/magento/* resolver existed. The site is working around it with an in-site adapter that re-uses the legacy mod.ts shape; this PR begins the upstream fix so the next Magento storefront doesn't need to repeat the work. Companion issue: #60 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(magento): address 3 cubic-dev-ai review findings (all P1) 1. package.json: `magento/` was in `exports` but not `files`. npm publish excludes any path not listed under `files`, so the entire magento folder would be missing from the published tarball — every consumer site would get module-resolution errors. Added `magento/` to the array alongside commerce/shopify/vtex/etc. 2. magento/loaders/cart.ts: cartId comes from a request cookie (`dataservices_cart_id`) and was spliced into the admin REST path unencoded. Wrapped both `site` and `cartId` in encodeURIComponent so user input can't break out of the path segment to tack on query or fragment parts that would hit a different endpoint with the privileged Bearer token attached. 3. magento/client.ts: magentoFetch() accepted any `path` value, including absolute URLs to arbitrary hosts. The auth headers (Bearer, Referer, x-origin-header) were attached unconditionally, so a caller passing `https://attacker.example/...` would leak the Magento admin token. Now constructs a URL up front, compares its origin against the configured baseUrl, and force-disables `authenticated` when the request is cross-origin. Genuine third-party calls remain possible via the explicit `authenticated: false` opt-out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(magento): biome formatting + add magento/** to lint includes Biome's includes array was missing magento/**, so the new package was skipped by lint entirely. Added it to the includes alongside the other apps. Also ran biome --write to convert space indentation to tab (project convention) across the 5 magento source files. Note: the test job also surfaces pre-existing lint errors in vtex/__tests__/ client-set-cookie-forward.test.ts and website/components/__tests__/ OneDollarStats.test.ts. Those are unrelated to this PR — they were already failing on main before any magento file was added. Splitting that into a separate PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(magento): strip ALL Magento-identity headers in cross-origin requests Cubic's second pass caught that the previous fix only suppressed the Bearer token but still leaked `x-origin-header` (an opaque secret) and a forced `Referer` pointing at the Magento storefront URL whenever a caller passed an absolute non-Magento URL. Now `buildHeaders` takes an explicit `attachMagentoIdentity` flag. `magentoFetch` sets it to `target.origin === baseUrl.origin`, so a cross-origin call gets only whatever headers the caller passed in their `opts.headers` — no Magento-side credentials, no origin-header secret, no forced Referer. Callers wanting a genuinely cross-origin GET should still flip `authenticated: false` at the call site for intent, but security no longer relies on that single flag being correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(magento): pin behavior parity with deco-cx/apps/magento (Fresh/Deno prod) Adds 21 unit tests across 3 files covering the surfaces this PR ports. Tests pin behavior the Fresh/Deno production code (deco-cx/apps/magento) established, so future refactors of the apps-start port can refactor freely without drifting from prod. magento/__tests__/client.test.ts (12 tests): configureMagento / getMagentoConfig: - throws before configure (no implicit defaults) - returns the configured value after configure magentoFetch — same-origin: - attaches Authorization + x-origin-header + forced Referer - resolves relative path against baseUrl - authenticated:false drops Bearer (still attaches origin/referer) - preserves caller-supplied Referer (no force-overwrite) magentoFetch — cross-origin guard: - strips Bearer (regression for cubic finding #1) - strips x-origin-header (regression for cubic finding #2) - strips forced Referer (regression for cubic finding #2) - forwards caller-supplied headers through - same-origin absolute URL still gets identity headers magento/__tests__/features.test.ts (2 tests): - returns the configured features object - returns {} when features omitted magento/__tests__/cart.test.ts (7 tests): - returns null when no cookie (matches prod's `if (!cartId) return null`) - reads dataservices_cart_id cookie (JSON-quoted form) - reads dataservices_cart_id cookie (raw form, no quotes) - props.cartId override beats the cookie - URL-encodes cartId — regression for cubic finding #3 (path injection) - 404 returns null (expired cookie) - non-404 errors throw - cart fetch attaches Bearer + x-origin-header (same-origin) Drive-by fix: cart loader was hitting /:site/V1/carts/:cartId, but Magento exposes the cart endpoint at /rest/:site/V1/carts/:cartId — the /rest/ prefix is mandatory and matches the legacy clientAdmin's typed key `GET /rest/:site/V1/carts/:cartId`. Tests pin the corrected path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…phql) (#62) Second PR in the magento-port series. Lands the pure-utility files that every remaining loader/action will depend on, before the heavier product loaders themselves. What's included (5 source files, 1 test pair = 2 files, 0 wire-ups needed in apps-start beyond the existing `./magento/utils/*` export): magento/client.ts + export interface FiltersGraphQL — URL search-param filter mapping (value: Magento attribute slug, type: EQUAL|MATCH|RANGE). Lives with the rest of the config types so utils/constants.ts and utils/graphql.ts share one source of truth. magento/utils/constants.ts (NEW) Verbatim port of deco-cx/apps/magento/utils/constants.ts: - URL_KEY: PDP slug attribute name - IN_STOCK / OUT_OF_STOCK: schema.org availability strings - DEFAULT_GRAPHQL_FILTERS: the 31-entry array of attribute/operator pairs that filtersFromUrlGraphQL crosses against URL params - REMOVABLE_URL_SEARCHPARAMS: keys stripped before forwarding URLs - Cart totals field names (GRAND_TOTAL/SUBTOTAL/DISCOUNT_AMOUNT/…) - Cookie names (SESSION_COOKIE/CUSTOMER_COOKIE/CART_COOKIE/ FORM_KEY_COOKIE) — single source of truth so loaders/actions/ middleware don't drift on string literals. magento/utils/stringifySearchCriteria.ts (NEW) Verbatim port. Flattens a nested Magento REST searchCriteria object into the bracketed query-string keys the API requires (searchCriteria[filterGroups][0][filters][0][field]=…). magento/utils/graphql-types.ts (NEW) Subset of deco-cx/apps/magento/utils/clientGraphql/types.ts — the input types graphql.ts produces. Extended as more loaders land. ProductFilterInput / FilterEqualTypeInput / FilterMatchType- Input / FilterRangeTypeInput / ProductSortInput / ProductSort / FilterProps / CustomFields. magento/utils/graphql.ts (NEW) Verbatim port (signature-preserving): - transformSortGraphQL({sortBy, order}) → ProductSortInput - filtersFromUrlGraphQL(url, customFilters?) → ProductFilterInput (URL params × DEFAULT_GRAPHQL_FILTERS + customFilters) - filtersFromLoaderGraphQL(fromLoader?) → ProductFilterInput - transformFilterGraphQL(url, customFilters?, fromLoader?) → merged ProductFilterInput (loader filters override URL on collision) - transformFilterValueGraphQL(value, "EQUAL"|"MATCH"|"RANGE") → typed filter input (RANGE splits on first underscore) - formatUrlSuffix(str) → normalize for Magento urlResolver (strip leading "/", ensure trailing "/") - getCustomFields(CustomFields, fallback?) → resolved attribute list for product projection Tests (24 new, all passing): magento/__tests__/stringifySearchCriteria.test.ts (4 tests) - top-level scalar flattening - nested object/array path style - multiple filterGroups (OR semantics) - empty input magento/__tests__/graphql.test.ts (20 tests) - transformSortGraphQL: 4 cases (no sortBy, default ASC, DESC, custom value) - transformFilterValueGraphQL: 4 cases (EQUAL/MATCH/RANGE base + RANGE multi-underscore edge case) - filtersFromUrlGraphQL: 5 cases (defaults, RANGE, MATCH, customFilters layering, empty) - filtersFromLoaderGraphQL: 2 cases (undefined → {}, collapses to keyed object) - transformFilterGraphQL merge order: 2 cases (collision: loader wins; non-colliding: merged) - formatUrlSuffix: 3 cases (strip leading /, append trailing /, no-op when already correct) - getCustomFields: 4 cases (active=false, overrideList present, fallback, default config) Total magento suite: 5 files / 49 tests / 191ms. The `./magento/utils/*` export entry in package.json already covers the four new util files — no exports-table edit needed in this PR. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#63) Third PR in the magento-port series (after #61 scaffold + #62 utils foundation). Lands the two simplest actions — both are self-contained HTTP/GraphQL POSTs with no Granado-specific dependencies. Files (4 new, 1 modified): magento/types.ts (modified) + NewsletterData { success, message } + ProductStockAlertResponse { data?: { productStockAlert: ... } } magento/actions/newsletter/subscribe.ts (new) POSTs to /rest/:site/V1/newsletter/subscribed with the customer email and the resolved storeId. Verbatim port of the legacy 3-arg loader, but reads config from getMagentoConfig() and uses magentoFetch (so auth/origin/Referer headers come along automatically on same-origin). Returns null on success:false or non-2xx; payload otherwise — matches prod behavior. magento/actions/product/stockAlert.ts (new) Fires the ProductStockAlert GraphQL mutation against <baseUrl>/ graphql. The legacy version called ctx.clientGraphql.query with a STALE cache hint, but mutations are write-only — no cache effect. Port omits the hint; behavior is observationally identical from a consumer's perspective. Returns { data: { productStockAlert } } or { error: string }. magento/__tests__/newsletter-subscribe.test.ts (new — 6 tests) - POSTs to encoded path - Body shape { email, store_id: number } - storeId coerced to number even when CMS holds it as string - Returns payload on success - Returns null on success:false (Magento failure shape) - Returns null on non-2xx HTTP status magento/__tests__/product-stockAlert.test.ts (new — 4 tests) - POSTs to <baseUrl>/graphql with the mutation body - Returns { data: { productStockAlert } } on success - Returns { error } when GraphQL payload lacks productStockAlert - Returns { error } when fetch throws Test totals: 7 files / 59 tests passing in ~165ms. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fourth PR in the magento-port series. Lands the customer-identity and
wishlist surface — depends on nothing from transform.ts so it goes in
parallel with the upcoming product-loaders PR.
Files (8 new, 12 new tests):
magento/utils/client/types.ts (NEW)
Subset of deco-cx/apps/magento/utils/client/types.ts:
Customer, CarbonoCustomer, CartUser, MinicartImprovements,
CustomerSectionLoad (the response bundle from
`/customer/section/load`), Wishlist, WishlistItem,
WishlistItemImage. Extended as more loaders land.
magento/utils/user.ts (NEW)
`getUserCookie(headers)` — reads PHPSESSID via
@decocms/start/sdk/cookie. Same helper the legacy code had under
Deno's std/http/cookie.
magento/loaders/user.ts (NEW)
Resolves the current customer into a schema.org Person. Calls
/customer/section/load?sections=customer,carbono-customer with
Cookie: PHPSESSID=<sid>. Returns null on missing cookie /
carbono.data_id absent / customer slice absent / non-2xx / thrown
fetch — defensive enough that the storefront just renders the
logged-out UI on any failure.
magento/loaders/wishlist.ts (NEW)
Resolves the visitor's wishlist via
/customer/section/load?sections=wishlist. Returns null when no
session cookie or no wishlist slice in the bundle.
magento/actions/wishlist/addItem.ts (NEW)
POSTs FormData {product, form_key} to /wishlist/index/add/. On
{success:true} delegates to wishlist loader for the refreshed
list. Null on missing PHPSESSID/form_key or any failure.
magento/actions/wishlist/removeItem.ts (NEW)
POSTs FormData {item, uenc:"", form_key} to /wishlist/index/remove/.
The `uenc:""` is preserved byte-for-byte from prod — Magento's
wishlist controller treats absent vs empty differently.
Tests (3 files, 12 new tests):
__tests__/user-loader.test.ts (8 tests)
- null when no PHPSESSID
- correct REST URL + Cookie header
- happy-path Person mapping (givenName/familyName from fullname)
- empty familyName when fullname == firstname
- null when carbono.data_id missing
- null when customer slice absent
- null when fetch throws
- null on non-2xx
__tests__/wishlist-loader.test.ts (5 tests)
- null when no PHPSESSID
- correct URL with ?sections=wishlist
- returns full wishlist payload
- null when bundle lacks wishlist slice
- null on non-2xx
__tests__/wishlist-actions.test.ts (8 tests)
addItem: null when PHPSESSID missing / form_key missing /
success:false / fetch throws; POSTs correct FormData; delegates to
loader on success.
removeItem: correct POST body (item + uenc:"" + form_key);
returns refreshed wishlist on success; null when PHPSESSID absent.
Test totals across the port: 8 files / 71 tests / 205ms.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ontrol (#65) Fifth PR in the magento-port series — the foundation that unblocks PDP/PLP/list/relatedProducts loaders. Imports nothing from the in-flight user/wishlist PR so the two can land in either order (there's a trivial merge in client/types.ts when both have landed — same file, disjoint sections, no actual conflict). Files (4 new + 1 modified, 22 new tests): magento/utils/constants.ts (modified) + MAX_RATING_VALUE / MIN_RATING_VALUE — used by transform.ts (and the eventual review/rating loaders) when mapping Magento's integer-rating scale into schema.org's 1–5 range. magento/utils/cacheTimeControl.ts (new) Verbatim port. SEARCH_PARAMS_TO_IGNORE (utm_*, gclid, fbclid, queryID, …) + filterSearchParams / filterSearchParamsFromURL. Product loaders pass request URLs through these before using them as cache keys so tracking parameters don't fragment the cache. magento/utils/client/types.ts (new — product types section) Subset of deco-cx/apps/magento/utils/client/types.ts focused on the PDP/PLP surface: CustomAttribute, CategoryLink, MagentoCategory, MagentoPriceInfo (full nested shape including extension_ attributes.{msrp, tax_adjustments, weee_attributes}), MagentoStock, MagentoImage, MediaEntry, MagentoProduct. Field names byte-for-byte match Magento's REST output — sites already render against them. magento/utils/transform.ts (new) Six pure helpers, signature-preserving port of deco-cx/apps/magento/utils/transform.ts: toProduct — MagentoProduct → schema.org Product (with isVariantOf wrapper, additionalProperty from custom_attributes, AggregateOffer) toOffer — price_info + stock_item → schema.org Offer[] with ListPrice / SalePrice + installment ladder calculateInstallments (internal) — À vista + 2x..Nx, capped by min(floor(price/minInstallment), maxInstallments) toImages — media_gallery_entries OR images → ImageObject[], with toURL prefix handling toURL — promote protocol-relative // URLs to https toBreadcrumbList — categories[] → ListItem[]; falls back to product-name-only when flag + empty categories toSeo — custom_attributes → {title, description, canonical} (meta_title > title, comma-joined array values) Excluded (verbatim from prod, will follow in dedicated PRs): • toProductGraphQL, toAggOfferGraphQL, toOfferGraphQL, toProductListingPageGraphQL, toImageGraphQL — land with the PLP/list loaders that consume them • toReviewAmasty, toLiveloPoints — Granado-only, belong in the consumer site's `magento-granado` extension, not in @decocms/apps magento/__tests__/transform.test.ts (new — 22 tests) Each helper exhaustively covered: toURL: 3 cases (// promote, https passthrough, http passthrough) toSeo: 4 cases (meta_title > title, fallback, missing both, array-value join) toOffer: 6 cases (no price_info, in-stock, out-of-stock, Sale/ ListPrice emission, installments capped by both bounds, floor-0 fallback to 1) toImages: 3 cases (media_gallery + imagesUrl, images[] fallback, // promote inside the URL prefix) toBreadcrumbList: 3 cases (product-name fallback, ordered mapping, filter null/empty/zero-position) toProduct: 3 cases (base mapping, isVariantOf wrap, fallback to product.price when price_info absent) Test totals across the magento port: 6 files / 71 tests / 156ms. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(algolia): initial scaffold port from deco-cx/apps to apps-start
Mirrors the existing magento/ and vtex/ shape: configureAlgolia + lazy
getAlgoliaClient + initAlgoliaFromBlocks(blocks) with Secret-shaped
adminApiKey resolution through process.env. loaders/client.ts shim
matches the deco-cx call site signature so legacy site code doing
invoke.algolia.loaders.client({}) keeps working when routed through
the loader registry.
algoliasearch is declared as an optional peerDependency so sites
without Algolia don't get the SDK transitively.
12 tests cover the config / client / init contract. The product
loaders (list, listingPage, suggestions) plus utils (highlight,
product) are tracked as follow-up PRs — granadobr-tanstack keeps a
Granado-specific transform layer over toProduct that isn't portable
as-is.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(algolia): pin algoliasearch to v4 to keep the Fresh API call shape
algoliasearch v5 reshapes search request bodies (top-level params,
`requests: [...]` wrapper, removed/renamed types like
MultipleQueriesQuery) which would break every existing Deco site
loader. Pinning the peer dep to ^4 || ^5 with v4 in devDependencies
lets downstream sites stay on v4 without rewriting their loaders;
those that opt into v5 just need to bump and adjust call sites.
Switched the production import to the v4 default-export shape and
updated the test mock accordingly. 545/545 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Search-only sites that never set their CMS Secret env var (e.g. ADMIN_KEY) still get a working SearchClient as long as searchApiKey is populated on the deco-algolia block — which is the common case since searchApiKey is a public, ship-to-browser value. Previously `getAlgoliaClient()` threw "adminApiKey is required" on the very first home request when the worker env var wasn't set, breaking every ProductShelf even though the indexing path was never exercised. Indexing actions that genuinely need admin scope should read `getAlgoliaConfig().adminApiKey` directly and surface their own "admin key missing" error. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rs (#68) The algoliasearch v4 default requester loads node:http via the NodeHttpRequester, which crashes on first request in Cloudflare Workers (no node:http module). Pass `@algolia/requester-fetch` explicitly so every Deco runtime — Workers, Bun, Node 18+ — uses the global fetch. Without this, granadobr-tanstack's home ProductShelves silently swallowed the loader exception and rendered as empty skeletons even though credentials, init wiring, and the SDK construction all looked correct. requester-fetch declared as an optional peer dep with the same ^4 range as algoliasearch. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v4 of the SDK + every `@algolia/*` sub-package imports either
`node:http` (`@algolia/requester-node-http`) or `crypto`
(`@algolia/client-search`) at module load time, which crashes on
Cloudflare Workers before any request is made. Passing
`createFetchRequester()` doesn't help because the import side-effect
fires first.
v5 uses the global `fetch` and Web Crypto APIs only — runs on
Workers, Bun, Deno, modern Node — so this also unblocks
granadobr-tanstack ProductShelves on the deployed worker.
BREAKING: sites consuming `@decocms/apps/algolia` must update their
loaders from the v4 `client.search([{ indexName, query, params:
{...} }])` shape to v5 `client.search({ requests: [{ indexName,
query, ...flatParams }] })`. Granadobr-tanstack will land the loader
update alongside the version bump.
Test mock updated to mirror v5's named export shape.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #69 migrated algoliasearch to v5 but the squash commit's leading "!" made semantic-release's angular preset skip the release. This empty commit re-triggers the version bump so 2.8.0 lands on npm. BREAKING CHANGE: client.search() signature changed from v4 array to v5 { requests: [...] }. See PR #69 for full migration notes.
Ports the Salesforce Marketing Cloud Personalization (Evergage) campaign personalization API as a new `@decocms/apps/salesforce` package, mirroring the algolia scaffold pattern (#66). Scope: read-path loaders for homepage shelves, PDP recommendations, and cart-aware cross-sell. No actions, no analytics. The legacy Deno loaders are stateless — config (`baseUrl` / `dataset` / `campaignId` / `cookieName`) comes in via loader props rather than a global `configureSalesforce`, since real sites typically run multiple Evergage datasets per worker. Layout: - `types.ts` — `SalesforceProduct` (open via index signature), `PersonalizationBody`, response shapes. - `utils/parseUserCookie.ts` — decode the URL-encoded JSON cookie Evergage drops on the browser; falls back to anonymous fallback. - `utils/httpClient.ts` — runtime-agnostic Proxy client supporting the legacy `client["POST /api2/event/:dataset"]` indexed-route syntax used by every Deno-era loader. - `utils/transform.ts` — `createProductTransformer({ propertyMapper? })` so site-side wrappers can project dataset-specific Evergage columns (`Marca`, `Volume`, `Linha`, …) without forking the schema.org map. - `loaders/products/{list,listRecomended,listCart}.ts` — the three campaign endpoints granadobr-tanstack consumes today. Cookie access goes through `getCookies()` from `@tanstack/react-start/server` (parameterless, ALS-backed) because the framework's `commerceLoader(resolvedProps)` path drops the `req` argument before reaching the loader. Mirrors the workaround already used in helsinki's site-side packs. Tests: 48 new tests (parseUserCookie, transform, httpClient) lock the contract against the deco-cx/apps Deno baseline — all mock fetch, no Evergage credentials needed. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#72) `initMagentoFromBlocks` and `initAlgoliaFromBlocks` each carried their own local `resolveSecret` helper that *only* read `process.env[name]`. For sites that ship their secrets through the production Deco CMS — where every Secret block looks like `{ encrypted: "<aes-cbc hex>", name: "ENV_NAME" }` — the env-var-only helper silently produced `apiKey: ""` and the downstream `magentoFetch` suppressed the `Authorization: Bearer` header. Result: every Magento request returned 401 Unauthorized, which surfaced in the field as a minicart that never loaded (cart loader threw on the auth failure). VTEX and Shopify already accept a `ResolveSecretFn` parameter and use the shared `resolveSecret` from `@decocms/start/sdk/crypto`, which walks: 1. plain string (dev override) 2. `{ get: () => string }` (legacy Secret object) 3. `{ encrypted: "<hex>" }` decrypted via (prod default) `DECO_CRYPTO_KEY` (AES-CBC) 4. `process.env[name]` (fallback) Magento + Algolia now follow the same chain. Both inits are async (decryption is async) — site setups need to `await` the call before any loader fires. `resend/client.ts` does not have an `initFromBlocks` helper yet — left a TODO referencing this commit so the same migration ships when Resend grows a CMS-block bootstrap (it's currently configured via `configureResend({ apiKey: process.env.X! })`). Tests: - magento/__tests__/client.test.ts: 5 new cases covering missing block, plain string, Secret + env var, no-decrypt no-env-var fallback, and dual apiKey + originHeader resolution. - algolia/__tests__/client.test.ts: all existing init tests converted to async + new env-var-fallback case for the encrypted branch without DECO_CRYPTO_KEY. Breaking change: `initMagentoFromBlocks` and `initAlgoliaFromBlocks` now return `Promise<void>` and `Promise<boolean>` respectively. Site setups must add `await`. Existing call sites (granadobr-tanstack) will be updated in the bump-and-refactor follow-up. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…entries (#74) Module-level `inflight: Map<string, Promise<CacheEntry>>` only evicted entries via `.finally()` on the stored Promise. If the underlying `fetch()` never settled (TCP connection dropped without FIN, CDN holds response open), the `.finally()` never ran and the Map entry leaked forever — every subsequent request for the same cache key joined the zombie Promise, pinning context references in memory until `exceededMemory`. Wrap `executeFetch` with a 10s timeout so `.finally()` always runs. The underlying hung fetch is abandoned (CF runtime will GC it after the request ends) but the cache slot is freed and the next request retries. Observed production impact on a TanStack Start storefront (24h window): - 514 hard exceededMemory crashes - 403 canceled requests - 371 HTTP 503s - One PLP route accounted for 82% of events (single poisoned cache key) - CPU near zero + wall_time in tens of minutes = isolates sleeping awaiting the zombie Promise, not computing Closes #73
* feat(blog): add blog app with loaders, types, and commerce loader factory Create reusable blog app for TanStack Start sites with: - Blog types (BlogPost, Author, Category, BlogPostPage, BlogPostListingPage) - CMS records access via loadBlocks() from @decocms/start/cms - Post filtering, sorting, and pagination utilities - 8 loaders (BlogPostPage, BlogPostItem, BlogpostListing, BlogRelatedPosts, GetCategories, Blogpost, Category, Author) - Commerce loader factory (createBlogCommerceLoaders) following the VTEX pattern - App module with manifest and registry entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(blog): add biome-ignore comments for lint warnings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(blog): rename commerceLoaders.ts to loaderMap.ts Blog loaders are not commerce-specific — the file name was misleading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(blog): add unit tests for core functions, records, loaders, and module Cover handlePosts pure functions, getRecordsByPath CMS access, all 5 loaders, and the blog app configure() entry point (60 tests). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Apply biome auto-format fixes to resolve the 3 formatter errors reported by `npm run lint`. No behavior changes — line wrapping only. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
Author
|
Fechando — vamos direto em |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bring the
nextbranch up tomain. Thenextchannel has been dormant:@decocms/apps@nextcurrently points to1.15.0-next.2while@latestis4.1.0—nextis 40 commits behind. Without this catch-up, any prerelease cut fromnextwould be missing the algolia v5 migration, salesforce/blog scaffolds, magento secret rework, several VTEX fixes, etc.After this merges, prereleases (
@decocms/apps@next) will be cut from a base of4.1.0, so opt-in customers (npm install @decocms/apps@next) can validate behaviour changes before promotion to@latest.Test plan
nextproduces a sensible version tag (likely4.2.0-next.0once the next feature PR lands).@decocms/apps@nextdist-tag updates to a 4.x prerelease.🤖 Generated with Claude Code
Summary by cubic
Syncs the
nextbranch withmain, bringing in new apps, VTEX fixes, and a Bun-based CI/release pipeline. This aligns prerelease work with the current codebase and unblocks testing of recent changes.New Features
@decocms/apps/algolia(v5 SDK, server/client config, client loader).@decocms/apps/magentoscaffold (client config, features/cart/user/wishlist loaders, newsletter and stock-alert actions, transform + GraphQL/utils).@decocms/apps/salesforcescaffold (campaign product loaders, transform, HTTP client, cookie parsing).@decocms/apps/blogwith post/category/author types and loaders; registered inregistry.ts.vtex/invoke.ts.Bug Fixes
Set-Cookiethrough cart-adjacent actions, normalize cookie domain to storefront host, and make header mergesHeaders-aware to preserveCookie.image[0]and realinventoryLevel; add options to control lean-variant fields.bun.lock,bun install,bun run), run releases frommainonly, and simplify.releasercpublish command.Written for commit e61499a. Summary will update on new commits.