Skip to content

Commit 8138eb1

Browse files
committed
Bound the Cloudflare WordPress runtime
1 parent c590c4a commit 8138eb1

11 files changed

Lines changed: 261 additions & 35 deletions

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
"smoke": "tsx scripts/run-smoke.ts",
9696
"test:redaction": "tsx tests/redaction.test.ts",
9797
"test:cloudflare-runtime": "tsx tests/cloudflare-runtime.test.ts && node ./node_modules/typescript/bin/tsc -p packages/runtime-cloudflare --noEmit",
98+
"test:cloudflare-wordpress-auth": "tsx tests/cloudflare-wordpress-auth.test.ts",
99+
"test:cloudflare-wordpress-archive-corpus": "tsx tests/cloudflare-wordpress-archive-corpus.test.ts",
98100
"test:agent-task-contracts": "tsx tests/agent-task-contracts.test.ts && tsx tests/agent-task-canonical-evidence.test.ts && npm run test:agent-task-workflow-interface && npm run test:runtime-sources-materialization && tsx tests/agent-task-reusable-workflow.test.ts",
99101
"test:agent-task-workflow-interface": "tsx tests/run-agent-task-reusable-workflow-interface.test.ts",
100102
"test:agent-task-runtime-package-staging": "tsx tests/agent-task-runtime-package-staging.test.ts",

