Skip to content

Add support for skew protection #746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ pnpm-lock.yaml
.vscode/setting.json
test-fixtures
test-snapshots
playwright-report
playwright-report
2 changes: 1 addition & 1 deletion examples/playground14/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": "worker.ts",
"name": "api",
"name": "playground14",
"compatibility_date": "2024-12-30",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
Expand Down
3 changes: 2 additions & 1 deletion examples/playground15/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
import { initOpenNextCloudflareForDev, getDeploymentId } from "@opennextjs/cloudflare";

initOpenNextCloudflareForDev();

Expand All @@ -10,6 +10,7 @@ const nextConfig = {
// Generate source map to validate the fix for opennextjs/opennextjs-cloudflare#341
serverSourceMaps: true,
},
deploymentId: getDeploymentId(),
};

export default nextConfig;
16 changes: 10 additions & 6 deletions examples/playground15/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
import { defineCloudflareConfig, type OpenNextConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";

export default defineCloudflareConfig({
incrementalCache: kvIncrementalCache,
enableCacheInterception: true,
});
export default {
...defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
}),
cloudflare: {
skewProtectionEnabled: true,
},
} satisfies OpenNextConfig;
2 changes: 1 addition & 1 deletion examples/playground15/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"cf-typegen": "wrangler types --env-interface CloudflareEnv"
},
"dependencies": {
"next": "^15.1.7",
"next": "^15.3.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
Expand Down
8 changes: 4 additions & 4 deletions examples/playground15/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "api",
"name": "playground15",
"compatibility_date": "2024-12-30",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"kv_namespaces": [
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_KV",
"id": "<BINDING_ID>"
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "pg15"
}
],
"vars": {
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@types/mock-fs": "catalog:",
"@types/node": "catalog:",
"@types/picomatch": "^4.0.0",
"cloudflare": "^4.4.1",
"diff": "^8.0.2",
"esbuild": "catalog:",
"eslint": "catalog:",
Expand Down
10 changes: 10 additions & 0 deletions packages/cloudflare/src/api/cloudflare-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ declare global {
CACHE_PURGE_ZONE_ID?: string;
// The API token to use for the cache purge. It should have the `Cache Purge` permission
CACHE_PURGE_API_TOKEN?: string;

// The following variables must be provided when skew protection is enabled
// The name of the worker (as defined in the wrangler configuration)
CF_WORKER_NAME?: string;
// The subdomain where the previews are deployed, i.e. `<version-name>.<domain>.workers.dev`
CF_PREVIEW_DOMAIN?: string;
// Should have the `Workers Scripts:Read` permission
CF_WORKERS_SCRIPTS_API_TOKEN?: string;
// Cloudflare account id
CF_ACCOUNT_ID?: string;
}
}

Expand Down
16 changes: 16 additions & 0 deletions packages/cloudflare/src/api/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ interface OpenNextConfig extends AwsOpenNextConfig {
* @default false
*/
dangerousDisableConfigValidation?: boolean;

/**
* Enable skew protection.
*
* Note: Skew Protection is experimental and might break on minor releases.
*
* @default false
*/
skewProtectionEnabled?: boolean;
};
}

Expand All @@ -169,4 +178,11 @@ export function getOpenNextConfig(buildOpts: BuildOptions): OpenNextConfig {
return buildOpts.config;
}

/**
* @returns Unique deployment ID
*/
export function getDeploymentId(): string {
return `dpl-${new Date().getTime().toString(36)}`;
}

export type { OpenNextConfig };
2 changes: 1 addition & 1 deletion packages/cloudflare/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./cloudflare-context.js";
export { defineCloudflareConfig, type OpenNextConfig } from "./config.js";
export { defineCloudflareConfig, getDeploymentId, type OpenNextConfig } from "./config.js";
18 changes: 7 additions & 11 deletions packages/cloudflare/src/cli/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-ass
import { compileEnvFiles } from "./open-next/compile-env-files.js";
import { compileImages } from "./open-next/compile-images.js";
import { compileInit } from "./open-next/compile-init.js";
import { compileSkewProtection } from "./open-next/compile-skew-protection.js";
import { compileDurableObjects } from "./open-next/compileDurableObjects.js";
import { createServerBundle } from "./open-next/createServerBundle.js";
import { createWranglerConfigIfNotExistent } from "./utils/index.js";
Expand Down Expand Up @@ -58,18 +59,7 @@ export async function build(
printHeader("Generating bundle");
buildHelper.initOutputDir(options);

// Compile cache.ts
compileCache(options);

// Compile .env files
compileEnvFiles(options);

// Compile workerd init
compileInit(options);

// Compile image helpers
compileImages(options);

// Compile middleware
await createMiddleware(options, { forceOnlyBuildOnce: true });

Expand All @@ -83,6 +73,12 @@ export async function build(
}
}

