Repository navigation
v8 N-16: drafts are the fast-preview pointer, with only the branch's changes over the local release - #619
Draft
tlgimenes wants to merge 27 commits into
Draft
v8 N-16: drafts are the fast-preview pointer, with only the branch's changes over the local release#619tlgimenes wants to merge 27 commits into
tlgimenes wants to merge 27 commits into
Conversation
… production forDraft on a hosted CMS no longer downloads a complete draft snapshot. remoteLoader.load(pointer) now captures the production snapshot it already has (newest release, else the fallback) once, fetches the immutable overlay manifest the pointer's version names and only the changed-block assets missing from its cache, and layers replacements and tombstones over the captured map (list = union minus deletions). It never fetches a production revision to align. The Loader interface is unchanged: the composed view is an ordinary Snapshot whose revision is "<base revision>~<overlay version>". - Contract: GET /sites/<site>/drafts/<version>.json and /sites/<site>/draft-blocks/<hash>.json on the delivery origin, with the site token and the pointer's query (the grant) forwarded verbatim. The pointer must be <delivery host>/sites/<site>/drafts?<grant>@<version>. - Integrity: the manifest must hash (SHA-256 of canonical JSON) to its version and each block to its hash; strict manifest shape, disjoint set/delete, format 1 only. - Caches per site: manifests by version + grant (a version alone unlocks nothing), 16 MiB of verified blocks by hash, 3 composed views keyed by captured base and version; ContentStore keeps 3 drafts and drops them when a hot reload swaps the fallback under the same loader. - One 64 MB budget per draft (manifest + blocks), charged while downloading. - Any failure is LOADER_FAILED; failures aren't cached. Forced variants keep applying on top. - Protocol: computeBlockHash, computeOverlayVersion, DRAFT_OVERLAY_FORMAT, DraftOverlay, and draftOverlayFixtures golden hashes for the preparer. - Conformance DO-1..DO-14 for every documented overlay claim; existing draft claims moved to overlays. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…se checks Review fixes on top of the overlay loader (docs: content-delivery#exact-draft-previews, hosted-releases-internals). - Grant expiry on warm servers: an authorized overlay manifest is cached by version + grant only for its response's `max-age` (none, 0 or no-store is not cached), so an expired grant is asked about again and fails. The CMS reuses a loaded draft for at most a minute before `load(pointer)` runs again. - `forDraft` schedules the same background release check `forRelease` does: a server that only serves previews still picks up new releases. It never runs in front of the draft and is not an alignment fetch. - A shared changed-block download is bounded on its own and then charged to every waiting draft, so one draft's budget never fails another's. - Lean: one `BoundedMap` (LRU, optional size) for content.ts and remoteLoader.ts; the composed-view cache is gone (ContentStore already keeps drafts). - Conformance: DO-2 now asserts the release check never blocks a draft; DO-15..18 cover manifest max-age, no-store, the CMS draft TTL and release pickup on preview-only servers. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
4 tasks done
…nd free-form maps - A string/number literal select lists its values in the order the annotation (or the alias it names) writes them, not the checker's type-id order, which another file could change. - @Format datetime is the date-time picker, as v7 wrote it (and deco check now validates it as one). - Record<string, any> fields no longer offer the saved-block picker and every block as a choice: every block's output fits a map of anything. Found migrating an Eitri app's editor forms to v8. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Adds reference/native-apps.md for apps whose v7 binding only generated Studio files (bundled JSON, native rendering): dependencies, prestart scripts, committed schema.gen.json, the non-discovered block map, deco serve with --allow-origin and no preview, a static bundle/content/forms parity harness, and pinning a blocks release with the form fixes. parity.md gains the pending-approval state and sorts editor-form differences into stale v7 files, dropped v7 heuristics, and CLI bugs. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
- Next.js: add @decocms/blocks to transpilePackages while the 8.1.0-next prereleases publish TypeScript source (v7's withDeco used to). - Verify once without a local link: a clean frozen-lockfile install and build, plus a schema.gen.json diff against the npm CLI. - Gotchas: vendored loaders the v7 site never ran, explicit error reporting in place of onResolveError, drafts on static Next pages, listing pages once per revision (probes included), head meta order, Jest with an ESM package, bundled .deco/blocks imports under hosted releases, v7 leftovers (env, /deco /live /.decofile exclusions), editor settings that leave with the site block. - Parity: build against what ships, ISR build age, flakes under load, naming what the cases don't cover; editor forms: dynamic option pickers become text fields, page form order and labels, the RichText widget substring rule, literal order depending on the v7 generator. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…rule A server that renders a draft is a production server reading a draft pointer: its release checks are the same as any other's. Comments and DO-18 say so; HD-18 is the one-host recipe (pointer and cookie gated by the app's own rule). The check-schedule tests (HP-5..8, cms interval tests) await exactly the background work they scheduled instead of a fixed sleep, so they no longer read a count short under load. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
/** @Format color */ type TextTone = "black" | "white" gives each TextTone field (optional, nullable, listed) format: "color", the select's values kept, as v7 wrote it for its color picker. A field's own @Format wins. Unit test plus conformance sch-26. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
v8 has no async rendering. The script replaces website/sections/
Rendering/Lazy.tsx and SingleDeferred.tsx ({ section }) with their
section and Deferred.tsx ({ sections }) with its sections, spliced into
the list that held it, in pages and saved blocks; wrapper options go.
A wrapper with several sections where one block goes is reported.
Idempotent. Parity notes: color-picker loss is not approved, annotate
the alias @Format color.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes
force-pushed
the
v8-16-draft-overlays
branch
from
October 5, 2026 13:43
56828b5 to
3a98553
Compare
A v7 website/loaders/secret.ts block marks its encrypted string "format": "secret", so the guard refused every apply that carried one (an app config, for instance) once a v7 site was edited through the content protocol. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Remove the bearer token from `deco serve` entirely: no random token, no --token flag (now an unknown flag), no DECO_SERVE_TOKEN, no Authorization check on /rpc or /assets, and no token in the site editor link, which is now https://studio.decocms.com/site-editor#endpoint=<encoded endpoint>. What protects it is unchanged: it listens on 127.0.0.1 by default, browser requests need an allowed Origin, the Host header must be its own address (DNS rebinding), requests without Origin are accepted, and CORS and Chrome local-network preflights are answered. --host beyond loopback prints a one-line warning that other machines on the network can reach it. Tests cover: no token anywhere (DECO_SERVE_TOKEN ignored, --token unknown, a stray Authorization header ignored), foreign Origin refused (including on preflight), rebinding Hosts refused (evil.com, 127.0.0.1.evil.com, another port, missing Host), no-Origin requests accepted, --host accepting its own Host and warning. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
… analytics The well-known saved block CMS, of the built-in type cms-settings, replaces the telemetry and analytics built-ins (no aliases) with three sections: preview, telemetry and analytics. Code keeps destinations, secrets and caps: telemetry.limits caps the rates, and the new createCMS preview.hosts caps the hosts content may allow previews on. - cms.settings() resolves CMS from the release already in memory (the content module at boot, the hosted release once a background check swaps it in), never a draft and never a fetch; defaults filled in, caps applied, a failing section falls back to its defaults. It never rejects. - cms.draftPointer and cms.draftCookie replace the free draftPointer, draftCookie and DRAFT_COOKIE: on a host outside the effective list they ignore the draft, so the request gets the release, never an error. - Host patterns: exact names, *.name wildcards (label by label, two labels at least), "*", optional ports, IPs exact, punycode; createCMS throws on an invalid pattern, content drops it, and content entries must fall within code's list. - Telemetry follows the release's telemetry section, read when the release changes and then at most once a minute (date rules only). - deco schema emits cms-settings in the content group; deco check points a leftover telemetry/analytics block at the CMS block and warns on a CMS of another type. - The v7-to-v8 migration folds the Site block's previewHosts, a literal OneDollarStats collectorAddress and prerelease Telemetry.json / Analytics.json into CMS.json, idempotently, and reports env-only settings. - Examples: async client(request); schemas regenerated. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…, log refused origins --host 0.0.0.0/:: prints a 127.0.0.1 site-editor link (Studio accepts only loopback); a specific non-loopback --host warns the site editor connects only through 127.0.0.1/localhost. --host is lowercased to match the Host check. Each refused browser origin is logged once with the --allow-origin to pass, since the browser shows the 403 as a network error. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ter preview hosts - createCMS returns a handle per block map on the shared instance: content, caps, telemetry and update checks are shared, and each handle resolves with its own map. A second bundle in the process (Next's proxy.ts importing the app's cms) no longer replaces the app's map; adopt() only swaps content. - ContentStore.update() keeps the last served release in memory, so a custom loader's settings (and preview hosts) don't fall back to the defaults (every host) between an update and the next load. - cms.settings() is deep-frozen: one object per release reaches every caller. - Host matching: a URL with a username or password is unreadable (only "*" matches), and a hostname with an empty label matches nothing but "*". - v7-to-v8 migration: previewHosts are trimmed, lowercased and checked as host patterns (invalid ones reported); on TanStack Start the <site>.deco.site and <site>.deco-cx.workers.dev hosts v7 always allowed for DECO_SITE_NAME are added (or reported when the name isn't found); the behaviour-change note says what v7 actually allowed. @decocms/blocks/cli exports the host parser. - The observability fixture uses the CMS block instead of the removed type. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
An instance an older copy of @decocms/blocks stored on globalThis (a dev reload across versions) has no handles, so adopting it threw "existing.handle is not a function". createCMS now replaces it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
This was referenced Oct 5, 2026
deco serve has no token, and now no origin allowlist either: CORS is answered for any Origin (reflected, with Vary: Origin), and Chrome's Private/Local Network Access preflights are still answered. Any website open in the browser can read and write the content through it, so it is meant to run only while editing. Removed: --allow-origin (and the parser's "list" flag kind it alone used), the STUDIO_ORIGINS allowlist, the refused-origin 403 and its log line, and the Host-header check (cp-43), which is pointless once every origin is allowed. --host still prints its network warning. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
….1 and ::1 Without --host, deco serve listens on 127.0.0.1 and ::1 on one port (::1 skipped where IPv6 is unavailable), so http://localhost:<port> reaches it whichever address the OS gives localhost. Every startup line and the site editor link now say localhost. --host keeps listening on that one address. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
A site whose schema isn't generated yet is a normal state: blocks.list and
blocks.apply already work without one, so the site editor can still list
and edit every block. schema.get now says so with a typed result instead of
NotFound:
{ notModified: false, version: null, resolvedRef, schema: null }
There's no version to poll with, so a client reads it unconditionally until
a schema appears. A site with no .deco folder is still NotFound (the snapshot
check), and a sent ifSchemaMatch conflicts with actual: null. describe
doesn't report it: that would read the whole schema on every describe.
Conformance: new schema/absent case (listed in cp-50), schema/read skips on a
schemaless endpoint, cp-31 now claims schema: null plus a list and a save.
deco serve's startup line says "no schema yet: run deco schema for typed
forms".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
- /deco/invoke: one server function per key imported by the call site, never a rebuilt invoke tree or string-keyed dispatcher; v7's invoke passed no app context; keys v7 had no handler for stay explicit failures; check the client bundle for server code. The script's report line says the same. - Caching and privacy: gate the copied worker entry's draft bypass on cms.draftPointer (v7 gated it on preview hosts); never let router hydration wait on a cross-origin call. - Rendering: pages awaited because headers depend on the result; side effects of deferred sections now start on load, and gating them on v8's layout doesn't reproduce v7's (short v7 layouts mounted them on load); Tailwind generates classes from words in comments and docs. - Framework code: module-level config singletons are cross-request state; identification headers are the site's; keep v7's platform client or adopt the v8 one, and drop the unused dependency. - Hosted releases replace v7 fast deploy (secrets, memory estimate); telemetry bindings with no v8 writer are a listed decision; service.version from the build commit. - Verify: typecheck against the v7 count, type fixes can change the editor form. Parity: pin request.cf across checkouts, re-record bad baselines, track flake counts, prove code fixes on every page type, name what a replaying harness can't see. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
From migrating a non-ejected VTEX FastStore storefront (Next.js 16 Pages Router, private): - reference/faststore.md: keep v7's seam (a vendored runtime built from the published package, the Content Platform client patch returning FastStore's section shape); drafts on Pages Router SSG need a hook in Next's compiled pages runtimes (a require.cache patch never runs), gated on a parseable pointer and memoised per request; x-nextjs-cache: HIT is no evidence; password-gated preview domains must read the v8 cookie; preview hosts and draft sources (no loopback in production); imported settings vs the SEO form; schema-derived defaults mapped through the block map; case-only name pairs; global sections that matched per URL. - SKILL.md: probe drafts by hand on dev and a production build (a parity harness can't see a dead draft path when v7's was dead too); list the dev host in preview.hosts (v8 ignores DECO_ALLOWED_PREVIEW_HOSTS). - gotchas.md: deco check in the build gates the deploy (importers must emit check-clean content); don't route on undeclared __ fields; case-only name pairs; Pages Router drafts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…e branch's changes
Product-owner decision: no /preview protocol. A draft pointer names Studio's
API (<host>/api/<org>/decofile/<project>/<branch>/changes?token=…@<version>),
which answers { format: 1, set, delete }: only what the draft branch changed
against production. cms.forDraft fetches it once and layers it over the
production snapshot the server already has.
- draftChanges.ts: fetch only from createCMS({ preview: { sources } })
(default studio.decocms.com; refused before any fetch otherwise), https
except loopback, v=<version>, no cookies or credentials, no redirects,
200 only, 16 MiB, 10 s, strict shape; set/delete layering.
- A `local` pointer (deco serve) names no draft: nothing fetched, forced
variants still apply.
- Drafts need no site/token. Loader.load() takes no pointer; remoteLoader
serves releases only.
- Removed: overlay manifests and block blobs, grants, blob caches, the
delivery-host-only pointer rule, computeBlockHash/computeOverlayVersion/
DRAFT_OVERLAY_FORMAT/DraftOverlay and draftOverlayFixtures.
- parseDraftPointer accepts a bracketed IPv6 host.
- Conformance DP-1..DP-20 replace DO-1..DO-18.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
… version A fetch polyfill that ignores redirect: "manual" (React Native, whatwg-fetch) could follow a redirect off the allowed host; the SDK now refuses any response that was redirected or came from a host outside preview.sources. The version is URL-encoded so a looser version format can't add query parameters. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
deco schema writes a top-level "blocksMajor": 8 (BLOCKS_MAJOR, exported from @decocms/blocks/protocol: the package major, not the version string). It is the only signal Studio reads to tell a v8 site from a v7 one; the schema file name does not tell. deco check fails on a schema without it or with another value and says to run deco schema. schema.get serves it as-is. Claims sch-27, chk-16, cp-51; example schemas regenerated; v7-to-v8 migration skill notes it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ts and OTEL names
- Content protocol: the handler no longer authenticates (no token,
authorize or scope; the asset handler neither). Who may call it is the
mounting server's job: deco serve on loopback, Studio behind its session.
The client's `token` option goes too.
- Removed: requestKey and idempotency receipts (describe.writes),
ifSchemaMatch, the ref parameter and refs/autoCreate, and describe.limits
with the read/batch limits (list, schema, batch response). blocks.apply
keeps its own guards (500 names, 1 MiB per entry, 8 MiB per request) as
internal constants. ifMatch and assets.maxBytes stay. Storages lose
refs/idempotency/limits/getReceipt, and snapshot()/readSchema() take no
options. Conformance suite updated to match.
- Draft cookie is `__deco_draft` again (v7's name), same attributes.
- Draft pointer hosts follow v7: `.decocms.com` suffix,
local.studio.decocms.com, localhost/127.0.0.1/[::1]/.localhost, with
DECO_PREVIEW_API_DOMAINS replacing the list. preview.sources is removed.
- Telemetry reads v7's DECO_OTEL_{METRICS,LOGS,TRACES}_ENDPOINT,
DECO_OTEL_HEADERS and DECO_OTEL_AUTH_TOKEN as aliases of the standard
OTEL_EXPORTER_OTLP_* names (standard wins), including per-signal URLs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The default preview API domains now match v7's draftSource exactly (local.studio.decocms.com, localhost, 127.0.0.1, .localhost, .decocms.com). DECO_PREVIEW_API_DOMAINS still replaces the list, and a listed [::1] is still treated as loopback (plain HTTP, any port). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
What
Draft previews go back to the v7 "fast preview" protocol: Studio mints a pointer that the SDK fetches. The only change from v7 is the answer: the pointer returns only what the draft branch changed compared with production, not a whole decofile.
cms.forDraft(pointer)layers those changes over the production snapshot the server already has in memory.There's no
/previewroute, no "preparing" state, no overlay or blob uploads to object storage, no/api/_deliverydraft routes and no overlay grants. Drafts don't needsite/token.Spec: deco-sites/docs-tanstack#5 (into #4's branch): content-delivery#draft-previews, hosted-drafts (#who-may-preview), api-reference (#draft-pointers, #loaders), content (#write-a-loader), releases-and-drafts.
Draft pointer contract (what the SDK enforces)
?__draft=and the__deco_draftcookie, v7's name;?__draft=offends a preview):<studio host>/api/<org>/decofile/<project>/<branch>/changes?token=<draft token>@<version>. The version is the commit of the editor's last save; Studio doesn't read it, it only gives each save a new address.773229cc): the preview API domains v7 shipped: the.decocms.comsuffix (studio.decocms.com,pr-*.pr.studio.decocms.com,local.studio.decocms.com) andlocalhost,127.0.0.1,[::1],*.localhost.DECO_PREVIEW_API_DOMAINS(a comma list; a leading dot is a suffix match on a label boundary, anything else an exact host) replaces the list, exactly as in v7. Never content. A port is allowed only on loopback hosts andlocal.studio.decocms.com. Anything else is refused before any fetch: look-alikes (evil-decocms.com,studio.decocms.com.evil.example), metadata IPs, userinfo, schemes,//paths.GET https://<host><path>&v=<version>(?v=when the path has no query). The scheme comes from the domain that admitted the host: plainhttponly forlocalhost,*.localhost,127.0.0.1and[::1]. The pointer's__variantparameters are removed first. The request has no cookies, noauthorizationand no site token, andredirect: "manual"means a redirect is a failure. A response that was redirected anyway (a fetch polyfill such as React Native's orwhatwg-fetchignoringredirect: "manual"), or whose URL has another origin, is refused. The version is URL-encoded intov.content-length), within 10 s. The body must be exactly{ "format": 1, "set": { "<block>": <block JSON> }, "delete": ["<block>"] }: no other keys, string names, no repeats, andsetanddeletedisjoint.setreplaces production's whole (draft wins per file). A block indeleteis absent from lookup andlist. Every other block is production's own object, and production is never mutated.listis production's names plusset's, minusdelete. Forced variants apply on top. The draft's revision is the opaque<release revision>~<version>, andforRevisioncan't reach it.LOADER_FAILED, with the cause. It never falls back to published content.local: a pointer whose version islocal(deco serve) names no draft. Nothing is fetched, and only its forced variants apply.Breaking changes (v8 prerelease)
Loader.load()takes no pointer. Drafts are the CMS's job and work over any loader, so custom-loader drafts are gone.remoteLoaderserves releases only.@decocms/blocks/protocol:computeBlockHash,computeOverlayVersion,DRAFT_OVERLAY_FORMAT,DraftOverlay. Removed from/protocol/conformance:draftOverlayFixtures.parseDraftPointernow also accepts a bracketed IPv6 host ([::1]:4000). (preview.sources, added earlier in this PR, is removed again in773229ccin favour of v7'sDECO_PREVIEW_API_DOMAINS.)CMS settings block
One optional, well-known saved block,
CMS(.deco/blocks/CMS.json), of the built-in typecms-settings, replaces thetelemetryandanalyticsbuilt-ins and theTelemetry/Analyticssaved blocks outright (v8 is prerelease-only, so there are no aliases):{ "__resolveType": "cms-settings", "preview": { "hosts": ["staging.example.com"] }, "telemetry": { "enabled": true, "metrics": true, "errorSampleRate": 0.05, "traceSampleRate": 0 }, "analytics": { "enabled": true, "collector": "…" } }telemetry: { endpoint, headers, limits }and analytics' code options, as before, pluspreview: { hosts }, the most content may allow (astelemetry.limitscaps the rates).cms.settings()is typed, with defaults filled in and caps applied. It reads the current production release already in memory: the bundled content at boot and offline, and the hosted release once the background check swaps it in. It never fetches, never reads a draft and never rejects. Telemetry, analytics (AnalyticsScriptin the root layout reads(await cms.settings()).analytics) and the draft helpers all use it. The result is frozen.deco schemaemitscms-settings.deco checkpoints a leftovertelemetry/analyticsblock atCMSand warns on aCMSof another type.deco-v7-to-v8-migrationskill,siteSettings.ts): folds a v7 site's Site-blockpreviewHostsintopreview.hosts, trimmed and lowercased as v7 compared them. An invalid entry is reported. On TanStack Start, the<site>.deco.site/<site>.deco-cx.workers.devhosts v7 added forDECO_SITE_NAMEare added too, or reported when the name isn't found. It also folds a literalOneDollarStatscollectorAddressand an already-migrated site'sTelemetry.json/Analytics.jsoninto their sections, variants included. It's idempotent and never replaces aCMSof another type. Settings that lived only in the environment are listed in the report.CMS.jsonwithblocks.applyguarded byifMatch: { CMS: null }(feat: support Blocks v8 (content protocol) behind a flag studio#7728).Preview hosts
*.namewildcards (whole leading label, matched label by label from the right, at least two labels after the*),"*", optional ports, and exact IPs. Matching is lowercase, ignores one trailing dot and compares punycode.x.example.com.attacker.comnever match.a..example.com), matches only"*".createCMSthrows on an invalid code pattern; content drops one. A content entry counts only when it falls within code's list.cms.draftPointer(request)/cms.draftCookie(request)(they replace the freedraftPointer,draftCookieandDRAFT_COOKIE) ignore a draft on a host outside the effective list, so the request gets published content, never an error.?__draft=offworks on any host. This isn't access control (the signed, expiring token in the pointer is); it keeps drafts off public domains, caches and search.e7a1d1d1,963fd2b4):createCMScall is a handle with its own block map on the shared instance, which still shares content, caps, telemetry and update checks. Before, Next'sproxy.tsimporting the app'scmsreplaced the app's block map, and PLPs answered 500 on faststore-fila. The documented recipe works again, and faststore-fila'sproxyCms.tsworkaround is deleted. The same block map returns the same handle, and an instance stored by an older package copy is replaced rather than adopted.update()keeps the last release in memory. Settings no longer fall back to the defaults (every host) until the next load.Restored legacy secret guard exemption (
63b07191)A v7
website/loaders/secret.tsblock marks its encrypted string"format": "secret", so the v8 secret guard refused everyblocks.applythat carried one once a v7 site was edited through the content protocol. The exemption that upstreamccdf6c25had added was lost; this restores it. v8Secretfields stay strictly validated.deco servehas no token (9a45c8c7,85bf7112)There's no bearer token,
--tokenorDECO_SERVE_TOKEN. What protects the server is unchanged: it listens on loopback by default, browser requests need an allowedOrigin, andHostmust be its own address.--host 0.0.0.0prints a127.0.0.1site-editor link.--hostis case-insensitive, and a refused origin is logged once with the--allow-originto pass.Update (
46c0ea4b, product-owner decision):deco servenow answers every origin (CORS reflectsOrigin, withVary: Origin; Chrome's private-network preflight is still answered). Removed--allow-origin, the Studio origin allowlist, the refused-origin 403 and its log line, and theHostcheck. With no token either, any website open in the browser can read and write the content, so the docs say to run it only while editing.Update (
c606a458, product-owner decision):deco serveshows and linkslocalhost: every startup line and the site-editor link (#endpoint=http%3A%2F%2Flocalhost%3A<port>%2Frpc, also for--host 0.0.0.0/::), and without--hostit listens on127.0.0.1and::1on one port (::1skipped where IPv6 is unavailable), sohttp://localhost:<port>reaches it either way.--hoststill listens on that one address.Product-owner fixes (
773229cc)createContentHandler/createAssetHandlerlosetoken,authorizeand the receiptscope(server/auth.tsdeleted); the client losestoken. Who may call the handler is the mounting server's job:deco serveon loopback, Studio's/rpcbehind Studio's session.ErrorCode.Unauthorized/Forbiddenstay as vocabulary for such a host.requestKey+ idempotency receipts (describe.writes),ifSchemaMatch(+Conflict.data.schema), therefparameter andrefs/autoCreate, anddescribe.limitswith the read/batch limits (list, schema, batch response) and theirlimitsoptions.blocks.applykeeps its own guards as internal constants (500 names, 1 MiB per entry, 8 MiB per request);ifMatchandassets.maxBytesstay; a removed parameter is now an unknown one (InvalidParams).ContentStorage:snapshot()andreadSchema()take no options;StorageDescriptionlosesrefs/idempotency/limits;getReceipt,StoredReceipt,CommitAttempt.ref/receiptandapplyRequestDigest/APPLY_DIGEST_DOMAINare gone.resolvedRefandexpectedSchemaVersion(the secret guard's) stay. Conformance suite updated (no limit/idempotency/ref/token cases; apply guards checked against the constants;apply/unknown-paramscovers the removed names).__deco_draftagain (v7's name), stillHttpOnly; Secure; SameSite=None; Partitioned, and?__draft=off.preview.sourcesis removed.DECO_OTEL_{METRICS,LOGS,TRACES}_ENDPOINT→OTEL_EXPORTER_OTLP_{METRICS,LOGS,TRACES}_ENDPOINT(full per-signal URLs, now honoured too),DECO_OTEL_HEADERS→OTEL_EXPORTER_OTLP_HEADERS, andDECO_OTEL_AUTH_TOKENas theauthorizationheader unless the headers set one. The standard name wins when both are set; a signal with no URL is dropped.repo-content-storage.ts) must droprefs/idempotency, itsrefchecks and the{ ref }arguments tosnapshot/readSchema; nothing in Studio used the other removed options.Commits
773229ccproduct-owner fixes (above): no handler auth, unused protocol options deleted,__deco_draft, v7 draft hosts +DECO_PREVIEW_API_DOMAINS,DECO_OTEL_*aliases.e07ae05dreview fixes: refuse a redirected draft response (or one from outsidepreview.sources) even when a fetch polyfill followed the redirect; URL-encode the version. Conformance DP-21.5f9dd074fast-preview drafts: the pointer returns only the branch's changes;draftChanges.ts(fetch, checks, layering),preview.sources,Loader.load()without a pointer. Removes the overlay/blob/grant machinery and its protocol exports, fixtures and DO-1..18 conformance; adds DP-1..20.5020aea2overlays (superseded by5f9dd074): loader, protocol helpers, conformance DO-1..DO-14.b69e7634no preview-server concept (comments, DO-18 wording, HD-18 one-host recipe gating pointer and cookie); check-schedule tests (HP-5..8, cms interval tests) await exactly the background work they scheduled, so they no longer flake under load.0141f5a5deco schema:@formaton a type alias reaches every field of that type (plain, optional, nullable, listed, literal select), so/** @format color */ type TextTone = "black" | "white"gets v7'sformat: "color"with its values kept. A field's own@formatwins. Unit test + conformance sch-26; documented in schema.mdx.3a985534v7→v8 migration skill: unwraps v7 async-rendering wrappers (website/sections/Rendering/Lazy.tsx,SingleDeferred.tsx,Deferred.tsx) to the sections they held, in pages and saved blocks; idempotent, with fixtures.32650f6breview fixes (overlay parts superseded):max-age, 60 s CMS draft TTL)forDraftschedules the release checkBoundedMap(LRU, optional size) instead of two map classesContentStorealready keeps drafts63b07191restored legacy secret guard exemption (above).9a45c8c7,85bf7112deco servehas no token; link on a wildcard host, case-insensitive--host, refused origins logged.dddefd54the CMS settings block,cms.settings(), host patterns,cms.draftPointer/cms.draftCookie, schema/check support, migration fold, docs.e7a1d1d1review fixes: per-call block maps (the Next proxy bug), settings held across a custom loader's update, frozen settings, userinfo and empty-label hosts, migration host normalisation and the TanStack deco-hosted hosts, the stale observability fixture.963fd2b4an instance stored by an older package copy is replaced, not adopted.766b0ff7schema.getwith no schema yet returns{ notModified: false, version: null, resolvedRef, schema: null }instead of NotFound (no.decofolder is still NotFound);blocks.list/blocks.applyalready work without one. Conformanceschema/absent+ cp-31; docs: content-protocol "No schema yet" (docs commitbefb394e, imported by deco-sites/docs-tanstack#4). Studio side: feat: support Blocks v8 (content protocol) behind a flag studio#7728.Tests
bun run check(typecheck, biome, knip) passes.bun run testat773229cc: 1863 passed, 64 skipped (71 files).v, no variants, no cookies/credentials/site token, no redirects)listas the union minus deletes//, backslash, uppercase)DECO_PREVIEW_API_DOMAINSreplacing the default, not settable from contentcontent-lengthLOADER_FAILEDon every call, never reusedsite/tokenneeded or sentlocalpointers__proto__entriesDecisions (product owner)
/preview, no preparing state, no object-storage overlays, no grants). Accepted with the docs' decisions:Loader.load()has no pointer.The default source is exactlySuperseded by decision 6.studio.decocms.com.localversion names no draft.deco schema(@formaton a type alias reaches its fields). The other differences are accepted.CMS(typecms-settings), which replaces thetelemetry/analyticsbuilt-ins and blocks outright. Code holds destinations, secrets and caps, includingpreview.hosts.cms.settings()reads only the production release in memory, never a draft and never the network. Drafts on a host outside the list get published content. The site editor edits the block under "Settings". There's no channel-manifest setting and no protocol change.773229cc):deco serveand the protocol handler have no token (Studio's session protects its/rpc); the cookie is v7's__deco_draft; draft hosts are v7's domains withDECO_PREVIEW_API_DOMAINS; v7'sDECO_OTEL_*names keep working; unused protocol options (request keys, schema preconditions, refs, advertised limits) are deleted.Stack: v8 ← … ← 15 ← 16
🤖 Generated with Claude Code
https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Summary by cubic
Hosted drafts now load as a pointer to only the branch's changes, layered over the production snapshot already in memory. The PR also centralizes preview, telemetry and analytics settings in the
CMSblock, moves draft helpers onto the CMS, and drops the content protocol's shared authentication anddeco serve's bearer token, origin allowlist, and Host check. The conformance tests now read the docs from deco-sites/docs-tanstack.Drafts and settings
{ format, set, delete }document — layers them over production, applies tombstones and variants, and fails withLOADER_FAILEDwithout a published fallback. Its hosts follow v7's patterns exactly (.decocms.comsuffix,local.studio.decocms.com,localhost,127.0.0.1,*.localhost;[::1]is no longer a default, but a listed[::1]still counts as loopback), withDECO_PREVIEW_API_DOMAINSreplacing the list.__deco_draftagain andpreview.sourcesis gone.cms.settings()reads release settings without fetching, applies defaults and caps, and gatescms.draftPointer()andcms.draftCookie()by configured preview hosts.telemetryandanalyticsbuilt-ins are replaced by thecms-settingsblock; existing sites must migrate these settings into.deco/blocks/CMS.json, and telemetry also reads v7'sDECO_OTEL_*environment names as aliases.deco serveon loopback). Request keys and idempotency receipts,ifSchemaMatch,refparams anddescribe.limitsare removed;blocks.applykeeps its own guards (500 names, 1 MiB per entry, 8 MiB per request).preview.hoststightens: URL credentials make a host unreadable except by*, and an empty label matches nothing but*.Migration and tooling
CMS.json, and unwraps v7 async-rendering wrappers. Lessons from a second TanStack + VTEX storefront cover the removed/deco/invoke(one server function per call site), gating draft bypass oncms.draftPointer, module-level config singletons as cross-request state, and parity gotchas (pinrequest.cf, track flake counts). The FastStore one covers a vendored runtime built from the published package, Pages Router SSG drafts hooked into Next's compiled runtimes, listing the dev host inpreview.hosts, anddeco checkgating the build. A placeholder site name replacesfila-storein the deployment skill.previewHostsare trimmed, lowercased and validated as host patterns, with the TanStack default hosts (<site>.deco.site,<site>.deco-cx.workers.dev) added when the site name is found;@decocms/blocks/cliexports the host parser.@formatannotations, mapsdatetimetodate-time, avoids saved-block pickers for free-form maps, and stamps every schema withblocksMajor: 8— the only signal Studio reads to tell a v8 site from a v7 one — whichdeco checkrequires.schema.getreports "no schema yet" asschema: null(withversion: null) instead ofNotFound, so clients can list and edit blocks without typed forms and keep polling until one appears.deco servehas no bearer token,--allow-origin, or Host check: it answers any origin's CORS, so it's for editing only, and without--hostit listens on127.0.0.1and::1on one port, so every startup line and the site-editor link saylocalhost.website/loaders/secret.tsblocks are excluded from the v8 secret guard so their existing ciphertext is preserved.Written for commit 73bc4f3. Summary will update on new commits.