packages/runtime-cloudflare/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ The entry Worker executes PHP-WASM and WordPress. The named `WordPressStateCoord
88

99
On cold start, the entry Worker uses the acquired pointer to rebuild PHP-WASM's disposable SQLite index from canonical MDI Markdown and JSON files. A missing pointer materializes the packaged canonical MDI seed and boots one PHP-WASM primary runtime. The build-time PHP CLI generator creates that archive from `wordpress-install-seed.sqlite` through MDI's public `bootstrap_existing_cache()` API, validates its pinned MDI revision and input digest, and never packages SQLite. The runtime updates `siteurl` and `home` through WordPress APIs using the request origin and sets the admin password from `WORDPRESS_ADMIN_PASSWORD`; only WordPress's password hash is canonical. Bootstrap persists and CAS-promotes this mutation before serving the next request.
1010

11+
Canonical browser, health, and mutation boots require the separately managed `WORDPRESS_AUTH_SECRET` Worker secret. The entry Worker derives the eight WordPress auth keys and salts from that secret with a versioned, site-scoped (`default`) SHA-256 domain separator before `wp-load.php`. It never logs, persists, or returns the secret or derived values. Configure it independently from the bootstrap password with `wrangler secret put WORDPRESS_AUTH_SECRET --config packages/runtime-cloudflare/wrangler.jsonc`; rotating `WORDPRESS_ADMIN_PASSWORD` does not rotate authentication salts or invalidate sessions.
12+
1113
After each mutating HTTP request, the entry runtime invokes MDI's explicit request-boundary flush, collects canonical files, stores immutable content-addressed R2 objects and a revision manifest, then commits the new pointer through the held lease. GET, HEAD, and asset requests release without promotion. Failed requests abort their leases; stale leases recover by token/version/expiry checks. The entry isolate can cache one runtime only for the exact acquired pointer revision and exits it after promotion or when another isolate advances the pointer. It does not persist SQLite. Existing manifests are reused when canonical file hashes have not changed.
1214

15+
The Worker forwards browser cookies directly to Playground and disables Playground's internal cookie store, preventing an empty per-isolate store from replacing a valid browser session after cold restart. `?phase=canonical-auth` is a boolean-only diagnostic for the canonical runtime; it reports cookie parsing and verification stages without returning credential, token, hash, or salt material.
16+
1317
The bundled MDI source is pinned to immutable commit `1870fb41279e7eb5946e506c9c7406f1f1ea6dc3` from [MDI PR #127](https://github.com/Automattic/markdown-database-integration/pull/127). The bundle generator, worker provenance, and source-contract test use the same revision.
1418

1519
## Verification
1620

1721
1. Run `npm run generate:cloudflare-canonical-mdi-seed` to regenerate the deterministic canonical MDI archive and provenance manifest.
1822
2. Run `npm run test:cloudflare-runtime` for routing, canonical-state, generator reproducibility, filtered archive, source contract, and TypeScript coverage.
1923
3. Run `npm run cloudflare:dry-run` to compile the Worker without creating Cloudflare resources.
20-
4. Run `npm run cloudflare:local-gate` for isolated local workerd evidence. It injects a test admin password and verifies the login form, cookie-authenticated admin redirect, authenticated REST post publication, public rendering, representative frontend/admin assets, PHP diagnostics, and the same login/post checks after a cold restart using the persisted R2/DO state.
24+
4. Run `npm run cloudflare:local-gate` for isolated local workerd evidence. It injects stable test-only admin-password and auth-secret values, verifies the login form, cookie-authenticated admin redirect, authenticated REST post publication, public rendering, representative frontend/admin/editor assets, PHP diagnostics, an existing authenticated cookie after cold restart, and a fresh login after restart.
2125

2226
This document describes local candidate verification only. It does not claim remote deployment.

packages/runtime-cloudflare/src/request-routing.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export type WorkerRequestRoute =
33
| { kind: "health" }
44
| { kind: "r2-state" }
55
| { kind: "r2-mutate" }
6+
| { kind: "canonical-auth" }
67
| { kind: "probe"; phase: string }
78

89
export function routeWorkerRequest(request: Request): WorkerRequestRoute {
@@ -11,5 +12,6 @@ export function routeWorkerRequest(request: Request): WorkerRequestRoute {
1112
if (phase === "health") return { kind: "health" }
1213
if (phase === "r2-state") return { kind: "r2-state" }
1314
if (phase === "r2-mutate") return { kind: "r2-mutate" }
15+
if (phase === "canonical-auth") return { kind: "canonical-auth" }
1416
return { kind: "probe", phase }
1517
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const WORDPRESS_AUTH_CONSTANTS = ["AUTH_KEY", "SECURE_AUTH_KEY", "LOGGED_IN_KEY", "NONCE_KEY", "AUTH_SALT", "SECURE_AUTH_SALT", "LOGGED_IN_SALT", "NONCE_SALT"] as const
2+
3+
export type WordPressAuthConstant = (typeof WORDPRESS_AUTH_CONSTANTS)[number]
4+
5+
export async function deriveWordPressAuthConstants(rootSecret: string, site: string): Promise<Record<WordPressAuthConstant, string>> {
6+
if (!rootSecret) throw new Error("WORDPRESS_AUTH_SECRET is required for canonical WordPress runtime boots.")
7+
const encoder = new TextEncoder()
8+
const entries = await Promise.all(WORDPRESS_AUTH_CONSTANTS.map(async (name) => {
9+
const input = encoder.encode(`wp-codebox/wordpress-auth/v1\0${site}\0${name}\0${rootSecret}`)
10+
const digest = await crypto.subtle.digest("SHA-256", input)
11+
return [name, Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")] as const
12+
}))
13+
return Object.fromEntries(entries) as Record<WordPressAuthConstant, string>
14+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export interface WordPressArchiveEntry {
2+
path: string
3+
uncompressedSize: number
4+
isDirectory: boolean
5+
}
6+
7+
const REQUIRED_RUNTIME_EXTENSION = /\.(?:php|json|crt|html)$/
8+
const RUNTIME_FONT_OR_IMAGE_EXTENSION = /\.(?:woff2?|ttf|otf|eot|svg|png|jpe?g|gif|webp|avif|ico)$/
9+
const RUNTIME_SCRIPT_OR_STYLE_EXTENSION = /\.(?:css|js|mjs)$/
10+
11+
export function isWordPressRuntimeFile(path: string, archivePaths: ReadonlySet<string>): boolean {
12+
if (!path.startsWith("wordpress/") || path.endsWith("/") || path.endsWith(".map")) return false
13+
if (path.startsWith("wordpress/wp-content/themes/")) return true
14+
if (REQUIRED_RUNTIME_EXTENSION.test(path)) return true
15+
if (path.includes("/src/") || path.includes("/test/") || path.includes("/tests/") || path.includes("/node_modules/")) return false
16+
if (RUNTIME_FONT_OR_IMAGE_EXTENSION.test(path)) return true
17+
if (!RUNTIME_SCRIPT_OR_STYLE_EXTENSION.test(path)) return false
18+
if (path.endsWith(".mjs") || path.endsWith(".min.js") || path.endsWith(".min.css")) return true
19+
20+
const minifiedSibling = path.replace(/\.(?:js|css)$/, (extension) => `.min${extension}`)
21+
return !archivePaths.has(minifiedSibling)
22+
}
23+
24+
export function summarizeWordPressRuntimeCorpus(entries: Iterable<WordPressArchiveEntry>) {
25+
const entryList = Array.from(entries)
26+
const archivePaths = new Set(entryList.map((entry) => entry.path))
27+
const selected = entryList.filter((entry) => !entry.isDirectory && isWordPressRuntimeFile(entry.path, archivePaths))
28+
return {
29+
selected,
30+
selectedFiles: selected.length,
31+
selectedBytes: selected.reduce((total, entry) => total + entry.uncompressedSize, 0),
32+
}
33+
}

0 commit comments

Comments
 (0)