compileEnvFiles(options);
compileInit(options);
compileImages(options);
// Compile skew protection, needs the assets to be copied first (see `createStaticAssets`)
compileSkewProtection(options, config);

await createServerBundle(options);

await compileDurableObjects(options);
Expand Down
2 changes: 2 additions & 0 deletions packages/cloudflare/src/cli/build/open-next/compile-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function compileInit(options: BuildOptions) {

const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
const basePath = nextConfig.basePath ?? "";
const deploymentId = nextConfig.deploymentId ?? "";

await build({
entryPoints: [initPath],
Expand All @@ -27,6 +28,7 @@ export async function compileInit(options: BuildOptions) {
define: {
__BUILD_TIMESTAMP_MS__: JSON.stringify(Date.now()),
__NEXT_BASE_PATH__: JSON.stringify(basePath),
__DEPLOYMENT_ID__: JSON.stringify(deploymentId),
},
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { describe, expect, it } from "vitest";

import { filesToTree } from "./compile-skew-protection";

describe("filesToTree", () => {
it("should return an empty tree for an empty array of paths", () => {
const paths: string[] = [];
const tree = filesToTree(paths);
expect(tree).toEqual({ f: [], d: {} });
});

it("should correctly add a single file at the root", () => {
const paths = ["file.txt"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: ["file.txt"],
d: {},
});
});

it("should correctly add multiple files at the root", () => {
const paths = ["file1.txt", "file2.txt"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: ["file1.txt", "file2.txt"],
d: {},
});
});

it("should correctly add a single file in a single folder", () => {
const paths = ["folder/file.txt"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: [],
d: {
folder: {
f: ["file.txt"],
d: {},
},
},
});
});

it("should correctly add multiple files in the same folder", () => {
const paths = ["folder/file1.txt", "folder/file2.txt"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: [],
d: {
folder: {
f: ["file1.txt", "file2.txt"],
d: {},
},
},
});
});

it("should correctly add files in nested folders", () => {
const paths = ["folder1/folder2/file.txt"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: [],
d: {
folder1: {
f: [],
d: { folder2: { f: ["file.txt"], d: {} } },
},
},
});
});

it("should handle mixed files and folders at different levels", () => {
const paths = ["root_file.txt", "folderA/fileA.txt", "folderA/subfolderB/fileB.txt", "folderC/fileC.txt"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: ["root_file.txt"],
d: {
folderA: {
f: ["fileA.txt"],
d: {
subfolderB: {
f: ["fileB.txt"],
d: {},
},
},
},
folderC: {
f: ["fileC.txt"],
d: {},
},
},
});
});

it("should handle paths with leading/trailing slashes gracefully", () => {
const paths = ["/folder/file.txt", "another_folder/file.txt/"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: [],
d: {
folder: {
f: ["file.txt"],
d: {},
},
another_folder: {
f: ["file.txt"], // Trailing slash on file name is removed by filter(Boolean)
d: {},
},
},
});
});

it("should handle duplicate file names in different folders", () => {
const paths = ["folder1/file.txt", "folder2/file.txt"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: [],
d: {
folder1: {
f: ["file.txt"],
d: {},
},
folder2: {
f: ["file.txt"],
d: {},
},
},
});
});

it("should handle folders with the same name but different parents", () => {
const paths = ["a/b/file1.txt", "c/b/file2.txt"];
const tree = filesToTree(paths);
expect(tree).toEqual({
f: [],
d: {
a: {
f: [],
d: {
b: {
f: ["file1.txt"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is horrible to look at with tab 😄

d: {},
},
},
},
c: {
f: [],
d: {
b: {
f: ["file2.txt"],
d: {},
},
},
},
},
});
});
});
Loading
Loading