@vinext/cloudflare 1.0.0-beta.1 → 1.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +193 -1
  2. package/dist/cache/cdn-adapter-config.d.ts +31 -0
  3. package/dist/cache/cdn-adapter-config.js +95 -0
  4. package/dist/cache/cdn-adapter.d.ts +48 -15
  5. package/dist/cache/cdn-adapter.js +50 -13
  6. package/dist/cache/cdn-adapter.runtime.d.ts +26 -4
  7. package/dist/cache/cdn-adapter.runtime.js +150 -37
  8. package/dist/cache/cdn-adapter.worker.d.ts +31 -0
  9. package/dist/cache/cdn-adapter.worker.js +373 -0
  10. package/dist/cache/cdn-build-id.d.ts +6 -0
  11. package/dist/cache/cdn-build-id.js +9 -0
  12. package/dist/cache/config.d.ts +137 -0
  13. package/dist/cache/config.js +87 -0
  14. package/dist/cache/kv-data-adapter.d.ts +32 -8
  15. package/dist/cache/kv-data-adapter.js +5 -1
  16. package/dist/cache/kv-data-adapter.runtime.d.ts +45 -13
  17. package/dist/cache/kv-data-adapter.runtime.js +132 -52
  18. package/dist/cache/kv-key.d.ts +15 -0
  19. package/dist/cache/kv-key.js +43 -0
  20. package/dist/cache/response-store-adapter.d.ts +73 -0
  21. package/dist/cache/response-store-adapter.js +64 -0
  22. package/dist/cache/response-store-adapter.self-contained.worker.d.ts +18 -0
  23. package/dist/cache/response-store-adapter.self-contained.worker.js +12 -0
  24. package/dist/cache/response-store-adapter.service-binding.worker.d.ts +16 -0
  25. package/dist/cache/response-store-adapter.service-binding.worker.js +11 -0
  26. package/dist/cache/response-store-adapter.worker.d.ts +14 -0
  27. package/dist/cache/response-store-adapter.worker.js +278 -0
  28. package/dist/cache/response-store-cdn.runtime.d.ts +5 -0
  29. package/dist/cache/response-store-cdn.runtime.js +41 -0
  30. package/dist/cache/response-store-data.runtime.d.ts +34 -0
  31. package/dist/cache/response-store-data.runtime.js +300 -0
  32. package/dist/cacheability-artifact.d.ts +10 -0
  33. package/dist/cacheability-artifact.js +215 -0
  34. package/dist/cacheability-manifest-limits.d.ts +4 -0
  35. package/dist/cacheability-manifest-limits.js +7 -0
  36. package/dist/cacheability-probe.d.ts +49 -0
  37. package/dist/cacheability-probe.js +660 -0
  38. package/dist/cdn-warm.d.ts +110 -12
  39. package/dist/cdn-warm.js +880 -82
  40. package/dist/cli.d.ts +1 -1
  41. package/dist/cli.js +15 -1
  42. package/dist/deploy-config.d.ts +14 -11
  43. package/dist/deploy-config.js +24 -5
  44. package/dist/deploy-help.d.ts +3 -4
  45. package/dist/deploy-help.js +65 -22
  46. package/dist/deploy.d.ts +139 -42
  47. package/dist/deploy.js +946 -132
  48. package/dist/images/images-optimizer.d.ts +5 -5
  49. package/dist/images/images-optimizer.runtime.d.ts +1 -5
  50. package/dist/prerender-kv-populate.d.ts +3 -4
  51. package/dist/prerender-kv-populate.js +51 -18
  52. package/dist/tpr.d.ts +34 -70
  53. package/dist/tpr.js +50 -603
  54. package/dist/utils/cache-control-metadata.d.ts +3 -4
  55. package/dist/version-deploy.d.ts +46 -17
  56. package/dist/version-deploy.js +151 -14
  57. package/dist/version-headers.d.ts +2 -0
  58. package/dist/version-headers.js +2 -0
  59. package/dist/worker-deployment-url.d.ts +9 -0
  60. package/dist/worker-deployment-url.js +45 -0
  61. package/dist/workers-dev-url.d.ts +2 -3
  62. package/dist/wrangler-config.d.ts +19 -0
  63. package/dist/wrangler-config.js +294 -0
  64. package/dist/wrangler-version-metadata.d.ts +7 -0
  65. package/dist/wrangler-version-metadata.js +9 -0
  66. package/package.json +21 -11
