Skip to content

Tags: decocms/blocks

Tags

blocks-v7.21.0

Toggle blocks-v7.21.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.10

Toggle blocks-v7.20.10's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.9

Toggle blocks-v7.20.9's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.8

Toggle blocks-v7.20.8's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.7

Toggle blocks-v7.20.7's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.6

Toggle blocks-v7.20.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.5

Toggle blocks-v7.20.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.4

Toggle blocks-v7.20.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.3

Toggle blocks-v7.20.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

blocks-v7.20.2

Toggle blocks-v7.20.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>