feat(tanstack): server issues the CDN segment token, so regionalized stores engage - #524
Conversation
…stores engage The first version had the client derive the token from what it could observe (`navigator.userAgent`), and rejected any segment richer than device. That is why it engaged on exactly one kind of site: one whose segment reduces to device alone. A regionalized VTEX store — most of them — segments on region too, and region comes from `request.cf.regionCode`, which exists only on the server. Observed on a live site: the client sent `__cseg=desktop.<build>` while the worker recomputed `desktop|sc=1|r=RJ`, so the marker never matched and the feature was permanently inert. Fail-closed did its job; the feature just never did anything. Invert it: the server computes the token from the segment it already built, publishes it to the page, and the client echoes it back. - `segmentToken` now folds sales channel, region, geo and a site's own custom dimensions INTO the token instead of refusing them. Those are what the key must distinguish, not reasons to give up on caching. `loggedIn` is still a hard refusal — a personalized response never belongs in a shared entry, no matter how precise the key. - The token is a hash: `regionId` can contain the old separator (`v2.XXXX`), values would need escaping, and there is no reason to publish a visitor's region in a URL that ends up in logs. - `requestSegmentDescriptor` folds in `__cf_geo`, which `buildSegment` does not carry — it comes from `buildGeoCacheParam`. Both now read the same helpers so the URL cannot distinguish less than the Worker's key does. - `CdnSegmentMarker` publishes it via the RequestContext bag, the same server-to-client path `DraftPreviewIndicator` uses. Per-request through AsyncLocalStorage — a module global here would be the layout-cache race again. Verification is unchanged and still by recomputation, so the marker is never trusted. Verified end to end on a built site with no site-level config: the worker issued `1q6wwk1`, the SSR published it, the client echoed it, and the response came back `public, max-age=1800`. Fail-closed intact on the same build — no marker, forged token, another device's token, bot UA and login cookie all `no-store`, HTML documents `no-store`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/tanstack/src/sdk/cdnSegment.ts">
<violation number="1" location="packages/tanstack/src/sdk/cdnSegment.ts:70">
P1: When two segment descriptors collide in this 32-bit hash, `cdnCacheableServerFn` accepts the same `__cseg` marker for both requests, allowing a front cache to serve one region or custom-dimension response to another. Use a collision-resistant digest, making token generation and verification asynchronous if necessary.</violation>
</file>
<file name="packages/tanstack/src/hooks/CdnSegmentMarker.tsx">
<violation number="1" location="packages/tanstack/src/hooks/CdnSegmentMarker.tsx:24">
P2: When SSR publishes a token, the client hydration tree omits this server-rendered `<script>` because the browser `RequestContext` bag is empty. React therefore reports a hydration mismatch and can discard the SSR tree; read `window.__DECO_CSEG` on the client and render the same script node, as `DraftPreviewIndicator` does.</violation>
</file>
<file name="packages/tanstack/src/sdk/workerEntry.test.ts">
<violation number="1" location="packages/tanstack/src/sdk/workerEntry.test.ts:828">
P2: The geo regression test no longer exercises the leak it claims to guard. The old test drove a geo-keyed site through the worker and asserted `CDN-Cache-Control: no-store`; the replacement only asserts that `segmentToken` returns different hashes for two hand-written strings (`desktop|geo=BR|RJ` vs `desktop|geo=BR|SP`). Any hash differentiates distinct inputs, so this passes even if `requestSegmentDescriptor` dropped the geo dimension entirely — exactly the cross-region data leak this PR is meant to prevent — and it never runs the server-issue / client-echo / verify loop. Since regionalized pricing/stock is the security-critical path here, add a test whose token comes from the worker's real descriptor (e.g. build `hashSegment(buildSegment(req))` + `|geo=...` and send it against a geo-keyed worker, or factor `requestSegmentDescriptor` out so the test and production share it).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if (!segmentDescriptor) return null; | ||
|
|
||
| return `${seg.device}.${buildHash}`; | ||
| return djb2Hex(`${segmentDescriptor}|${buildHash}`); |
There was a problem hiding this comment.
P1: When two segment descriptors collide in this 32-bit hash, cdnCacheableServerFn accepts the same __cseg marker for both requests, allowing a front cache to serve one region or custom-dimension response to another. Use a collision-resistant digest, making token generation and verification asynchronous if necessary.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tanstack/src/sdk/cdnSegment.ts, line 70:
<comment>When two segment descriptors collide in this 32-bit hash, `cdnCacheableServerFn` accepts the same `__cseg` marker for both requests, allowing a front cache to serve one region or custom-dimension response to another. Use a collision-resistant digest, making token generation and verification asynchronous if necessary.</comment>
<file context>
@@ -1,74 +1,71 @@
+ if (!segmentDescriptor) return null;
- return `${seg.device}.${buildHash}`;
+ return djb2Hex(`${segmentDescriptor}|${buildHash}`);
}
</file context>
| import { CSEG_BAG_KEY, CSEG_GLOBAL } from "../sdk/cdnSegment"; | ||
|
|
||
| export function CdnSegmentMarker() { | ||
| const token = RequestContext.getBag<string>(CSEG_BAG_KEY); |
There was a problem hiding this comment.
P2: When SSR publishes a token, the client hydration tree omits this server-rendered <script> because the browser RequestContext bag is empty. React therefore reports a hydration mismatch and can discard the SSR tree; read window.__DECO_CSEG on the client and render the same script node, as DraftPreviewIndicator does.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tanstack/src/hooks/CdnSegmentMarker.tsx, line 24:
<comment>When SSR publishes a token, the client hydration tree omits this server-rendered `<script>` because the browser `RequestContext` bag is empty. React therefore reports a hydration mismatch and can discard the SSR tree; read `window.__DECO_CSEG` on the client and render the same script node, as `DraftPreviewIndicator` does.</comment>
<file context>
@@ -0,0 +1,35 @@
+import { CSEG_BAG_KEY, CSEG_GLOBAL } from "../sdk/cdnSegment";
+
+export function CdnSegmentMarker() {
+ const token = RequestContext.getBag<string>(CSEG_BAG_KEY);
+ if (!token) return null;
+ return (
</file context>
| it("never lets two regions share a token", () => { | ||
| // The leak this guards: RJ and SP resolving to the same colo must not | ||
| // share an entry, since their regionalized pricing and stock differ. | ||
| expect(tok("desktop|geo=BR|RJ")).not.toBe(tok("desktop|geo=BR|SP")); |
There was a problem hiding this comment.
P2: The geo regression test no longer exercises the leak it claims to guard. The old test drove a geo-keyed site through the worker and asserted CDN-Cache-Control: no-store; the replacement only asserts that segmentToken returns different hashes for two hand-written strings (desktop|geo=BR|RJ vs desktop|geo=BR|SP). Any hash differentiates distinct inputs, so this passes even if requestSegmentDescriptor dropped the geo dimension entirely — exactly the cross-region data leak this PR is meant to prevent — and it never runs the server-issue / client-echo / verify loop. Since regionalized pricing/stock is the security-critical path here, add a test whose token comes from the worker's real descriptor (e.g. build hashSegment(buildSegment(req)) + |geo=... and send it against a geo-keyed worker, or factor requestSegmentDescriptor out so the test and production share it).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tanstack/src/sdk/workerEntry.test.ts, line 828:
<comment>The geo regression test no longer exercises the leak it claims to guard. The old test drove a geo-keyed site through the worker and asserted `CDN-Cache-Control: no-store`; the replacement only asserts that `segmentToken` returns different hashes for two hand-written strings (`desktop|geo=BR|RJ` vs `desktop|geo=BR|SP`). Any hash differentiates distinct inputs, so this passes even if `requestSegmentDescriptor` dropped the geo dimension entirely — exactly the cross-region data leak this PR is meant to prevent — and it never runs the server-issue / client-echo / verify loop. Since regionalized pricing/stock is the security-critical path here, add a test whose token comes from the worker's real descriptor (e.g. build `hashSegment(buildSegment(req))` + `|geo=...` and send it against a geo-keyed worker, or factor `requestSegmentDescriptor` out so the test and production share it).</comment>
<file context>
@@ -774,72 +778,69 @@ describe('cdnCacheControl: "serverfn-segment"', () => {
+ it("never lets two regions share a token", () => {
+ // The leak this guards: RJ and SP resolving to the same colo must not
+ // share an entry, since their regionalized pricing and stock differ.
+ expect(tok("desktop|geo=BR|RJ")).not.toBe(tok("desktop|geo=BR|SP"));
});
</file context>
|
🎉 This PR is included in version 7.58.0 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
Follow-up to #523. That PR made the feature arrive with a version bump — but on a real store it turned out to
be permanently inert, and this fixes why.
What we saw in production
montecarlo, on 7.57, with the marker being sent correctly:The client sent
device. The worker recomputeddevice + salesChannel + regionand refused. Fail-closed didexactly its job — the feature just never did anything.
Why the client could never get it right
devicesalesChannel/ VTEXregionIdvtex_segmentrequest.cf.regionCodeThe
r=RJabove is a Cloudflare region code, not a VTEXregionId— it comes fromrequest.cf, which thebrowser never sees. So on any regionalized store, a client-derived token could not match. And regionalized is
the normal case, not the exception.
The fix: invert who issues the token
The server computes it from the segment it already built, publishes it to the page, the client echoes it back.
segmentTokenfolds dimensions in instead of refusing them. Sales channel, region, geo and a site's owncustom fields are what the key must distinguish — not reasons to give up on caching.
loggedInremains ahard refusal: a personalized response never belongs in a shared entry, however precise the key.
regionIdcan contain the previous separator (v2.XXXX), values would needescaping, and there is no reason to publish a visitor's region in a URL that lands in logs.
requestSegmentDescriptorfolds in__cf_geo, whichbuildSegmentdoes not carry — it comes frombuildGeoCacheParam. Both read the same helpers, so the URL cannot distinguish less than the Worker's key.CdnSegmentMarkerpublishes it through the RequestContext bag — the same server-to-client pathDraftPreviewIndicatoralready uses. Per-request via AsyncLocalStorage; a module global here would be thelayout-cache race all over again.
Verification is unchanged: the worker recomputes and compares. The marker is never trusted.
Verified end to end
Built site, no site-level config (
src/start.tsabsent, nocdnCacheControl):Fail-closed on that same build:
public, max-age=1800✅no-storeno-storeno-storeno-storeno-storeno-store2592tests passing. Typecheck clean.Trade-off worth knowing
More dimensions in the URL means more entries to warm. A store spread across many regions will see a lower hit
rate in front of the Worker than one concentrated in a few.
That is a smaller ceiling, not a regression: a miss in front of the Worker falls through to the Worker's own
cache — which is already segmented — not to VTEX. The floor is exactly today's behaviour.
🤖 Generated with Claude Code
Summary by cubic
Switches the CDN segment marker from a client-derived token to one the server issues, so regionalized stores actually engage. Previously the client built the token from the user agent and refused any segment richer than device, but region is resolved from
request.cfon the server — a client token never matched on a regionalized store and the feature stayed inert.What changed
segmentTokennow hashes the full segment descriptor (device, sales channel, region, geo, custom dimensions) instead of refusing those dimensions;loggedInstill yields no token.CdnSegmentMarkerwrites it to the page anddecoServerFnFetchechoes it back on/_serverFnURLs.regionIdcan contain the old separator and a visitor's region should not appear in logged URLs.no-storeon an exact match, so stale or forged markers are harmless.Trade-off
Written for commit dd24327. Summary will update on new commits.