@@ -1,5 +1,80 @@
1
+ import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-build-id.js";
2
+ import { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "../version-headers.js";
3
+ import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
4
+ import { fnv1a64 } from "vinext/internal/utils/hash";
1
5
  import { getRequestExecutionContext } from "vinext/shims/request-context";
2
6
  //#region src/cache/cdn-adapter.runtime.ts
7
+ /**
8
+ * Cloudflare CDN cache adapter — edge-managed page-level ISR backed by the
9
+ * Cloudflare Workers Cache (`ctx.cache`).
10
+ *
11
+ * Unlike the origin-managed default adapter (which stores rendered artifacts in
12
+ * the data cache and serves HIT/STALE itself), this adapter delegates serving
13
+ * to Cloudflare's edge:
14
+ *
15
+ * - The origin never serves from a store — `get` returns `null`, so any
16
+ * request that reaches the Worker renders fresh. The edge absorbs HIT/STALE
17
+ * traffic and revalidates in the background (the `UPDATING` cache status).
18
+ * - `set` is a no-op: the platform caches the *response* based on its
19
+ * cache headers, so there is nothing to persist at the origin.
20
+ * - `buildResponseHeaders` emits the SWR policy as `Cloudflare-CDN-Cache-Control`
21
+ * (`public, max-age=…, stale-while-revalidate=…`) so the edge caches and
22
+ * revalidates. The inner response's `Cache-Control` is
23
+ * `public, max-age=0, must-revalidate` so a browser never serves a stored copy
24
+ * without revalidating against the edge; the uncached gateway changes that to
25
+ * `private, max-age=0, must-revalidate` before public egress so personalized
26
+ * request-stage headers cannot enter another shared cache. A `Cache-Tag`
27
+ * header lets entries be purged by tag. Note the edge directive uses `max-age`
28
+ * (not `s-maxage`):
29
+ * the framework computes the policy with `s-maxage` for shared caches, but
30
+ * `Cloudflare-CDN-Cache-Control` is already CDN-scoped so `max-age` is the correct knob
31
+ * for the edge to honor max-age + stale-while-revalidate.
32
+ * - `revalidateTag` purges the edge via the request context's `cache.purge({ tags })`.
33
+ *
34
+ * Tags use fixed-size lowercase digests before emission and purge because
35
+ * Workers Cache tags are case-insensitive printable ASCII, while Next.js tags
36
+ * are case-sensitive arbitrary strings.
37
+ *
38
+ * The default export is the adapter factory the generated
39
+ * `virtual:vinext-cache-adapters` registration imports; configure it from
40
+ * vite.config via the {@link cdnAdapter} builder in `./cdn-adapter.ts` (which
41
+ * `require.resolve`s this file).
42
+ */
43
+ const DEFAULT_VERSION_METADATA_BINDING = "CF_VERSION_METADATA";
44
+ const WORKER_VERSION_OVERRIDE_HEADER = "Cloudflare-Workers-Version-Overrides";
45
+ function versionValidationFailure(message, status = 500) {
46
+ return Response.json({ error: message }, {
47
+ status,
48
+ headers: { "Cache-Control": "no-store" }
49
+ });
50
+ }
51
+ const CACHEABLE_EDGE_DIRECTIVE_RE = /(?:^|,)\s*(?:s-maxage|max-age)\s*=/i;
52
+ const EDGE_POLICY_HEADERS = ["CDN-Cache-Control", "Cloudflare-CDN-Cache-Control"];
53
+ function getBuildIdentityResponseHeader() {
54
+ const buildId = getVinextCdnBuildIdentity();
55
+ return buildId ? { [VINEXT_CDN_BUILD_ID_HEADER]: buildId } : {};
56
+ }
57
+ /** Remove every response header whose cache semantics are owned by Cloudflare. */
58
+ function clearCloudflareCdnResponseHeaders(cacheControl) {
59
+ return {
60
+ ...getBuildIdentityResponseHeader(),
61
+ "Cache-Control": cacheControl,
62
+ "CDN-Cache-Control": null,
63
+ "Cloudflare-CDN-Cache-Control": null,
64
+ "Cache-Tag": null
65
+ };
66
+ }
67
+ /** Interpret Cloudflare's edge policy before core applies a replacement policy. */
68
+ function hasExplicitCloudflareNonCacheableResponsePolicy(headers) {
69
+ const edgePolicies = EDGE_POLICY_HEADERS.map((name) => headers.get(name));
70
+ if (edgePolicies.some((value) => value && isNonCacheableCacheControl(value))) return true;
71
+ const hasCacheableEdgePolicy = edgePolicies.some((value) => value && CACHEABLE_EDGE_DIRECTIVE_RE.test(value));
72
+ const browserPolicy = headers.get("Cache-Control");
73
+ return Boolean(!hasCacheableEdgePolicy && browserPolicy && isNonCacheableCacheControl(browserPolicy));
74
+ }
75
+ function readCloudflareResponseCacheControl(headers) {
76
+ return headers.get("Cloudflare-CDN-Cache-Control") ?? headers.get("CDN-Cache-Control") ?? headers.get("Cache-Control");
77
+ }
3
78
  function getWorkersCache() {
4
79
  const cache = getRequestExecutionContext()?.cache;
5
80
  if (cache && typeof cache.purge === "function") return cache;
@@ -28,7 +103,7 @@ const UNBOUNDED_SWR_SECONDS = 31536e3;
28
103
  /**
29
104
  * Convert the framework's shared-cache policy into a CDN-scoped one:
30
105
  * `s-maxage=…` → `max-age=…` (the edge honors `max-age` inside
31
- * `CDN-Cache-Control`), give a value-less `stale-while-revalidate` an explicit
106
+ * `Cloudflare-CDN-Cache-Control`), give a value-less `stale-while-revalidate` an explicit
32
107
  * seconds value (Cloudflare ignores the bare directive), and ensure a leading
33
108
  * `public`.
34
109
  */
@@ -37,30 +112,61 @@ function toEdgeCacheControl(cacheControl) {
37
112
  return /\bpublic\b/.test(withMaxAge) ? withMaxAge : `public, ${withMaxAge}`;
38
113
  }
39
114
  /**
40
- * Cloudflare's `Cache-Tag` header budget is 16 KB total with each tag capped at
41
- * 1024 bytes. Keep a conservative ceiling so a page with a large tag set never
42
- * produces an oversized (silently-dropped) header.
115
+ * Cloudflare's `Cache-Tag` header budget is 16 KB total. Fixed-size digests let
116
+ * the full Next.js tag set fit without dropping valid long or Unicode tags.
43
117
  */
44
- const MAX_CACHE_TAG_BYTES = 8 * 1024;
45
- const MAX_SINGLE_TAG_BYTES = 1024;
118
+ const MAX_CACHE_TAG_BYTES = 16384;
119
+ const CACHE_TAG_PREFIX = "vinext-";
120
+ /** Encode a case-sensitive Next.js tag into a fixed Workers Cache tag. */
121
+ function encodeCloudflareCacheTag(tag) {
122
+ return `${CACHE_TAG_PREFIX}${fnv1a64(`0:${tag}`)}${fnv1a64(`1:${tag}`)}`;
123
+ }
46
124
  /**
47
- * Build a `Cache-Tag` header value from canonicalised tags. Tags containing a
48
- * comma (the header separator) or exceeding the per-tag size are skipped, and
49
- * the whole value is bounded to stay within Cloudflare's limit.
125
+ * Build a complete `Cache-Tag` header value from canonicalised tags. Returning
126
+ * null makes the response uncacheable rather than caching with incomplete
127
+ * invalidation metadata.
50
128
  */
51
129
  function formatCacheTag(tags) {
52
- const parts = [];
53
- let total = 0;
54
- for (const tag of tags) {
55
- if (!tag || tag.includes(",") || tag.length > MAX_SINGLE_TAG_BYTES) continue;
56
- const next = total + tag.length + (parts.length > 0 ? 1 : 0);
57
- if (next > MAX_CACHE_TAG_BYTES) break;
58
- parts.push(tag);
59
- total = next;
60
- }
61
- return parts.length > 0 ? parts.join(",") : null;
130
+ const value = tags.map(encodeCloudflareCacheTag).join(",");
131
+ return value && value.length <= MAX_CACHE_TAG_BYTES ? value : null;
62
132
  }
63
133
  var CloudflareCdnCacheAdapter = class {
134
+ versionMetadata;
135
+ versionMetadataBinding;
136
+ requiresCompletedResponseAdmission = true;
137
+ responseVary = "verbatim";
138
+ responsePolicy = {
139
+ isHeader(name) {
140
+ const normalized = name.toLowerCase();
141
+ return EDGE_POLICY_HEADERS.some((header) => header.toLowerCase() === normalized);
142
+ },
143
+ readCacheControl(headers) {
144
+ return readCloudflareResponseCacheControl(headers);
145
+ },
146
+ hasExplicitNonCacheablePolicy(headers, baseline) {
147
+ if (baseline) {
148
+ for (const name of EDGE_POLICY_HEADERS) {
149
+ const value = headers.get(name);
150
+ if (value !== baseline.get(name) && value !== null && isNonCacheableCacheControl(value)) return true;
151
+ }
152
+ const browserPolicy = headers.get("Cache-Control");
153
+ return Boolean(browserPolicy !== baseline.get("Cache-Control") && browserPolicy && isNonCacheableCacheControl(browserPolicy));
154
+ }
155
+ return hasExplicitCloudflareNonCacheableResponsePolicy(headers);
156
+ }
157
+ };
158
+ constructor(versionMetadata, versionMetadataBinding = DEFAULT_VERSION_METADATA_BINDING) {
159
+ this.versionMetadata = versionMetadata;
160
+ this.versionMetadataBinding = versionMetadataBinding;
161
+ }
162
+ validateRequest(request) {
163
+ const expectedVersionId = request.headers.get(VINEXT_EXPECTED_WORKER_VERSION_HEADER);
164
+ if (expectedVersionId === null) return null;
165
+ if (!request.headers.has(WORKER_VERSION_OVERRIDE_HEADER)) return versionValidationFailure(`received ${VINEXT_EXPECTED_WORKER_VERSION_HEADER} without ${WORKER_VERSION_OVERRIDE_HEADER}.`);
166
+ if (!this.versionMetadata) return versionValidationFailure(`Cloudflare CDN prewarming requires the \`${this.versionMetadataBinding}\` version metadata binding. Add "version_metadata": { "binding": "${this.versionMetadataBinding}" } to the active Wrangler environment. Named environments do not inherit this binding from the top level.`);
167
+ if (expectedVersionId !== this.versionMetadata.id) return versionValidationFailure(`Cloudflare invoked Worker version ${this.versionMetadata.id}, but vinext warmup expected ${expectedVersionId}.`, 503);
168
+ return null;
169
+ }
64
170
  ownsBackgroundRevalidation = false;
65
171
  /**
66
172
  * The origin keeps no page store — return null so the request renders fresh.
@@ -71,33 +177,40 @@ var CloudflareCdnCacheAdapter = class {
71
177
  }
72
178
  /** No-op: the platform caches the response via its headers, not an origin store. */
73
179
  async set(_key, _data, _ctx) {}
180
+ buildResponseIdentityHeaders() {
181
+ return getBuildIdentityResponseHeader();
182
+ }
74
183
  buildResponseHeaders(input) {
75
- if (!input.cacheControl) return { "Cache-Control": NO_STORE };
76
- if (/\b(?:no-store|no-cache|private)\b/.test(input.cacheControl)) return {
77
- "Cache-Control": input.cacheControl,
78
- "CDN-Cache-Control": null,
79
- "Cloudflare-CDN-Cache-Control": null,
80
- "Cache-Tag": null
81
- };
82
- const headers = {
184
+ if (input.pendingDynamicCheck) return clearCloudflareCdnResponseHeaders(NO_STORE);
185
+ if (!input.cacheControl) return clearCloudflareCdnResponseHeaders(NO_STORE);
186
+ if (isNonCacheableCacheControl(input.cacheControl)) return clearCloudflareCdnResponseHeaders(input.cacheControl);
187
+ const cacheTag = input.tags?.length ? formatCacheTag(input.tags) : null;
188
+ if (input.tags?.length && !cacheTag) return clearCloudflareCdnResponseHeaders(NO_STORE);
189
+ return {
190
+ ...getBuildIdentityResponseHeader(),
83
191
  "Cache-Control": BROWSER_REVALIDATE,
84
- "CDN-Cache-Control": toEdgeCacheControl(input.cacheControl)
192
+ "CDN-Cache-Control": null,
193
+ "Cloudflare-CDN-Cache-Control": toEdgeCacheControl(input.cacheControl),
194
+ "Cache-Tag": cacheTag
85
195
  };
86
- if (input.tags && input.tags.length > 0) {
87
- const cacheTag = formatCacheTag(input.tags);
88
- if (cacheTag) headers["Cache-Tag"] = cacheTag;
89
- }
90
- return headers;
91
196
  }
92
197
  /** Purge edge-cached responses by tag via the request context's `cache.purge`. */
93
198
  async revalidateTag(tags, _durations) {
94
199
  const cache = getWorkersCache();
95
200
  if (!cache) return;
96
- const tagList = (Array.isArray(tags) ? tags : [tags]).filter((t) => typeof t === "string" && t.length > 0);
201
+ const tagList = (Array.isArray(tags) ? tags : [tags]).filter((t) => typeof t === "string");
97
202
  if (tagList.length === 0) return;
98
- await cache.purge({ tags: tagList });
203
+ const result = await cache.purge({ tags: tagList.map(encodeCloudflareCacheTag) });
204
+ if (result?.success === false) {
205
+ const errors = result.errors.map(({ code, message }) => `${code}: ${message}`).join(", ");
206
+ throw new Error(`[vinext] Workers Cache purge failed${errors ? `: ${errors}` : ""}`);
207
+ }
99
208
  }
100
209
  };
101
- const createCloudflareCdnCacheAdapter = () => new CloudflareCdnCacheAdapter();
210
+ const createCloudflareCdnCacheAdapter = ({ env, options } = {}) => {
211
+ const binding = options?.versionMetadataBinding ?? DEFAULT_VERSION_METADATA_BINDING;
212
+ const candidate = env?.[binding];
213
+ return new CloudflareCdnCacheAdapter(candidate && typeof candidate === "object" && "id" in candidate && typeof candidate.id === "string" ? { id: candidate.id } : void 0, binding);
214
+ };
102
215
  //#endregion
103
- export { CloudflareCdnCacheAdapter, createCloudflareCdnCacheAdapter as default };
216
+ export { CloudflareCdnCacheAdapter, createCloudflareCdnCacheAdapter as default, encodeCloudflareCacheTag };
@@ -0,0 +1,31 @@
1
+ import { WorkerEntrypoint } from "cloudflare:workers";
2
+ import "vinext/internal/server/cloudflare-workers-tracing";
3
+ import { VinextAssetFetcher } from "vinext/server/multi-stage";
4
+ //#region src/cache/cdn-adapter.worker.d.ts
5
+ type CloudflareStageContext = {
6
+ assets?: VinextAssetFetcher;
7
+ cache?: unknown;
8
+ exports?: Record<string, unknown>;
9
+ props?: unknown;
10
+ hostRuntime?: "worker";
11
+ passThroughOnException?(): void;
12
+ waitUntil?(promise: Promise<unknown>): void;
13
+ };
14
+ type CachePurgeOptions = {
15
+ tags: string[];
16
+ };
17
+ /** Cache-bearing entrypoint. Workers Cache HITs bypass this class entirely. */
18
+ export declare class VinextCachedResponse extends WorkerEntrypoint<unknown, unknown> {
19
+ fetch(request: Request): Promise<Response>;
20
+ purge(options: CachePurgeOptions): Promise<unknown>;
21
+ }
22
+ /** Uncached response entrypoint. Bypass and probe renders execute only here. */
23
+ export declare class VinextUncachedResponse extends WorkerEntrypoint<unknown, unknown> {
24
+ fetch(request: Request): Promise<Response>;
25
+ }
26
+ /** Uncached gateway: request routing and middleware always execute here. */
27
+ declare const _default: {
28
+ fetch(request: Request, env: unknown, context: CloudflareStageContext | undefined): Promise<Response>;
29
+ };
30
+ //#endregion
31
+ export { _default as default };
@@ -0,0 +1,373 @@
1
+ import { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity } from "./cdn-build-id.js";
2
+ import { NEXTJS_CACHE_HEADER, VINEXT_CACHE_HEADER, VINEXT_PRERENDER_READINESS_PATH, VINEXT_RSC_VARY_HEADER } from "vinext/internal/server/headers";
3
+ import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
4
+ import { WorkerEntrypoint } from "cloudflare:workers";
5
+ import "vinext/internal/server/cloudflare-workers-tracing";
6
+ import { loadVinextRequestStage } from "vinext/server/request-stage";
7
+ import { loadVinextResponseStage } from "vinext/server/response-stage";
8
+ import { traceCachedResponseStart } from "vinext/internal/server/response-start-tracing";
9
+ //#region src/cache/cdn-adapter.worker.ts
10
+ const CACHED_RESPONSE_STAGE_EXPORT = "VinextCachedResponse";
11
+ const UNCACHED_RESPONSE_STAGE_EXPORT = "VinextUncachedResponse";
12
+ const AUTHORIZATION_TRANSPORT_HEADER = "x-vinext-internal-authorization";
13
+ const REQUEST_CACHE_CONTROL_TRANSPORT_HEADER = "x-vinext-internal-request-cache-control";
14
+ const REQUEST_CF_TRANSPORT_HEADER = "x-vinext-internal-request-cf";
15
+ const REQUEST_PRAGMA_TRANSPORT_HEADER = "x-vinext-internal-request-pragma";
16
+ const CLOUDFLARE_EDGE_POLICY_HEADER = "Cloudflare-CDN-Cache-Control";
17
+ const SHARED_RESPONSE_STAGE_HEADER = "x-vinext-cloudflare-shared-response-stage";
18
+ const RESPONSE_STAGE_WIRE_CACHE = {
19
+ bypass: "vinext-cloudflare-v1:bypass",
20
+ shared: "vinext-cloudflare-v1:shared"
21
+ };
22
+ const FRAMEWORK_RESPONSE_VARY_FIELDS = new Set(VINEXT_RSC_VARY_HEADER.split(",").map((name) => name.trim().toLowerCase()));
23
+ function isResponseStageReadinessRequest(request) {
24
+ return request.url.includes(VINEXT_PRERENDER_READINESS_PATH) && new URL(request.url).pathname === VINEXT_PRERENDER_READINESS_PATH;
25
+ }
26
+ function responseStageUnavailable() {
27
+ return new Response(null, {
28
+ status: 503,
29
+ headers: { "Cache-Control": "no-store" }
30
+ });
31
+ }
32
+ /** Stamp the entrypoint that actually produced a response, before Workers Cache stores it. */
33
+ function stampResponseStageBuildIdentity(response) {
34
+ const buildIdentity = getVinextCdnBuildIdentity();
35
+ if (!buildIdentity) return response;
36
+ try {
37
+ response.headers.set(VINEXT_CDN_BUILD_ID_HEADER, buildIdentity);
38
+ return response;
39
+ } catch {
40
+ const headers = new Headers(response.headers);
41
+ headers.set(VINEXT_CDN_BUILD_ID_HEADER, buildIdentity);
42
+ const webSocket = response.webSocket;
43
+ if (!webSocket && (response.status < 200 || response.status > 599)) {
44
+ const unavailable = responseStageUnavailable();
45
+ unavailable.headers.set(VINEXT_CDN_BUILD_ID_HEADER, buildIdentity);
46
+ return unavailable;
47
+ }
48
+ const init = {
49
+ headers,
50
+ status: response.status,
51
+ statusText: response.statusText
52
+ };
53
+ if (webSocket) init.webSocket = webSocket;
54
+ return new Response(response.body, init);
55
+ }
56
+ }
57
+ /** Reject a response routed to an entrypoint from another propagating build. */
58
+ function validateResponseStageBuildIdentity(response) {
59
+ const expectedBuildIdentity = getVinextCdnBuildIdentity();
60
+ if (!expectedBuildIdentity || response.headers.get("X-Vinext-Build-Id") === expectedBuildIdentity) return response;
61
+ response.body?.cancel().catch(() => {});
62
+ return responseStageUnavailable();
63
+ }
64
+ function stripUntrustedTransportHeaders(request) {
65
+ if (!request.headers.has(AUTHORIZATION_TRANSPORT_HEADER) && !request.headers.has(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER) && !request.headers.has(REQUEST_CF_TRANSPORT_HEADER) && !request.headers.has(REQUEST_PRAGMA_TRANSPORT_HEADER)) return request;
66
+ const headers = new Headers(request.headers);
67
+ headers.delete(AUTHORIZATION_TRANSPORT_HEADER);
68
+ headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
69
+ headers.delete(REQUEST_CF_TRANSPORT_HEADER);
70
+ headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
71
+ const sanitized = new Request(request, { headers });
72
+ const requestCf = Reflect.get(request, "cf");
73
+ if (requestCf !== void 0) Object.defineProperty(sanitized, "cf", {
74
+ configurable: true,
75
+ enumerable: true,
76
+ value: requestCf
77
+ });
78
+ return sanitized;
79
+ }
80
+ function withWorkerHostRuntime(context, env) {
81
+ const candidateAssets = env && typeof env === "object" && Reflect.has(env, "ASSETS") ? Reflect.get(env, "ASSETS") : void 0;
82
+ const assets = candidateAssets && (typeof candidateAssets === "object" || typeof candidateAssets === "function") && Reflect.has(candidateAssets, "fetch") && typeof Reflect.get(candidateAssets, "fetch") === "function" ? candidateAssets : context?.assets;
83
+ return {
84
+ ...assets === void 0 ? {} : { assets },
85
+ ...context?.cache === void 0 ? {} : { cache: context.cache },
86
+ ...context?.exports === void 0 ? {} : { exports: context.exports },
87
+ hostRuntime: "worker",
88
+ ...typeof context?.passThroughOnException === "function" ? { passThroughOnException: () => context.passThroughOnException?.() } : {},
89
+ ...context?.props === void 0 ? {} : { props: context.props },
90
+ ...typeof context?.waitUntil === "function" ? { waitUntil: (promise) => context.waitUntil?.(promise) } : {}
91
+ };
92
+ }
93
+ function hasFetch(value) {
94
+ return value !== null && (typeof value === "object" || typeof value === "function") && "fetch" in value && typeof value.fetch === "function";
95
+ }
96
+ function hasPurge(value) {
97
+ return value !== null && (typeof value === "object" || typeof value === "function") && "purge" in value && typeof value.purge === "function";
98
+ }
99
+ function getResponseStageBinding(context, exportName, serializedInvocation) {
100
+ const binding = context.exports?.[exportName];
101
+ if (typeof binding !== "function") return null;
102
+ const target = binding({ props: JSON.parse(serializedInvocation) });
103
+ return hasFetch(target) ? target : null;
104
+ }
105
+ async function createCacheFacingRequest(request, serializedInvocation) {
106
+ const authorization = request.headers.get("Authorization");
107
+ let serializedRequestCf = null;
108
+ const requestCf = Reflect.get(request, "cf");
109
+ if (requestCf !== void 0) try {
110
+ const json = JSON.stringify(requestCf);
111
+ if (json !== void 0) serializedRequestCf = encodeURIComponent(json);
112
+ } catch {}
113
+ const authorizationIdentity = authorization === null ? "absent" : `present:${authorization.length}:${authorization}`;
114
+ const frameworkVaryIdentity = JSON.stringify([...FRAMEWORK_RESPONSE_VARY_FIELDS].map((name) => [name, request.headers.get(name)]));
115
+ const responseStageBuildIdentity = getVinextCdnBuildIdentity() ?? "";
116
+ const bytes = new TextEncoder().encode(`${request.url}\0${serializedInvocation}\0${authorizationIdentity}\0${frameworkVaryIdentity}\0${responseStageBuildIdentity}`);
117
+ const key = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
118
+ const url = new URL(request.url);
119
+ url.searchParams.set("__vinext_cache_key", key);
120
+ const headers = new Headers(request.headers);
121
+ const requestCacheControl = headers.get("Cache-Control");
122
+ const requestPragma = headers.get("Pragma");
123
+ headers.delete("Cache-Control");
124
+ headers.delete("Pragma");
125
+ headers.delete("Authorization");
126
+ headers.delete(AUTHORIZATION_TRANSPORT_HEADER);
127
+ headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
128
+ headers.delete(REQUEST_CF_TRANSPORT_HEADER);
129
+ headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
130
+ if (authorization !== null) headers.set(AUTHORIZATION_TRANSPORT_HEADER, encodeURIComponent(authorization));
131
+ if (serializedRequestCf !== null) headers.set(REQUEST_CF_TRANSPORT_HEADER, serializedRequestCf);
132
+ if (requestCacheControl !== null) headers.set(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER, encodeURIComponent(requestCacheControl));
133
+ if (requestPragma !== null) headers.set(REQUEST_PRAGMA_TRANSPORT_HEADER, encodeURIComponent(requestPragma));
134
+ return new Request(url, {
135
+ cf: { vary: { default: { action: "passthrough" } } },
136
+ headers,
137
+ method: request.method
138
+ });
139
+ }
140
+ function restoreResponseStageRequest(request, requestUrl, requestMethod) {
141
+ const headers = new Headers(request.headers);
142
+ const serializedAuthorization = headers.get(AUTHORIZATION_TRANSPORT_HEADER);
143
+ const serializedRequestCacheControl = headers.get(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
144
+ const serializedRequestCf = headers.get(REQUEST_CF_TRANSPORT_HEADER);
145
+ const serializedRequestPragma = headers.get(REQUEST_PRAGMA_TRANSPORT_HEADER);
146
+ headers.delete("Cache-Control");
147
+ headers.delete("Pragma");
148
+ headers.delete("Authorization");
149
+ headers.delete(AUTHORIZATION_TRANSPORT_HEADER);
150
+ headers.delete(REQUEST_CACHE_CONTROL_TRANSPORT_HEADER);
151
+ headers.delete(REQUEST_CF_TRANSPORT_HEADER);
152
+ headers.delete(REQUEST_PRAGMA_TRANSPORT_HEADER);
153
+ if (serializedAuthorization !== null) try {
154
+ headers.set("Authorization", decodeURIComponent(serializedAuthorization));
155
+ } catch {}
156
+ let requestCf;
157
+ if (serializedRequestCf !== null) try {
158
+ requestCf = JSON.parse(decodeURIComponent(serializedRequestCf));
159
+ } catch {}
160
+ if (serializedRequestCacheControl !== null) try {
161
+ headers.set("Cache-Control", decodeURIComponent(serializedRequestCacheControl));
162
+ } catch {}
163
+ if (serializedRequestPragma !== null) try {
164
+ headers.set("Pragma", decodeURIComponent(serializedRequestPragma));
165
+ } catch {}
166
+ const restored = new Request(new Request(requestUrl, request), {
167
+ headers,
168
+ method: requestMethod
169
+ });
170
+ let didAccessRequestCf = false;
171
+ if (requestCf !== void 0) Object.defineProperty(restored, "cf", {
172
+ configurable: true,
173
+ enumerable: true,
174
+ get() {
175
+ didAccessRequestCf = true;
176
+ return requestCf;
177
+ }
178
+ });
179
+ return {
180
+ didAccessRequestCf: () => didAccessRequestCf,
181
+ request: restored
182
+ };
183
+ }
184
+ function preventResponseCaching(response) {
185
+ const headers = new Headers(response.headers);
186
+ headers.set("Cache-Control", "no-store");
187
+ headers.delete("CDN-Cache-Control");
188
+ headers.delete("Cloudflare-CDN-Cache-Control");
189
+ headers.delete("Cache-Tag");
190
+ return new Response(response.body, {
191
+ headers,
192
+ status: response.status,
193
+ statusText: response.statusText
194
+ });
195
+ }
196
+ /**
197
+ * Cloudflare consumes its private cache policy before returning a cached
198
+ * entrypoint response. Strip it here as well for uncached/local fallbacks so
199
+ * the outer gateway never forwards an inner shared-cache policy after adding
200
+ * request-specific middleware or routing headers.
201
+ */
202
+ function markSharedResponseStage(response, provenanceToken, responseStageProps, exposeEntrypointCacheStatus = false) {
203
+ const headers = new Headers(response.headers);
204
+ const cacheStatus = exposeEntrypointCacheStatus ? headers.get("CF-Cache-Status") : null;
205
+ headers.set(SHARED_RESPONSE_STAGE_HEADER, cacheStatus ? `${provenanceToken}:${encodeURIComponent(cacheStatus)}` : provenanceToken);
206
+ return traceCachedResponseStart(new Response(response.body, {
207
+ headers,
208
+ status: response.status,
209
+ statusText: response.statusText
210
+ }), cacheStatus, responseStageProps);
211
+ }
212
+ function finalizeGatewayResponse(response, provenanceToken) {
213
+ const provenance = response.headers.get(SHARED_RESPONSE_STAGE_HEADER);
214
+ const usedSharedResponseStage = provenance === provenanceToken || provenance?.startsWith(`${provenanceToken}:`) === true;
215
+ const sharedResponseStageCollision = provenance !== null && !usedSharedResponseStage;
216
+ const cacheControl = response.headers.get("Cache-Control");
217
+ if (!response.headers.has(CLOUDFLARE_EDGE_POLICY_HEADER) && !usedSharedResponseStage && !sharedResponseStageCollision) return response;
218
+ const headers = new Headers(response.headers);
219
+ headers.delete(SHARED_RESPONSE_STAGE_HEADER);
220
+ headers.delete(CLOUDFLARE_EDGE_POLICY_HEADER);
221
+ if (usedSharedResponseStage || sharedResponseStageCollision) {
222
+ headers.delete("CDN-Cache-Control");
223
+ headers.delete("Cache-Tag");
224
+ headers.delete(VINEXT_CACHE_HEADER);
225
+ headers.delete(NEXTJS_CACHE_HEADER);
226
+ if (!cacheControl || !isNonCacheableCacheControl(cacheControl)) headers.set("Cache-Control", "private, max-age=0, must-revalidate");
227
+ }
228
+ if (usedSharedResponseStage && provenance?.startsWith(`${provenanceToken}:`)) {
229
+ const cacheStatus = decodeURIComponent(provenance.slice(provenanceToken.length + 1));
230
+ headers.set(VINEXT_CACHE_HEADER, cacheStatus);
231
+ headers.set(NEXTJS_CACHE_HEADER, cacheStatus);
232
+ }
233
+ return new Response(response.body, {
234
+ headers,
235
+ status: response.status,
236
+ statusText: response.statusText
237
+ });
238
+ }
239
+ function hasTaggedCustomVary(response) {
240
+ if (!response.headers.get("Cache-Tag")) return false;
241
+ return (response.headers.get("Vary") ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean).some((name) => !FRAMEWORK_RESPONSE_VARY_FIELDS.has(name));
242
+ }
243
+ function withResponseStagePurge(context) {
244
+ const factory = context.exports?.[CACHED_RESPONSE_STAGE_EXPORT];
245
+ if (typeof factory !== "function") return context;
246
+ const fallback = context.cache;
247
+ return {
248
+ ...context,
249
+ cache: { purge(options) {
250
+ const target = factory({ props: {} });
251
+ if (hasPurge(target)) return target.purge(options);
252
+ return hasPurge(fallback) ? fallback.purge(options) : void 0;
253
+ } }
254
+ };
255
+ }
256
+ function getResponseStageInvocation(value, expectedCache) {
257
+ if (!value || typeof value !== "object") return null;
258
+ const expectedResponseStageBuildIdentity = Reflect.get(value, "expectedResponseStageBuildIdentity");
259
+ if (expectedResponseStageBuildIdentity !== void 0 && typeof expectedResponseStageBuildIdentity !== "string") return null;
260
+ const options = Reflect.get(value, "options");
261
+ if (!options || typeof options !== "object") return null;
262
+ const wireCache = Reflect.get(options, "cache");
263
+ let cache;
264
+ if (expectedResponseStageBuildIdentity !== void 0 && wireCache === RESPONSE_STAGE_WIRE_CACHE.shared) cache = "shared";
265
+ else if (expectedResponseStageBuildIdentity !== void 0 && wireCache === RESPONSE_STAGE_WIRE_CACHE.bypass) cache = "bypass";
266
+ else if (expectedResponseStageBuildIdentity === void 0 && getVinextCdnBuildIdentity() === null && (wireCache === "shared" || wireCache === "bypass")) cache = wireCache;
267
+ else return null;
268
+ if (expectedCache !== void 0 && cache !== expectedCache) return null;
269
+ const requestUrl = Reflect.get(value, "requestUrl");
270
+ if (typeof requestUrl !== "string") return null;
271
+ const requestMethod = Reflect.get(value, "requestMethod");
272
+ if (typeof requestMethod !== "string" || requestMethod.length === 0) return null;
273
+ try {
274
+ new URL(requestUrl);
275
+ } catch {
276
+ return null;
277
+ }
278
+ return {
279
+ ...typeof expectedResponseStageBuildIdentity === "string" ? { expectedResponseStageBuildIdentity } : {},
280
+ options: {
281
+ ...options,
282
+ cache
283
+ },
284
+ props: Reflect.get(value, "props"),
285
+ requestMethod,
286
+ requestUrl
287
+ };
288
+ }
289
+ async function invokeResponseStage(request, env, context, invocation) {
290
+ const dispatchResponseStage = (stageRequest, props, options) => invokeResponseStage(stageRequest, env, context, {
291
+ options,
292
+ props,
293
+ requestMethod: stageRequest.method,
294
+ requestUrl: stageRequest.url
295
+ });
296
+ const dispatchRequestStage = async (stageRequest) => {
297
+ const { handleRequestStage } = await loadVinextRequestStage();
298
+ return handleRequestStage(stageRequest, env, context, dispatchResponseStage);
299
+ };
300
+ const { handleResponseStage } = await loadVinextResponseStage();
301
+ return handleResponseStage(request, env, context, invocation.props, dispatchRequestStage, invocation.options);
302
+ }
303
+ /** Cache-bearing entrypoint. Workers Cache HITs bypass this class entirely. */
304
+ var VinextCachedResponse = class extends WorkerEntrypoint {
305
+ async fetch(request) {
306
+ const context = withWorkerHostRuntime(this.ctx, this.env);
307
+ const invocation = getResponseStageInvocation(context.props, "shared");
308
+ if (!invocation) return stampResponseStageBuildIdentity(new Response("Invalid vinext response-stage invocation", {
309
+ status: 400,
310
+ headers: { "Cache-Control": "no-store" }
311
+ }));
312
+ if (invocation.expectedResponseStageBuildIdentity !== void 0 && invocation.expectedResponseStageBuildIdentity !== getVinextCdnBuildIdentity()) return stampResponseStageBuildIdentity(responseStageUnavailable());
313
+ const restored = restoreResponseStageRequest(request, invocation.requestUrl, invocation.requestMethod);
314
+ const response = await invokeResponseStage(restored.request, this.env, context, invocation);
315
+ return stampResponseStageBuildIdentity(restored.didAccessRequestCf() || hasTaggedCustomVary(response) ? preventResponseCaching(response) : response);
316
+ }
317
+ async purge(options) {
318
+ const cache = Reflect.get(this.ctx, "cache");
319
+ if (!hasPurge(cache)) return void 0;
320
+ return cache.purge(options);
321
+ }
322
+ };
323
+ /** Uncached response entrypoint. Bypass and probe renders execute only here. */
324
+ var VinextUncachedResponse = class extends WorkerEntrypoint {
325
+ async fetch(request) {
326
+ const context = withResponseStagePurge(withWorkerHostRuntime(this.ctx, this.env));
327
+ const invocation = getResponseStageInvocation(context.props, "bypass");
328
+ if (!invocation) return stampResponseStageBuildIdentity(new Response("Invalid vinext response-stage invocation", {
329
+ status: 400,
330
+ headers: { "Cache-Control": "no-store" }
331
+ }));
332
+ if (invocation.expectedResponseStageBuildIdentity !== void 0 && invocation.expectedResponseStageBuildIdentity !== getVinextCdnBuildIdentity()) return stampResponseStageBuildIdentity(responseStageUnavailable());
333
+ return stampResponseStageBuildIdentity(await invokeResponseStage(request, this.env, context, invocation));
334
+ }
335
+ };
336
+ /** Uncached gateway: request routing and middleware always execute here. */
337
+ var cdn_adapter_worker_default = { async fetch(request, env, context) {
338
+ request = stripUntrustedTransportHeaders(request);
339
+ const sharedResponseStageProvenance = crypto.randomUUID();
340
+ const stageContext = withResponseStagePurge(withWorkerHostRuntime(context, env));
341
+ const dispatchResponseStage = async (stageRequest, props, options) => {
342
+ const expectedResponseStageBuildIdentity = getVinextCdnBuildIdentity();
343
+ const invocation = {
344
+ ...expectedResponseStageBuildIdentity === null ? {} : { expectedResponseStageBuildIdentity },
345
+ options,
346
+ props,
347
+ requestMethod: stageRequest.method,
348
+ requestUrl: stageRequest.url
349
+ };
350
+ const usesSharedCache = options.cache === "shared";
351
+ try {
352
+ const serializedInvocation = JSON.stringify({
353
+ ...invocation,
354
+ options: expectedResponseStageBuildIdentity === null ? options : {
355
+ ...options,
356
+ cache: RESPONSE_STAGE_WIRE_CACHE[options.cache]
357
+ }
358
+ });
359
+ const binding = getResponseStageBinding(stageContext, usesSharedCache ? CACHED_RESPONSE_STAGE_EXPORT : UNCACHED_RESPONSE_STAGE_EXPORT, serializedInvocation);
360
+ if (!binding) return responseStageUnavailable();
361
+ const entrypointRequest = usesSharedCache ? await createCacheFacingRequest(stageRequest, serializedInvocation) : stageRequest;
362
+ const response = validateResponseStageBuildIdentity(await binding.fetch(entrypointRequest));
363
+ return usesSharedCache ? markSharedResponseStage(response, sharedResponseStageProvenance, props, true) : response;
364
+ } catch (error) {
365
+ if (isResponseStageReadinessRequest(stageRequest)) return responseStageUnavailable();
366
+ throw error;
367
+ }
368
+ };
369
+ const { handleRequestStage } = await loadVinextRequestStage();
370
+ return finalizeGatewayResponse(await handleRequestStage(request, env, stageContext, dispatchResponseStage), sharedResponseStageProvenance);
371
+ } };
372
+ //#endregion
373
+ export { VinextCachedResponse, VinextUncachedResponse, cdn_adapter_worker_default as default };
@@ -0,0 +1,6 @@
1
+ //#region src/cache/cdn-build-id.d.ts
2
+ /** Build identity stamped by the Cloudflare CDN adapter on page responses. */
3
+ export declare const VINEXT_CDN_BUILD_ID_HEADER = "X-Vinext-Build-Id";
4
+ /** Opaque identity shared by every server entry emitted by one vite build. */
5
+ export declare function getVinextCdnBuildIdentity(): string | null;
6
+ //#endregion
@@ -0,0 +1,9 @@
1
+ //#region src/cache/cdn-build-id.ts
2
+ /** Build identity stamped by the Cloudflare CDN adapter on page responses. */
3
+ const VINEXT_CDN_BUILD_ID_HEADER = "X-Vinext-Build-Id";
4
+ /** Opaque identity shared by every server entry emitted by one vite build. */
5
+ function getVinextCdnBuildIdentity() {
6
+ return process.env.__VINEXT_RSC_BUILD_IDENTITY || process.env.__VINEXT_BUILD_ID || null;
7
+ }
8
+ //#endregion
9
+ export { VINEXT_CDN_BUILD_ID_HEADER, getVinextCdnBuildIdentity };