Tags: decocms/blocks
Tags
feat(cache): build-hash-versioned loader cache + on-demand purge route ( #393) * feat(cache): build-hash-versioned loader cache + on-demand purge route The in-memory loader/commerce cache (createCachedLoader) is per-isolate and TTL-only, so upstream data changes (e.g. a Magento catalog update) can keep returning stale values with no reliable way to invalidate short of waiting out the TTL. This versions loader cache keys by the build hash (__DECO_BUILD_HASH__, mirroring the edge cache's __v) so a redeploy orphans prior-build entries, adds a POST /_cache/purge-loaders route (Bearer PURGE_TOKEN) to flush the current isolate on demand, and warns once when a loader configures a very long maxAge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cache): address review — purge race guard, shared auth, honest docs Follow-up to the review of #393: - Fix the purge race: a loader in flight when POST /_cache/purge-loaders (or a decofile hot-reload) fired could repopulate the just-cleared entry with pre-purge data. clearLoaderCache() now bumps a generation counter; each loader invocation snapshots it and skips its deferred write when the generation changed, so a mid-flight purge is no longer silently undone. - Extract a shared authorizePurge() used by both purge endpoints (no more duplicated auth block that could drift) and compare the token in constant time. - Drop the redundant bustLoaderCache alias; callers use clearLoaderCache directly. - Correct the BUILD-prefix comment: it's defense-in-depth, not a warm-isolate invalidation lever (per-isolate Map already starts empty on a fresh isolate); align its truthiness gate with getBuildHash()/getDeploymentId(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(tanstack): permitir iframe do storefront pelo admin/studio por pa… …drão (#390) * fix(tanstack): allow deco admin/studio to iframe storefronts by default Storefronts could not be embedded in the deco admin/studio live-preview iframe (e.g. studio.decocms.com) out of the box: createDecoWorkerEntry shipped X-Frame-Options: SAMEORIGIN and the `csp` option only emitted a report-only CSP, so every site had to hand-add an enforcement `frame-ancestors` directive via securityHeaders to be embeddable. Ship a default enforcement `Content-Security-Policy: frame-ancestors` listing the deco admin/studio origins (DECO_ADMIN_FRAME_ANCESTORS, kept in sync with the admin CORS allowlist) and drop X-Frame-Options from the defaults — frame-ancestors (CSP level 3) supersedes it and, unlike the all-or-nothing SAMEORIGIN, still blocks arbitrary third-party framing. Sites can override via securityHeaders["Content-Security-Policy"] or opt out with securityHeaders: false. Also refreshes the stale DEFAULT_ADMIN_ORIGINS list in blocks/sdk/csp.ts to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(tanstack): narrow default frame-ancestors to studio origins only Trim DECO_ADMIN_FRAME_ANCESTORS to 'self' + https://studio.decocms.com + https://*.deco.studio, and revert the unrelated DEFAULT_ADMIN_ORIGINS refresh in blocks/sdk/csp.ts to keep this PR focused on the worker default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fix(cms): drop never-gated hidden array items instead of null holes (#… …388) Hiding an array item in the admin wraps it in a multivariate flag gated by a `never` matcher. `never` never matches, so the wrapper resolved to `null` and stayed in the array as a hole the section renders as an empty card (empty benefit rows, blank banners, etc.). Resolve a no-match multivariate to `undefined` (the "not present" sentinel) and filter `undefined` out of arrays in `internalResolve`. Legitimate `null` values (a matched variant resolving to null, a failed loader) are kept — only the no-match sentinel is filtered. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fix(blocks-cli): materialize CSV redirects into top-level blocks at g… …enerate time (#387) `website/loaders/redirectsFromCsv.ts` blocks only carry a `from` CSV path and an empty `redirects: []`; the Fresh/Deno loader that read the CSV at runtime was never ported to TanStack. Worse, these blocks are typically nested inside `site.routes[]`, and the runtime `loadRedirects` only scans TOP-LEVEL decofile entries — so CSV-backed bulk redirects never fired on migrated sites. Read each referenced CSV from `public/` at generate time (Node, fs available) and emit synthetic top-level `__csv_redirects__<name>` blocks that `loadRedirects` already understands. Runtime stays synchronous and does no I/O. CSV blocks are merged with lower precedence than curated redirect blocks, so a CMS-authored redirect wins over a bulk CSV row for the same `from`. Also fix `parseRedirectsCsv` to skip a `from,to` header row instead of turning it into a bogus `/from -> to` redirect. Verified against the granadobr-tanstack decofile: 18,455 CSV rows materialized, matchRedirect returns 301s for the migrated URLs, curated blocks unaffected. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(blocks): self-contained meta.gen.json for every framework (not ju… …st Eitri) (#386) PR #362 added a `--compose` flag so generate-schema can bake composeMeta's framework block types (Page, matchers, __SECTION_REF__, Resolvable) into meta.gen.json at generation time — but the orchestrator only passed it when `--platform eitri`. So on the default (cloudflare/tanstack) path the committed meta.gen.json stayed raw: no matchers, no pages, no `framework` field. That's why it diverges from /live/_meta, which composes at runtime. Make the file self-contained for every platform: - generate.ts: always forward `--compose`; `--framework eitri` stays gated to the eitri platform (everyone else gets composeMeta's default tanstack-start). - composeMeta: add an idempotency guard. It is NOT structurally idempotent (framework section refs get appended to root.sections.anyOf), and the tanstack runtime re-runs composeMeta on the file it loads from disk. A composed meta always carries a top-level `framework` field (raw generateMeta() never sets it) — use it as the sentinel and return the meta unchanged. Raw metas (older CLI output) still compose, so it's backward compatible. - Tests for both idempotency invariants (same object back, no anyOf duplication). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fix(tanstack): stop bundling site loaders/actions into the client (se… …cret-leak fix) (#379) * fix(tanstack): stop bundling site loaders/actions into the client (secret-leak fix) `.deco/loaders.gen.ts` registers every site loader/action behind a dynamic import() and is reachable from the CLIENT entry (router -> setup -> commerce-loaders -> loaders.gen). Without a client stub, Vite emits each loader/action module as a public client chunk, so any credential hardcoded in a site action's source is baked into the browser assets and becomes publicly downloadable — even though the exact same code stayed server-only under the old Fresh/Deno stack (invoke was an HTTP-by-name proxy; the action manifest never entered a client bundle). Fix: stub loaders.gen.ts to `export const siteLoaders = {}` on the client in the vite plugin, mirroring the existing blocks.gen/meta.gen client stubs. SSR keeps the real module. This is safe: invoke runs server-side only (/deco/invoke -> handleInvoke reads the registry via getRegisteredLoaders), and client components reach loaders/actions exclusively through the HTTP invoke proxy (which imports no modules), so the client never needs siteLoaders. Ships in @decocms/tanstack, so every site is fixed on framework upgrade. Also adds guardrails so this can't regress silently: - plugin.test.ts: permanent regression test for the load() hook (client stub vs SSR real module). - audit-secrets: a blocks-cli source scanner that flags credentials hardcoded in actions/loaders and server-only @decocms/blocks/sdk/crypto imported into "use client" files. Wired into `bun run check` (--mode block). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(blocks-cli): add loader/action bundle-leak rollout script Ops tool to propagate the loaders.gen client-stub fix to sites already in prod. Run against one or more site directories: tsx node_modules/@decocms/blocks-cli/scripts/upgrade-loader-leak-fix.ts \ ./site-a ./site-b --version 7.21.0 --build --verify-secret "$LEAKED_TOKEN" Per site it: 1. bumps every @decocms/* dependency to the fixed version (default: latest published @decocms/tanstack, resolved via `npm view`), 2. installs to update the lockfile, 3. audits the source (reusing audit-secrets) for credentials hardcoded in actions/loaders and reports them — these must be ROTATED by hand, a leaked secret stays compromised even after it stops shipping, 4. optionally rebuilds and greps the client output for a caller-supplied known-leaked value to confirm it's gone. Never deploys — prints the redeploy hint. Safe by default: --dry-run shows the plan, non-@decocms/tanstack sites are skipped, each site is isolated so one failure doesn't abort the batch. The --verify-secret value is supplied at runtime and never stored in the repo. Pure helpers (isAffectedSite / planDepBumps / applyDepBumps / findSecretInAssets) are unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(blocks-cli): scaffold server-only commerce/invoke registrati… …on boundary (#380) Follow-up to the loaders.gen client stub. Moves the COMMERCE_LOADERS + invoke registration out of the client-imported setup.ts into a new server-only module (setup/commerce-init.ts) that is imported ONLY by worker-entry.ts. Why: router.tsx imports "./setup" into the client bundle. setup.ts imported COMMERCE_LOADERS from setup/commerce-loaders.ts, which statically imports the site's loader/action modules (and platform commerce loaders). That pulled the whole loader/action graph into the browser bundle. Both consumers of the registration run server-side (CMS commerce-loader resolution via registerCommerceLoaders; the /deco/invoke handler via setInvokeLoaders), and client components reach loaders/actions only through the HTTP invoke proxy, so the client never needs it. Changes (scaffold templates + wiring): - new templates/commerce-init.ts — server-only registerCommerceLoaders + setInvokeLoaders(() => COMMERCE_LOADERS). - setup.ts — drops the COMMERCE_LOADERS import + both register calls; stays client-safe (section registration only). - server-entry.ts — worker-entry.ts imports "./setup/commerce-init" (both the vtex and generic variants). - phase-scaffold.ts writes src/setup/commerce-init.ts; phase-verify.ts expects it; no-legacy-packages.test.ts covers it. - new templates/commerce-init.test.ts asserts the boundary (registration in commerce-init, absent from setup.ts, imported by worker-entry not router). Known remaining edge (follow-up): setup.ts still imports initVtexFromBlocks / setVtexFetch from @decocms/apps-vtex for platform init, so @decocms/apps-vtex is not yet fully out of the client graph. Those apps carry no hardcoded secrets (VTEX credentials live in server-only app state), so this is bundle-size hardening, not a secret leak. Needs validation against a real VTEX site before merge. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(blocks-cli): keep meta.gen.json committed in scaffolded .gitignore ( #382) #378 gitignored both generated .deco snapshots. Only the decofile (blocks.gen.json) actually needs it — it's a single 10MB+ line that makes every content PR conflict irresolvably. meta.gen.json (the schema) is multi-line and merges cleanly, and keeping it committed means a scaffolded site has a working admin/Studio on a fresh clone without a schema regen. - scaffold: .gitignore now ignores only .deco/blocks.gen.json. - verify: drop meta.gen.json from the required ignore entries. - tanstack plugin: the meta cold-start bootstrap stays as a safety net for the missing-file case; comment updated to reflect meta is normally committed (no behavior change). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fix(tanstack): generate blocks.gen.json synchronously on dev cold-sta… …rt when missing (#381) Now that blocks.gen.json is gitignored (#378), a fresh clone has no snapshot on disk. The cold-start bootstrap regenerated it fire-and-forget, which races the first request: setup.ts reads the .json via the blocks.gen.ts load() hook, and a miss falls back to the empty stub — rendering a blank page until regen lands and reloads. Generate the snapshot synchronously when it is ABSENT (fresh clone only), blocking startup until the first request can see real content. When the file is already present, keep the cheap async refresh unchanged. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fix(security): stop auto-exposing generic MasterData CRUD as server f… …unctions (#332) Generic VTEX MasterData CRUD (createDocument, getDocument, patchDocument, searchDocuments, uploadAttachment) was listed in @decocms/apps-vtex's `invoke.vtex.actions`. The blocks-cli generator emits one top-level `createServerFn` per entry, which TanStack Start compiles into a public, unauthenticated `/_serverFn/<hash>` endpoint. These handlers take a client-supplied `entity` and proxy to the VTEX MasterData API with the app's admin appKey/appToken and no auth or entity allow-list — so any caller can `searchDocuments({ entity: "CL", ... })` to enumerate customer PII, or create/patch arbitrary entities. Broken access control. Authentication alone can't make a generic (entity, filter) proxy safe — it has no room for per-entity/per-record authorization — so remove it from the auto-exposed surface rather than guard it: - apps-vtex/invoke.ts: drop the 5 MasterData entries from invoke.vtex.actions (and their imports). The functions stay exported from actions/masterData for server-side use; sites needing MasterData define a narrow, entity-pinned, authenticated action in their own src/server/invoke.ts. - generate-invoke.ts: add a PRIVILEGED_ACTIONS denylist that skips + warns on these names even if re-added to the contract, so the exclusion can't silently regress. Also covers the apps-start legacy source path. - generate-invoke.test.ts: lock the guard — privileged actions are never emitted as consts / vtexActions entries / imports, safe actions still are. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PreviousNext