From 7b86ff651473e0480c2503cd1c2e5b430f392305 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 09:14:31 -0400 Subject: [PATCH 01/15] Instrument Cloudflare WordPress boot phases --- .../src/php-wasm-universal.d.ts | 1 + packages/runtime-cloudflare/src/worker.ts | 62 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts index 68a6eb24..3c410275 100644 --- a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts +++ b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts @@ -2,6 +2,7 @@ declare module "@php-wasm/universal" { export class PHP { constructor(runtimeId: number) run(request: { code: string }): Promise<{ text: string }> + exit(code?: number): void } export function loadPHPRuntime(loader: { diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index 7096bd1b..52f687da 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -1,5 +1,5 @@ import { loadPHPRuntime, PHP } from "@php-wasm/universal" -import { bootWordPressAndRequestHandler } from "@wp-playground/wordpress" +import { bootWordPressAndRequestHandler, type WordPressInstallMode } from "@wp-playground/wordpress" // The PHP-WASM package publishes this Emscripten loader without TypeScript declarations. // @ts-expect-error The adjacent Wasm declaration covers the compiled binary import. import { dependenciesTotalSize, init } from "../../../node_modules/@php-wasm/web-8-5/asyncify/php_8_5.js" @@ -13,7 +13,10 @@ const SITE_URL = "https://wp-codebox-runtime.invalid" let bootPromise: Promise<{ php: PHP; wordpressVersion: string }> | undefined export default { - async fetch(): Promise { + async fetch(request: Request): Promise { + const phase = new URL(request.url).searchParams.get("phase") + if (phase) return runBootProbe(phase) + const runtime = await (bootPromise ??= bootWordPressRuntime()) const phpVersion = (await runtime.php.run({ code: " { +async function runBootProbe(phase: string): Promise { + if (phase === "archives") { + const wordpressZip = await fetchArchive(WORDPRESS_ARCHIVE_URL, "wordpress.zip") + const sqliteZip = await fetchArchive(SQLITE_INTEGRATION_ARCHIVE_URL, "sqlite-database-integration.zip") + return probeResponse(phase, { wordpressArchiveBytes: wordpressZip.size, sqliteArchiveBytes: sqliteZip.size }) + } + + if (phase === "php") { + const php = new PHP(await createPhpRuntime()) + try { + const phpVersion = (await php.run({ code: " { const requestHandler = await bootWordPressAndRequestHandler({ - createPhpRuntime: () => loadPHPRuntime({ dependencyFilename: "php_8_5.wasm", dependenciesTotalSize, phpWasmAsyncMode: "asyncify", init }, { instantiateWasm: instantiatePrecompiledWasm(phpWasmModule) }), + createPhpRuntime, maxPhpInstances: 1, phpVersion: "8.5", siteUrl: SITE_URL, wordPressZip: fetchArchive(WORDPRESS_ARCHIVE_URL, "wordpress.zip"), - sqliteIntegrationPluginZip: fetchArchive(SQLITE_INTEGRATION_ARCHIVE_URL, "sqlite-database-integration.zip"), - wordpressInstallMode: "install-from-existing-files", + sqliteIntegrationPluginZip: includeSqlite ? fetchArchive(SQLITE_INTEGRATION_ARCHIVE_URL, "sqlite-database-integration.zip") : undefined, + wordpressInstallMode, }) const php = await requestHandler.getPrimaryPhp() const wordpressVersion = (await php.run({ code: " { const response = await fetch(url) if (!response.ok) throw new Error(`Unable to fetch ${name}: ${response.status}.`) @@ -52,3 +98,7 @@ async function fetchArchive(url: string, name: string): Promise { function instantiatePrecompiledWasm(module: WebAssembly.Module) { return (imports: WebAssembly.Imports, receiveInstance: (instance: WebAssembly.Instance, wasmModule: WebAssembly.Module) => void) => receiveInstance(new WebAssembly.Instance(module, imports), module) } + +function probeResponse(phase: string, evidence: Record): Response { + return Response.json({ schema: "wp-codebox/cloudflare-boot-probe/v1", phase, completed: true, evidence }) +} From acf4f248e78062ec4603f20e9b2f82275003317b Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 09:16:13 -0400 Subject: [PATCH 02/15] Split Cloudflare archive memory probes --- packages/runtime-cloudflare/src/worker.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index 52f687da..a5526a43 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -31,6 +31,13 @@ export default { } async function runBootProbe(phase: string): Promise { + if (phase === "wordpress-archive" || phase === "sqlite-archive") { + const archive = phase === "wordpress-archive" + ? await fetchArchive(WORDPRESS_ARCHIVE_URL, "wordpress.zip") + : await fetchArchive(SQLITE_INTEGRATION_ARCHIVE_URL, "sqlite-database-integration.zip") + return probeResponse(phase, { archiveBytes: archive.size }) + } + if (phase === "archives") { const wordpressZip = await fetchArchive(WORDPRESS_ARCHIVE_URL, "wordpress.zip") const sqliteZip = await fetchArchive(SQLITE_INTEGRATION_ARCHIVE_URL, "sqlite-database-integration.zip") From 888acc4a00433987eb79983826afdab049bee678 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 09:18:45 -0400 Subject: [PATCH 03/15] Probe PHP and WordPress archive overlap --- packages/runtime-cloudflare/src/worker.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index a5526a43..0cb619e6 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -54,6 +54,20 @@ async function runBootProbe(phase: string): Promise { } } + if (phase === "php-wordpress-archive" || phase === "wordpress-archive-php") { + const archive = phase === "wordpress-archive-php" + ? await fetchArchive(WORDPRESS_ARCHIVE_URL, "wordpress.zip") + : undefined + const php = new PHP(await createPhpRuntime()) + try { + const wordpressZip = archive ?? await fetchArchive(WORDPRESS_ARCHIVE_URL, "wordpress.zip") + const phpVersion = (await php.run({ code: " Date: Sat, 18 Jul 2026 09:22:24 -0400 Subject: [PATCH 04/15] Stream WordPress server files in Cloudflare boot --- package-lock.json | 1 + packages/runtime-cloudflare/package.json | 1 + .../src/php-wasm-universal.d.ts | 2 ++ packages/runtime-cloudflare/src/worker.ts | 30 +++++++++++++++++-- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a29e0c68..3de95ea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5061,6 +5061,7 @@ "version": "0.12.27", "dependencies": { "@automattic/wp-codebox-core": "file:../runtime-core", + "@php-wasm/stream-compression": "^3.1.45", "@php-wasm/universal": "3.1.45", "@php-wasm/web-8-5": "3.1.45", "@wp-playground/wordpress": "3.1.45" diff --git a/packages/runtime-cloudflare/package.json b/packages/runtime-cloudflare/package.json index c1711849..d0b7e661 100644 --- a/packages/runtime-cloudflare/package.json +++ b/packages/runtime-cloudflare/package.json @@ -5,6 +5,7 @@ "type": "module", "dependencies": { "@automattic/wp-codebox-core": "file:../runtime-core", + "@php-wasm/stream-compression": "^3.1.45", "@php-wasm/universal": "3.1.45", "@php-wasm/web-8-5": "3.1.45", "@wp-playground/wordpress": "3.1.45" diff --git a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts index 3c410275..235a181e 100644 --- a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts +++ b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts @@ -2,6 +2,8 @@ declare module "@php-wasm/universal" { export class PHP { constructor(runtimeId: number) run(request: { code: string }): Promise<{ text: string }> + mkdir(path: string): void + writeFile(path: string, data: Uint8Array): void exit(code?: number): void } diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index 0cb619e6..4c765b2f 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -1,4 +1,5 @@ import { loadPHPRuntime, PHP } from "@php-wasm/universal" +import { decodeZip } from "@php-wasm/stream-compression" import { bootWordPressAndRequestHandler, type WordPressInstallMode } from "@wp-playground/wordpress" // The PHP-WASM package publishes this Emscripten loader without TypeScript declarations. // @ts-expect-error The adjacent Wasm declaration covers the compiled binary import. @@ -68,10 +69,11 @@ async function runBootProbe(phase: string): Promise { } } - if (phase === "wordpress-files" || phase === "sqlite" || phase === "full") { + if (phase === "wordpress-files" || phase === "sqlite" || phase === "full" || phase === "streamed-wordpress") { const runtime = await bootWordPressRuntime( - phase === "full" ? "install-from-existing-files" : "do-not-attempt-installing", + phase === "full" || phase === "streamed-wordpress" ? "install-from-existing-files" : "do-not-attempt-installing", phase !== "wordpress-files", + phase === "streamed-wordpress", ) try { const phpVersion = (await runtime.php.run({ code: " { async function bootWordPressRuntime( wordpressInstallMode: WordPressInstallMode = "install-from-existing-files", includeSqlite = true, + streamWordPressFiles = false, ): Promise<{ php: PHP; wordpressVersion: string }> { const requestHandler = await bootWordPressAndRequestHandler({ createPhpRuntime, + hooks: streamWordPressFiles ? { beforeWordPressFiles: materializeWordPressServerFiles } : undefined, maxPhpInstances: 1, phpVersion: "8.5", siteUrl: SITE_URL, - wordPressZip: fetchArchive(WORDPRESS_ARCHIVE_URL, "wordpress.zip"), + wordPressZip: streamWordPressFiles ? undefined : fetchArchive(WORDPRESS_ARCHIVE_URL, "wordpress.zip"), sqliteIntegrationPluginZip: includeSqlite ? fetchArchive(SQLITE_INTEGRATION_ARCHIVE_URL, "sqlite-database-integration.zip") : undefined, wordpressInstallMode, }) @@ -103,6 +107,26 @@ async function bootWordPressRuntime( return { php, wordpressVersion } } +async function materializeWordPressServerFiles(php: PHP): Promise { + const response = await fetch(WORDPRESS_ARCHIVE_URL) + if (!response.ok || !response.body) throw new Error(`Unable to stream wordpress.zip: ${response.status}.`) + + const reader = decodeZip(response.body).getReader() + while (true) { + const { done, value: file } = await reader.read() + if (done) break + if (!file.name.startsWith("wordpress/") || file.name.endsWith("/") || !isWordPressServerFile(file.name)) continue + + const destination = `/${file.name}` + php.mkdir(destination.slice(0, destination.lastIndexOf("/"))) + php.writeFile(destination, new Uint8Array(await file.arrayBuffer())) + } +} + +function isWordPressServerFile(path: string): boolean { + return /\.(?:php|json|crt|html)$/.test(path) || path.endsWith("/style.css") +} + function createPhpRuntime() { return loadPHPRuntime( { dependencyFilename: "php_8_5.wasm", dependenciesTotalSize, phpWasmAsyncMode: "asyncify", init }, From 8208e1953a347f62ba42f243d14cd1fbc1183a69 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 09:25:21 -0400 Subject: [PATCH 05/15] Split streamed WordPress boot probes --- packages/runtime-cloudflare/src/worker.ts | 25 +++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index 4c765b2f..bdc4a271 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -69,11 +69,22 @@ async function runBootProbe(phase: string): Promise { } } - if (phase === "wordpress-files" || phase === "sqlite" || phase === "full" || phase === "streamed-wordpress") { + if (phase === "streamed-files") { + const php = new PHP(await createPhpRuntime()) + try { + const evidence = await materializeWordPressServerFiles(php) + const wordpressVersion = (await php.run({ code: " { +async function materializeWordPressServerFiles(php: PHP): Promise<{ materializedFiles: number; materializedBytes: number }> { const response = await fetch(WORDPRESS_ARCHIVE_URL) if (!response.ok || !response.body) throw new Error(`Unable to stream wordpress.zip: ${response.status}.`) + let materializedFiles = 0 + let materializedBytes = 0 const reader = decodeZip(response.body).getReader() while (true) { const { done, value: file } = await reader.read() @@ -118,9 +131,13 @@ async function materializeWordPressServerFiles(php: PHP): Promise { if (!file.name.startsWith("wordpress/") || file.name.endsWith("/") || !isWordPressServerFile(file.name)) continue const destination = `/${file.name}` + const bytes = new Uint8Array(await file.arrayBuffer()) php.mkdir(destination.slice(0, destination.lastIndexOf("/"))) - php.writeFile(destination, new Uint8Array(await file.arrayBuffer())) + php.writeFile(destination, bytes) + materializedFiles++ + materializedBytes += bytes.byteLength } + return { materializedFiles, materializedBytes } } function isWordPressServerFile(path: string): boolean { From 394d62b698f2b356b108109f08558e26da5a25cc Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 10:34:09 -0400 Subject: [PATCH 06/15] Filter remote WordPress archive entries --- packages/runtime-cloudflare/src/worker.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index bdc4a271..53e53695 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -1,5 +1,5 @@ import { loadPHPRuntime, PHP } from "@php-wasm/universal" -import { decodeZip } from "@php-wasm/stream-compression" +import { decodeRemoteZip } from "@php-wasm/stream-compression" import { bootWordPressAndRequestHandler, type WordPressInstallMode } from "@wp-playground/wordpress" // The PHP-WASM package publishes this Emscripten loader without TypeScript declarations. // @ts-expect-error The adjacent Wasm declaration covers the compiled binary import. @@ -119,19 +119,19 @@ async function bootWordPressRuntime( } async function materializeWordPressServerFiles(php: PHP): Promise<{ materializedFiles: number; materializedBytes: number }> { - const response = await fetch(WORDPRESS_ARCHIVE_URL) - if (!response.ok || !response.body) throw new Error(`Unable to stream wordpress.zip: ${response.status}.`) - + const decoder = new TextDecoder() let materializedFiles = 0 let materializedBytes = 0 - const reader = decodeZip(response.body).getReader() + const stream = await decodeRemoteZip(WORDPRESS_ARCHIVE_URL, (entry: { path: Uint8Array }) => isWordPressServerFile(decoder.decode(entry.path))) + const reader = stream.getReader() while (true) { - const { done, value: file } = await reader.read() + const { done, value: entry } = await reader.read() if (done) break - if (!file.name.startsWith("wordpress/") || file.name.endsWith("/") || !isWordPressServerFile(file.name)) continue + const path = entry instanceof File ? entry.name : decoder.decode(entry.path) + if (!path.startsWith("wordpress/") || path.endsWith("/")) continue - const destination = `/${file.name}` - const bytes = new Uint8Array(await file.arrayBuffer()) + const destination = `/${path}` + const bytes = entry instanceof File ? new Uint8Array(await entry.arrayBuffer()) : entry.bytes php.mkdir(destination.slice(0, destination.lastIndexOf("/"))) php.writeFile(destination, bytes) materializedFiles++ From 4c7bd069cf92f2bac4644e835fbe1b3363c2b7d1 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 10:58:40 -0400 Subject: [PATCH 07/15] Boot Cloudflare WordPress from install seed --- .../assets/wordpress-install-seed.sqlite | Bin 0 -> 376832 bytes packages/runtime-cloudflare/src/sqlite.d.ts | 4 ++ packages/runtime-cloudflare/src/worker.ts | 37 +++++++++++++++++- packages/runtime-cloudflare/wrangler.jsonc | 5 ++- tests/cloudflare-runtime.test.ts | 8 ++++ 5 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 packages/runtime-cloudflare/assets/wordpress-install-seed.sqlite create mode 100644 packages/runtime-cloudflare/src/sqlite.d.ts diff --git a/packages/runtime-cloudflare/assets/wordpress-install-seed.sqlite b/packages/runtime-cloudflare/assets/wordpress-install-seed.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..3607f024a68946e792e2185ddfaddaa05eeb3a93 GIT binary patch literal 376832 zcmeFa31Az^bta61Aa1lMih?MLVp9}{#E_IoQ4}T3p`l1f;)sXDLlS2^20@@n_6P(x zXh0M_vwkj=l1X29P4;($4BC1$8qA_#QxWg?PTKENt`$y zJM+I+)eSU&ZlFPF#xu_2Xiz|R)qAhLdiAR6b=Rx;t5b4L5?3?Xjd)HRw(YUm?Y4(R z(PpzZ@27M8AUn&&-0#ZHap}rj6o_?|C+GO-Pq?xSPGru&y5Sw+y zVRm9x91!J=&6KnurE|rG1XlCrRWYs!pr6jY-^&ZvwP` z7sPn88OS$1b|bbsn#S@?&P+sa>`FjmdQ7$pbJ1u$&@r_&rZ={@FdLhk0XsAuomtR` zny4nDqkS0D;|49ZFj>*_wQ^=D(BmCHk(J^(Fy3NL-jI}Bd}9+8$E#Vi@x{405S6i* zUl^Od(vPn?igs6YOR?iM$2H`E*2z& zVJyX4lCpW1S>R~A6J`7-e|@Ovu95@u(m+qrC9EE&x*o7jj7imFw8 zn(0_+#j8?AC=;0t%@|#oo16wyFJ6u=iM^aCAF%qliI3rc69JA9aeiTLa(p4z2vY}l z{7$A0>X2?}=D_p1-*C?94oi~#2gcY1E5kaUr{v=Gi zS_CaqK84_IVMMRys1GE8V8t{-RgT#BrLnoO@rCGIY(BbRnE_IiR^$0pPE5e~8BgS- ztf)wATq{u4`lDs4SbIHXeV>G}I(B6Y#x<_vI1#f_bp|M-szQoWIP5r@lu zHPp;(WzPgD4f=UWC(<>UzTez#fPUkb?xf!oG}=sgQkj)_ zDt0TLm2o<*pJkWda+LIpWnI@O&$%r2;*^!6szatAwUQa0)zk|KnM^5ga9Ex2oM5< z03q-)MBs?sd9GFGe@JHKTT(U#|101eJ#h^F8}>oH?85)C!9V&V1PB2_fDj-A2mwNX z5Fi8y0YZQfAOr}3moEZ`oaelzUI2Xl?+|{}CVT`w=#LN}1PB2_fDj-A2mwNX5Fi8y z0YZQfAOv0<1Wr2K&3$b{wkFs;qV6o#cBbTWtEV?sPO3?4@XX*~EH1|q^33e0-Q7H- zma5!>%u5WbNo-9@OIg^>fCcgSzeD(pP55j0pg%%@5Fi8y0YZQfAOr{jLVyq;1PB2_ zfDm{YA<*pXvzIv!94*cvZ>4JhpZ^DZKWh_yROkr(a_GU}M}n6Fp9v)W-}JxUzZY)M zA0a>p5CVh%AwUQa0)zk|@Zut{5!m0{*WTG`Z}YG}+px_xWD5o>ACN&X-p!1XQ;Km{ zC)4T7#ChMuHU!#|6DI~woEjJ$9T++-4xKzZa`NoZnaYQ0n|@%|RR_BbDHp#Zju{_t z?6k;Hjcj;F3^%-WAexfXH}4QpPaTM=5f|@_qv8gR5sQeb9PhXpU)2aqM=dgiXn-5B zti&T~D(mv*omDvR0^S4l@Xp0=XVRIC?K@MX24HKoYhsn}td^TjU}v>!BD*_o(@jSm z93gIU_KrDj*z154Io=th`20T*ddeoWguWU2Z0G}_9}7JtoEBDu_X=+keqZ?Fi)-qM z8zDdl5CVh%AwUQa0)zk|KnM^5guwSB0*4y<+RGnPYc9M%M?+ti$xU2i`wuttbsF5) z4*oEq&74_Y6WW^V$#bNkFJeGj=O}VV8u~^JvQ)Zt8jm*g4VdNBoG>mEahc1c!9-lm z!QnKyuel=}ZGf}4>~#RZnK=*eY(A&(4OFivKbtS`zQ9rcTm2orAM!c8Z}5EGbKU(( z_m##EHXd>PxGU7K)3C>W)%Gdpw&Qb-$LwFVUE-R#<7^Lu_p8GzG_^uRK|EYwI2gPf z4B8Ccw3RqzRvwoUxtN?BoS9vb;UufvKA!ZR7>tPdv?8yirKBRIAXmSf2FFLPpC6jj zpOsdntdvej%8u;-NHNf6*GbV_Lkg`8m`HNcQI-GV%w#PQTEZy8fY(+8qB&9>h!PRB z9{@!)B;dRVJcbSqoP#5a@Qj)oa%B&pSYaJ9oweFWlO&PJr*n-3T$+pRQiJDHZOF4# z$FrtRXgoLL(4sk4f$46Pq3s~D@3PyP+FhvUZA0x=tDKaSL{?^p2sO;irZVZZ9Go&_ zvpo-g{8N2!Q2G2o%UlDSoqeq+UAsNl)Mj@u>AEbds_MIS&dkDzBzU}xXnU=xKx&rW z;S-VR#i<%%W%h$q?VY+NtEDCi>o{xKOn$SW&|I313=KwVFx>IAAjcNO!NGCpu7QK1 zr1ERUy4ybL6l+%g#NeQX+VA-G)h!Gj;o>MX#N8C8p&5l~tHYA;wz#{7V`u+f5TmQp zZfmyMjc|a*|KYqGILD@8DLSupio4o__BNql?I4&{%_#_WQ`&ZdSZdd>J@5QZZ4DY3 zv7kXu?m?OA8GukGOJh&%&b#9l8236frhv%ZR+>;fN~29xUJRuP)}u5fg1vA`6R1aN zG?_4kNka7riv^|e_!%M=m8QU*wbfnL8o>vmw0G;rf+{06ilQWCWiu7uj$Gs>}7Yv7Ra2A`@w$&1dH4B^^nae9)7!P40>x2J$hlXm7g!?_nY`C7< zGiGqMoP3KN%@_j}^%Oy{YSwBE(Qp(o2-asGZfeKbK1}YbO{7?}Fu}6Ynv6q|lyncm z-vIk34i27W|7K?K$V)tDQ%h^f;cG-W^i!s8ORbU5rWy;VB<*jjsTo5sTc{-1(^QFK$hyQRq?M|q=JgD91O6=`vncfalHHSsxR1)ta0QJ$tcY_BpoaYxHS z=)d<}gO7nT@X_CnGj6-^C-7;zDEx`^xxq7yp>=)BZ+(-v0^z zfAoJh&=#f~>8kS>^Pa zlw3RtdD_hKfL~z~vMt;LnL5leF%i*j%-0qTyYm)+K35%3;2GmX=hZ%7ZRCVwfU2H2 zHSd+`M6s1P(b)(P?XFrM>2^ZOZsumHst~JrPR^yI>Dm<4cAu&Z5)Omd)R$|+j#(eB zU2X6Xt!J{69Z>BivuaOHTmsQxeP`TX3JV0bmb(Z%VZ~J=E6hSJ0D!OzGSA2`l#|g~ z`P6CV1mx>9VyrrR(HEA=#FOKoH=RaJtmfkhUznbUdUqQ2?$W%d;$u+nPNSYsnpI9d z)D2i0$;mv=_z}x0$~iOjD&~U2b0y_L3)H0417=ECq{cm^$@|SzrkFDe6Up@2eYFM5 zY(W&6v$a{@k`)<_%sn#zz!*8gi>!4{x6`!+&Be28Qf{=ipz=n1O*&=Pr;6;cokwZ| z4wK1~X2!EfOim7iWQILJd(6P5$a3-oj00UpmQKs389P4G4D~~HBLy$dp4;A(1_AUg zqd}uQGa!`C9Up|;MomcMcCS)$Ereb$?mZVE3>$h z6WwOkzPuWf6Gx$aBSr&*nzeLn=mOIlF*1GSxnX!XVu7++3|t(YAacZLFe#@jCq+Pf z)JSCJVk>#!nq6TN!H%;~$Vl-`3TY21K(xcdP}FE(s4QA|!qpDdfhRGQVF@JbdZ(mlM%<4QW-;LGL~co8G~lLwT$Z66{tlpuL|imnz zzM69lCKSU4`dG|Z<`@G71}9df0v8MD99Cu%fnsL#Dw!#4F&B6DKpPn@-irAOP8hR^ zN--<*jhJ(WCGiC_5ZUU94Fu}aAn1S8=5oH!fun(N$Iv~J8-ede#dm@YHV!7U3eX1H3M_q92~G2QxfaY`(b*U%5S8V z*iapry+860INjrM`+F|u;;?$MQj}#>tmkYTpNozyM8$=%^HWjLdO5Mz=kfyo4p>pFgrIrwqSyBHDI!tEm4!2cNg)Yh1rQ&aX^$eHdE|% zreZ?^t9kRP7}o^Q2M0=#nv_I(W7jUmQ1B_hd*M<&G>z#^fcEc#7;iQM`KHHi#CAv1 zSiZ@biRg`83201@$#!8b8m$L9rnbiP#ugW5W0Nyrho+-53mQ=q)ns(E4`X`Vpv4v@ zD|)_G&MXCbyyL^mbj)~*_*@bunj4#_I9|=7jW5p4fvAkd{KDAum43XvcmqDbZ+>yQo;EAg*g!tD zeqnBMd_nk@?bePvekW50bx5}~bKrU1Z#d_4hb77W z17qxhm0_LFQ{Z(i@WrV!9Mj2+mbak4Q3PV4#^bx8dx8X%Vzc`4ob;g%Q1) zqdt%Xf)&#URXJkgm&WGC#uuVI_guRfQC%aC#}DV4#;+$24oGnc2#o2~ryL^O8=a zYchSmx!nL$ZohOV{YKMLYjbWbw`-@#?hcQS+jk=6eP=C|S%HZ8@Op%d({cSQyZn}; zq-QMax<+}T--hx3oUTvU zgts(KxIW>!(D2cQkK%RuBLoNmLg3|sz*Bx0Gz8g#oiv}?z&o4px}uy=VkdM~+d}@( zQ@*>&-O_Xqo#0$rG~utscKTlVs@^{Cc83!Q`_E3751XdgX+ALvGfp+Dr({^Q(mraz zJ9SsF`4P-!$|s!qB|zE4$jY<}XK{DWais~#8ld340y!3^XX=_=giaCUA2 z{9Q23oSmPqrlCPuW zuZF4%BEGUA_HN1W(pEkvrof?>TkjV)vr+=yp~IaC2y zvKlA|Qwf9Bt3}!>cwh{;OUaxA+OOSqECeo}N-;aD>s!@37B`vbY*$))A+1K7h4%_Kvd^uyBM8` z&W+*V5{upd3oy?NfaSITV$yBs_X-+JrdIs4yBsuvIw3$D=I7>PlN0s#c%6xt`_U<*jmmnrN3Id(kT1Hs5cfsfh39bkI4L|AdROTF;1`2$2o40E3ET>F`v1uP z8h@+rSACbg&wAhNJ>>ZX&$Rnl_gmb@8$Z&x;QGAlmTOPLdm1h|zv_IObI9>o$7>zE z_K(@O?T2l@jU->_zgI?@Vf%!)Z@}9jfIA}?4hEZz0}ikkAIe_+z~NSXGdzZ`1hV%c z@Z#9{1$f8-gZxr2r{96}9re;@^w7go_%deu9gXx%50duuwRxL%A-bw+lM@9|AG-(W zb$J1eb`-U_Iu!U?P8k~PO&xgmM@vCgKtL1sS~n-(=54bet^s+5ft*y6g4oxNB3*=G zmvmJmnACEzK=*nV(Ct*=sHz$2qk=Lt@DV@0Pp+i}!jBz6!XALSs|FCpg!xsrT~YV7 zK>m6sC*M|6qm9X{aHynpfIKDwVdyH|-tHQ5vzM65;J|A!^-XU5_k!H>9Z23vI~tK! zrDl|MGCy$`h=NX!ct-?}y;}EE2N0!9Z1r-Czw8ua?_wEU8}n&;o^0nVBi=|2m};=( z?__Gs_^$$5>xVd(0q?+_aY3_%TShcK+V29VY#Zlv&VGlS;_5r!%u*sthbhk|4yruw zw00FdHI~|qLN!-gIm4+5r zt`;^hvYPGI(o!29+s`?4S{2@agC@8PFNDAwu&|#^*I$=fkbxB=U|t0id!F3~q(^b2 zucgQA_1Tgay~~UO%b>l24C0snE;RPz&Bzm~Q?u{Xx5_H#@l@6f&!_#_3DPt7Sq zJ$zVRoh~OsoJ*TEh0~-g150i95<&DmW;O_1U{r4Lw$zwlB8l?h4d<5lziwJN8@LmK z`070VuOm3`M}k)Rw2_4R`s=`b%m+j*O;(Ypff>>aoR*dvx@%rezV@Wjggjo>EQ3z$ zYaXO)6$=BgO^n-PZmV$MOW+1{H5jzC430*taF~$C%K(R(6guK{mj*|z4sSwNg+tpU zP$0b20ED4e)E=miaCxs_VzQ}T@mqm(%*jdjczbFXDO1wgoBE8p_M^byF$ZT5sX54+ zGGM_5Fq8-5z`5Fi8y0Ycz= zjKEN1U;B{VV5cFv6BNDMnLDPOL-m$$3=Sd>uXP^BBT%5rv;cE-aA}TN$jSX#1IK~p zfTfC}W2&LQzWmO<`tm#OJ%;j_)=>{U$`k2<>>Z}r86>#8>g|SnZKnCuNmUQP*41v7 z3uorq?EIcy$Z1Lsb1~L(vb$zRmG@D|*lEaH^%84t?5;k@Xs88hM%8hQ)jk67bQ+4O z=2e+@-V5+_8&#aSdiCyN(G*FRRVCOl0{Dy=5menqispOL`*jb4vUEkPl;X=@LG;a$mO7{&z z**2p_sVu9qb2Oj8GDcM~EwdMDKVS$|i9niQO{0+A&@z^0=YEEwvun>OEN3d2MxGIo zlZA4^02DK?kXB5sf9MEw*cLM;3ikbA^(^Qk3Yhj~lul83`Aw}N=F;nz&s1x*oNBGT zH8ZNbJ*K>IZLH_*HYKU$WJ_^5*;q59%G(7Q+YNgQTbDC(%tz4x{iMCes4}@MztirvN$}s@{M!ld@q~9GJI*`Vo~xenmy;n518jkU4Qp{YSH)sj zHIu^oVDrNftm%#~1iLTVx9&thzWG%znx$>T6~*@M_<}EhFqU=|R|wPemfnCnytQS2 zdxqb$uw%D^h?U*C6@~WHz2Uclgc@oFA_~Cb8*Ado$pp==vGS03GYTwwwDU_YjMJwpt{=cX5Sy%Or{wJb<-d2yVF89sr&D) zE=KViM6ioaUYx<0PWY2W-BS1D&A`3!=!DrWK(+%|+ca0Up^fhh<(q`rZoq;(up>~y z0Fem{u-AiOb6|(El--gQ$%m~3(vKHR8{2iQh;R@l9Dgc5k(b0x*wcy+F>!mY6i=*k z=#Hr+$JBUz9M42N4RQN!Nn%{oRli?^&4*C=eoA?2TNoya z5iG2X55?Spx(9}0**gse0Uh-2#eo1~+H$23VPW^?6sVP2(izx7%X_=F&(&g>2He*^ z$o9^vc8Bj`R{F&uoxK8c#p6rvatLHZD*@m%cQc-V)SM<7lzt}vmCIm1)mXEt^2WTj6^5sK_HddJJ;#PMS^L_U$VlcO^ zy)2?GhGvtu@{n2O_~Q<53|OEG0K&Ha^Y+4#?aJ8r zWpzlmD7Drq#?tRTj0n)v%?~g>w1@*XjEXr})iAsPef{*H(A{=9E9zl89NDA?8WZ=^ zW=?$(E_=CQ(3-YoYq82(}$WZMg=)KrmFbNicwtDVa`2 z#2TksC4m{%d(fL_nO*I*0 zbh_%MqkBIE9qd5rVgN{W^>wxyh@{(iQYn_Y;e>m$fY2pBlc}>VnM!x9s3KkP?E)qJ zW)+prq<0r6m2JmRNlkeesYqRwtK3wfQk!=(YNK^gGktYJB|6s#L5mP6Zw?y@PDZCb{YN^L`akK|1u1;B zzmn7i`z~t7b_tZCgzx`{g6+1@ROmqP%fVj@zAiWuY!5vh`q$9!3G2eB@Dsuxy~GV2 ziAM+!0)zk|KnM^5ga9Ex2oM5Q3((d~q@0el8t26jmoHlgt}6gFpV;8#$&x=isz7J3JQTA;xc zPb^@p8BWtO1}*`Wj{_s-Kx~;xr%U1PB2_fDj-A2mwNX5FiA; z4-hb2<(0oCp)RIq{ZH%vm)ZIs-~YGm`9A2YR0TqS5Fi8y0YZQfAOr{jLVyq;1PB2_ zfDkZ60H6QU`ri~DxmB1SX60UgwV8#Of76)d*ujZ)DRHyp7yt_ahbsyls5_Ey1qli3(%82lAHv_nXAq=8gvBU_+dDt=ibt0DP~4xT8s3sWjQ zeWJj;U(^Yu#}wnTYbQ{=bPrm3Ym49w?>ONIsCN!nHuff`Hd(RxCPka;HvJ}q?#c*I z)b?^4sMCbq9IsF$T6uZvM>;elvZ)~nhdMW?G3`EIcE@%KMEY00Id#RG} zZU8Fnq3SUg)v&5$TZGJlMJP6i&gN9IW#{Rr=Pe+L(_;(cmtwP5ipM*cG>o zwSbZCXYgnv{U20T(=T?Hwa=9a7>i?bixu(ot=ERx&>k4f$HykvaZ2-1t~=~x0r;*8 zg+J$_a0b`pI6HJ;W>y><$11Vmej+**eQ8L-t!lAdO|UKnR}VD%4rucM%V03M0}iDc zTgpM*eu7~U3=cFa>*)Fs1K0n1e9zg0|0-mJE{OmC+R#10KL}=ndjlT|p5CVh%AwUQa0)zk|@bX6B{GK2L$ZzgzZ}MVAOrzvi-p~T&Pfk26a2gPD zYj-p-^cVgglap6NK&z`6V!N5p(#3!?lt~T$rptCY$i?gIFd?a~94wHNlNSRjUc0F) zi!+BGP|<0>3R>qbq|)NgscCF-V%o>aMRp^XE;OCm46pQZa-#$G`kRzRm@5fc$F69R z77YaW&UZrY@herDxyDUk!hkGO&E0Q_q=k(-s(MZYs_^b&D3u^l%WRoZ3{?}9dL zln1GAxHuXE1G~_LQelyxCNWUH*uW{bo5@^F_mQ%qR_b9VC$y_Rjx=0b}t1Cnxaz|2@Gs+JYYsO$c8IzA@Ax{QFQU_;aD(34Tj>LKqMI zsPMtiUkNSP73hx;AOr{jLVyq;1PB2_fDj-A2mwOiP7&y6>}zYXl?Sm`7pRjHdyc~W zHnaQcYEbCM!v`ud>AXR@uc3Q@Vn;=0m7|(bP6n$oR^2&)9-!4}Myu$j@mD3L*1|Uc z#0JcWl`QySjqj~q4b3&;IR-&P!Kyx0rL56`!xGS=A4@P5M61%E`pt5MF znn^{<*$qflR!VI`%}3xk47r+Y|M${wBa)C1AOr{jLVyq;1PB2_fDj-A2mwNX5V%tW@cF-8c+>{}=#LN} z1PB2_fDj-A2mwNX5Fi8y0YZQfAOz}1VCRTEU<>c~!^2mHgPw3axgn>IZfzbtv3+#t z#L<%%hPKYnom@}`kDXW;l+F(+6VfBIm!lVsUCCZqzj6Ka#_f$`OT*JkE0;5gQ_9BT z(!}ItMuUAkl*%OHsr8JKI|e%})^oW{ZxVIwq%Y#Zz+f z#Ndfj1B0UjL#M@|lV?Xxo*g>F=mZ1rwC!>Jwk_D<6TJV;`>^LTo}2F5ZeQb%xqjYt zrs1Q`-wu34_=V7M=YGfA>^Fm7w0$7>nL40oyr+@n-f?!j!(#*Xoo+dulx{-+mC~&o zg*_PMcly+Tt&=ko(Hr7&@%GN~UU&G>QTxtP3B{7w8?iDja#AZ_l5NnKM@gPaQp@7( zOi`ec%*$f$a%uhK3(-K9waCte z*wI=vPO)dOryuiHNNBN0CYzM9wdt!{On6XAonZY2oFl$M1mnA|D7Q1|%*HmfUz~p{ zZMWi8XfiEFWT*S6JA4(?wN$yZ45~8dVyS5DrscR-Z8A03bA|R6gyMUMtO&<*E+c#e zIbY`%-Qo6j`_pl~N-3%|$~}gPqkwuA+8-3Nvf1@bH@3JiJ2?Y{rlT_pOm8r*=`xS> zUK%UbK-IXWDXIKgeI=M~u4Oa%O#|@?v2F)@3VM_2fWFmb-)YiY1GZBt#y%l}zlsX} z>{fv(ZI{yQC3=FluvcremLsXd?(hgO3|2B^!WV1}7CBmlpPJi{opvv>lWBK{ySnU8 zck3G(N$YCWW!T)R=-A>+`dzfRCPt;E=$eA*2IBx&8@6^a2MkGhV;w3s=%KO()uP|T z^bM+AJW>I$cBh!VfUtI}tU=q#8dS>`gEKCqEbKM}sI_aIRrsm74cTe;B0HN0d4u+7 z4XTmWaS~P3%4Urgx%o>fM<&l&s1hj zPRFmh1MHtTICz%*<0CMucs!AlZ^iLk z5Y$AgG<5`&Ts)Tt$jgr;+CdKuOuJo{Ao)%T(xR>>8RAHfM?K`1ZkJNp*>`n#pc(kQIIeQ^lEK)=o>R zDAo!qsi(tRTlRX-Nt^6x3_mt%hNg;7tYkS68#26n{B^gw+~I)%`x`nm`+$HIrmtEd zZH5}DuoorSRl8Ag4aQjm1*<-qF#?9=%I7DVVD%|~Qc@CGc@uk1^@41Mvl-7y>6}3& z%7sZ}V2xfqU!hN2J7TEd>Nth~D7|J1&U>M5waUgzb~O?de%9s@ayH@X!v7KeSopB; ze&G$mJKzHS5dwq&AwUQa0)zk|KnM^5ga9Ex2oM5;LbshNapN0)zk|KnM^5ga9Ex2oM5<03kpK5CS>`%GUp1wFzI<-KDRD03kpK5CVh% zAwUQa0)zk|KnM^5ga9G%@?ba5&l{Dd*N?|uRt8YzYzXR_@BZb3V#6c0Do8bAl#-uLVyq; z1PB2_fDj-A2mwNX5Fi8y0YZQfc+nAPaC+?meuVHNh#vv`fIxs=yAMCS_~F41H-0qY zhYLR%@WY884yV)W#r^+6=o>cSw(w@*BZ5cxlJKX(CE*{1-w@_Q-w=LMNC_u}BQJUd zi6J3C2oM5<03kpK5CVh%AwUQa0)zk|aAyeYcMjPr-vkg^ouhWccM3vH&K?YyUwoq= z*zD}XEBcof0zPM}oxd>PKj0kU9Lio7@cEr>cJ)O8uK+>%b#DoHTAau26>kT)!_Hy5 z!D|7Hdz{^N{R;suB&T~7pdsk&uoqqgaE2h}fAKW{hab)Z&|U%vBAR>d3{omh2oM5< z03kpK5CVh%AwUQa0)zk|KnM^5FKq)UUfgfHh=&BA8f)uybNnYA`=3H03kpK5CVh%AwUQa0)zk|KnM^5 zgn$_Wfd+4T+4}z$GbHna*I1CekEH zWb)};V*!`uV$;;%`BWS7Y}N4`w#sue4lSB<6_^_7Lk;dbZ3mHkm)+LX?s9=nwhgsg zWuKIkL{{EJ1#Os_O=Z$+Iq7!JW_up~_-8)1dU|8!q?*JA&kPR6;&Ln@Ynf|Fx33kY zYqtlR+UyP{U01C>qcw<@I%j5Y#j}a^cviH%)>I%hOYiWB$n@e=4Y4x&L8|snU6a*P z6NPo0wQMH8*-&UMO-F_XBQ+TA_*#%-3*zA5ICR&*K~YlqwPM|EA9ac~D}Q2e&_eBZ zeEaGaCLQ099EFCso5D0SqcClCSQ6eAch_+2?B5GwbamQo%~re7hLi(Pa`D)$cq%V7 zEJf$FPH|Uz(B38#tQ`cisyPM0Zc5uu5KHYEw&$JSsjWdHBNjC1$vr4jJp&NRWNGZF z-FbK10^?qX#uN~_+e#CvM``jfoEIc{ft4m$kJ6L~_QEMmpdO{sWWp3C3DqYo7L>;0 zXNXu-ngVxhue&@)*%5poN_)3{ET}SKqbN#JRyI@d?HHP2U(pPI+xZm>J-X)T^tcxV z>Vo00S)JIn)e?v`ivoQ*uXtfRgt@E_{_hVSB{_R*RcjaZsc1)L~TN8LKT8CMkD6z^_n za>!BMXx1P~WnfWSoaNnb`+7}$PFca{HFlJzsSew#OitX*Wg)Kr582W-Art!N(Br|s z3eE(6CD85vDZk&B@_yQT+Vg%-r~B>h&c-*nzTvvj@FxvRcDwWA&iftja~!t684A|* zx3lLA*0&q%X`9^+Mcam|;h&PzHx>Md-I5h~B_+9+3M#tqO3fAk-+_f*(=d!8S)FAU zMY_uqrsmTq#(>3+QWQhCQZ3pz%M;pKM$SKqQgtztqPABRi(8Z`7tgLqxuTV8P1bI$ zk(wqqcnYOqCRbZ3T9QWIh_C6a+g(E^FoN<}HEu<8(Ad5J#?5~c#bLu^UGTv0STt>S z3!K|0K$y~nP?feyWHy)&i%lEkq)>1q?8fio7(&(&oe!g$H&SD*rZMh$3RRWBf?3%= zh(dJR+lrRXLj5%%a@(6yQ2kM-$*a|+n~$SlY?fd_OUluKj+D*?3L@PtZP^JQL#YNV z4h$7iZEj{Wx1^+hs36(h8XqzP94>1sfO5fZ#r4YfnF4B$l^r8L@*cP+6BTytoox>VO}Kf zV#^Vf&8kbfT(%d?!MJ1Zl@rdhmkSPb9VQnoH5p(|J7`j+E2`R{J(G8gm2fikf+Rq03kpK z5CVh%AwUQa0)zk|KnM^5ga9G%vOu80-r)6mc>KTTY{GLd3!zC!LVyq;1PB2_fDj-A z2mwNX5Fi8y0YZQfs1Jb#SpR$Z`u{sN;XCyerrd-8AwUQa0)zk|KnM^5ga9Ex2oM5< z03q=5K)~trdhz{#+n$$)yd)(dKnM^5ga9Ex2oM5<03kpK5CVh%AwUT14gq}rPwW5P zVMnD10YZQfAOr{jLVyq;1PB2_fDj-A2!WRi0zu!KY(dBCZNiPv9|wOqxDt53_XoT# z&zl=Q)^OhWcBjYjdjC6pKVy5P{|D`VRezmQr`_R^F8fYUPA8??aP;3M1SDnQ9N9OB zU#Poweta%Ewh$F3XC|UI#AV%`n3j84oSiATvn=*5BTb;R9C=;GNq4xj)Bbud95tB2 zlLj;F1UIGl?Ln2t!r1w#D5Ioi0YW}eT;{)!8i=tFy%?PnXJ!|~nZ>Co5rQdBkIgNK zm!nJK*y6(Mq=s1_hoS*dSgKxpPib*`pnFZkIiGE(wT@}7+aiL5PN#` z1=*=b)l0JMc<5g$69)mUG1-=s9$!S9UznR5U)X6La))Q3LB_NOQNwI1v6vR=Qy*p* zfYUOZt!N$Xj&adKQ)$_-sf>6mM|SLk?(jHB6)~5JM`hQ^bH;)^yiiq1)GU!5_i=al zBFHdgE`t`!S|`P63sPvss>)Hw6xkU#<_<4`Bsa|^iL3F>@l;I~URx*9s0EQK%2$<5 z%e~w$7Ftka46WBN;0{MY-T`xYSr?t0&`Ej9f|P2hs!LzR~N9PL0Fi71C1%5U*6(s|E#82*Q5}`S$n<2NJN!VzzSCZ2MEGQZ zeL{5XVpqm1C(Ud~X`yoafp>8*(um>!bgw%+2Hd+1xoh2y%~sSOSolPB{>5Tu66kV7 zcAkv5!;y&n>7g?Fg`7(3J!EFZ)SBURgjH@A(-@sdwG9|!0?lHmP^^Qn9OhG-Li5{f zj!5{?QTxu4)&hl?+dG+2X4s$Z)__3^i-Z9c?dwF;$de=P@O{9nM+3f4o_}EnD;D{k zQ~AL`*jE?zb9JL)Mx1q2mm`^Bclb=ZeWyiZ&dx}R#W2y%`~u$W>I01P&hcKiiJ}xv z6OZu|#&uEBfkr`}3fTrLpsUPNNvcq%%FGBXyQ<^79GUEC3_mt%x3Ma!hoK|RkAa*sPaI%>5yAq-0%Q>$8>_f!^CJ4)dw_UeY4_3nzw);1Ze2n(EF zo4$U*)w^6245pzKoFfHOvg7OqV?LlW=B1fqTI*;?X}|l7^+CH0s-T_}U#bGdtI+yd zj>t~;QFr(%NKtCgv<#|V$;krIa%t{WrK#v><+Xv6b78$&pi@OSo)c&Bni$Iwdj4O( zSA$9r0)zk|KnM^5ga9Ex2oM5<03kpK5CShZ1lak1p5|+p=GJ9Jgnw~SeMRb+Zy_VDNjXu;#j`mnDQ?NRb#Wt;l(O-hoL=h}rKHTi#M4PJ zDWxR*r4?8DMUX14Na8J7k#j&_((0gyN+K&sX;DtI%gMO1zLJS&lgE68T8i;oap0>U zIZ0g2W;Vq8_Stk&1Uz&wc+OAF|hD$76D$8VGO}CLS6ZlhY6y5(8bVQQ-bCTuwllV>x(l9HA6yNp!h3<}j#{ieEfoxBz>We|w%>8oo?Q%`8H&KE zZp9RAsJ{lG8U8{D@^01?GVDcJf-l?0pcW!9laL zp@EH7otC6YXL6DPs}-1i%lQp;c8t?w^Z9WxYb~Mt59|^mDfI$OP9687G~E^ltzv`m zm0{9%H^!~U??V}IN!zS0Y1=F{6OaDyg(WSlXMOO0@6b@Kx$o_>TqShNXG^>?TWKll z8LDP^YcxEr|80BlgZ>BsLVyq;1PB2_fDj-A2mwNX5Fi8y0Yc#W6#_C=nq4` z5&Fr{8$z!PoeUic*@IsW{(kVi!8Zg`!MWg>pg-_;f&Ur!t-#L&ek70&ydrQe5b;0f z|GfVb{`dLc;m`VK{X_o!zUO_P_kGm&)4m__-S#c{9`p_P!ake#E8b6d-|v05_ch*C z?vH_$jY$4`tF_372i0-H7LM z@x(fAe}SzLYmyR^((#p)lpM^YBP%Xjv=vLQX0iz>mXzX&9PHZ3Ny*q|Di52Bl=#`f zvroP{lRg@E+0L{zvHdjsjj>oFla*o{@y8+I6d~8LNiI39%XK8?vW*zfN=mm1lm=eu zvR!U%X8gD)vD`Xr;)$uurH#0p(xh8m?Rtz=SKHgLnU!wInY@zPj>R`Ovzc2`5-BD3CuJp`N@cd7lJH8hl388teiQ`iY>Fp0Er z0$4)RDyWIF4%JdNQ*tiHcAmf%Ex3@AvKtD>c4z@ADm1YdH8L9? z=To39xF-X|--zdcN6Vbc)+02rI%U$a)hz4-fXn-@x@-~kGOH?XQ^V_55bGw~_w=ev z8furzf;^dY$E?eCytO%>2Hna*GnDDs>S}n#W$Of;l#^>x4&Nr#H1NQ*%hpx6fv<9E zcMeY>O|3`*)ORhDg*MUdw_bMHx?7u+oSe9cjQ|pAx55uYt(u!Oy@72*QZ}vtsz)YW zw$aum4U0l^WBt?8mZan|>4TSmZ$ruD-4(0F92DgDYu@L;>n^C zC8f>W`Uq%fly&E_+t6_0gv)jxuu5eVNmnes4T_ga4xI$$9B0Z|mIIpvGVn?R|AXhD ziGxk@28apWoi}tm1$zcF>DDo@HRbnED*=s%fPG6@F{VYiZP?TV4GglM1C(2u^vXOm zcmgB~KUjWai{Ci(0O$_b0Wdi6tR~IIwgTF^4aM$fO{4Y^-aCc|p$_5uDsF7C8~e{P zUCgcLH&)VM9%6}XW)r|Wc*bQLXa>-=>zvpSw$s7WM)$VZz5Sz1hx41M3^Z0g3(6YL ztwY`cjMC(XQCqX+- zFzwLVg#oOp^Y)2Mx_JnCfY7v(%B;oMCaDC_-#Z8wgH5WCT)^(*F55*xy{5$SpflT2 zJi7s1b{%?j3X~j0+kXtYTDYlVP-!24igXrqKbuKON`6I2WaSkJx_wJOt3FcAW#fsP zEAT%c+1dwosiSEmWR$vc3j=#X{+{?_4*2}Wenhf?Jo`Z1=3U)?7i~;DL+rVdC zl5)8%7_7GW=c;@QFze_A5C@ua(#B>Ax_4!Ua1S%>St$WU!Ro{7-Y|%PX+L}qj7UPE zF5I|lm?C`Lz+7m8okKCmtw?CoT8~1%7PNj1L)>i`4&eQEXoCJO$UfeLI~~Db!@K3{ zx!k65_W1GaX5tuL&49I(6UU&c$3Y{G08+swjh(6hZJnT|txasGmCEd2IxMn^z>ZQd zLJMjhyYMhWxA$Bnlj>f*wM0UQtxvjJfpnjy9-rzEJszIMhqD{WV_!_k9;eAlomO{Jp24mYWal<1hT{GxK@xG>-d8$3d|r)S67X zI|MZz(B}lpgY=G-WD5s@kkPVLvON)>J7@6%YDY`(~vl z@-ocmaCZxz1VK%kz0BlbUspEd6b!0xG3;TK@Zw556^GsA3-L7^^jqA_G60BK>`t*Q zNxBL7I~##WpMH9^oynJ0`XKB<1RVpCStXW;r(@hkNLj2<1A`RJUQ$9Uw2A!!t~PSIVPuJf03L73uJfM z+OD$s6aZab0#*ZyPr}FiHhf%Ng^w%O@%jI&T@Tn?|H*aT^?>WS;H!oAdVbfl?wR$p zdQSQ`{ND_|H*i(>xbT<3o4ybE{xWzWf@ND2f z;5pxa@x9CcUjJ|UKM^|Z{j~Q>fw=z*fhUB2C!_VCEGYwi|z${q0?Y&_EVuZ^E- z{Bq-OHvW9$8TSK?D~+%AKG*oJ#%Z6={aN?7+`sSnw)@8c0Qw^Y2mwN%UIbd5qjsD( zLZA|KNhC8km4b)w{ z=BxHQhwSPKHj#&=;zkT7!rRTxKJ9Wkek%qJg#7J&&H?Qjt`T7UmQQXSbe_>J@+mD0 zU)kfxZ_yS35AQQc za#(>qQIVgm8aFF0$MZ=!lhM@;xx=(wN$aj9;wdSegf++>XSc=<*UC!D<7}~G``<() z6cA|zCbuz;p7OXZI|Eq`IXmq*O#t9wBwxp$t67+bGsRhBC~V@4C>vwzmQ@tD(tRc| z(Mr8$$qwhZ9a_}PVU`e`QM?Z`TVr3@3O1Xuhb(S<3*BPN*Z>u!z!w(la$e>)%pG8_ zM47e8tAW?+F|22q!|N4{p@FaKQLJD!@75!Sonwdqa{@7MPl;n|33b!Jr**^l`quTJ zvjbXO^eg^!Eu!db=~CvsT0j=sx*0o!y1tl4g<#@pOYOP6&IrH4DuB+Qs|Zq= zr|L?J^SHgjOErt?R&-6x=(-p#cc-zC*6#~$s43K8z4PfZ3Z*p~b2&YXR#6jQ>*@^V zNhDdkqI#Gv6{@A(x`-6Y{6^qX0k_fxR=ZM%Q?$)_pS{dglvJHb6Q~7c_YkUr8)+P| zHt-;wXI-ttvxJV8G2~U6owXqP2p&Rc^d6&gWf!&{#OC7O6wO2Q09L-lEp&hRm96`* zSkd8wj-az>Uy7Hw59o}+#jVqbSee&n6j`WpaF5R^)~x6Y=9iU`fV17M`FT!ae%6;X z#T(YQF+)IGQJz3G!sU(T)xqC7ht6Q9^P>9O#F=v(^|I*4IfkNX>V#%+0Hwk8TESt{ zkA1J;tLejjrD>$zJ##M-E_!Antct1^rWau<^SJb&ozpm%I$Q2RQbj*Yw@$H(E|#O1 zspwtlLb)}VnET`i(+7M?QE;7fq8vKM1v>RbM1Vo6`bs)jeKi-!VWeB)9cf3UHt>ub zLNRs3xmTnORb9iYc=uqt^BglzhQ5$iY*sWgs%<)ezjZE;Ftbc(kP42D{d%1#adEVi z7(C{VV&08?`UcP>%1DwaUr0@x{I)W4l$)D zbxH&g?V<+<{1E}nrI{|(6X8cpE1eKN))|Wa2QL;yRo1y4JjkHL>)^&xy5~8KNE~Mc zrQQV>5<@l7ITjjF$MG6E6`VLbK~I9i=WO%hSOs-t>;FK?7K{WB1cku!fqx48EkpqP zT;Pub9}E0$;5P&RIq;tXKM{CG;4Oj}`Wmdyw}X!dI|EM!6ye*#Kft>JUlRU8_*3DJ zgpUdTUHG8zYr-!JKPUW@@NVHpg|`S#3r`7oAuX&4%fgZ{CtMaLga?FE!f~M&-Z9uK z_=SehcSHXa`f}*=p+5`#ap>cr-w*vacJMDP{C#o$cvV(=k&gW*Kb6L=b`PJe^|AwUQa0)zk|KnM^5 zga9G%0wB=nY_fxI-0f_(!}|$Osp*sa@~d!lg091D?aOWL%a-=#mi8sDeaUHG6zxk^ z`|^bLWmEf-(Y~a$FB{sI6c^&A_WN<|i>ykyu3cE;Iahfi@g&KU1W#6Y66eV>Phu*e zSMr~asXt%Ae?H2SM|iTtlN&s_&Xa3AS>(wAPv&_t$CIl(xuVjZ1Io?Pb1 z!#tVf$t9j#Ep@0Jc;n6R|Ter|GbAM-8?zUlP;bd;Yp`j zLgYU?cygF0?L0ZelQy0l}#`XdAg0YZQfAOr{jLVyq; z1PB2_fDj-A2!WR*0*7IPfc#wPrY@XYRr zQ7J-z5Fi8y0YZQfAOr{jLVyq;1PB2_fDrheAb@cmDjxVd_BoGxE3V@E|AOtjO?W)8 z~(SC;!l z{)6)<{N%irBdG`7;ZcwaB952H#R!xNG-D=^F0Y9s1#S;i;kF5ptJ68Hp3_yi@w_J7 z)SHo={r9`WV^FzA%PNP!XrN;}4vpuO8PDV59;Fv#UClZyd~ zQ7ANlE}IEd$Y~-85?PMC<;q!ixV_!}PH23H0IyNi(uY;*3uEV}q9yv{6Pci(dkfKv z(K&Huc0rt3oSG7^%uP;@%`J(Sqf6r0;==4CD#dhkW}zR8lqf<)*@@_dvBjwcv1hQS zn47^OE<|rEh~u+UQ>gGWv*Tm)n5aC9ZCxt^7p*K~JkFsz&XuD=Wq# zgkXuaRMi|0amf{yeTl$VT^@3TduSs1x?di0%kldHa8j?ou+JHjHP0mo zW$+*XsJVLWp$d2_G8f=g?_gtUgq9;uj=0130ka;hF$?AS7Z!`V$nTuWZ*gYw>Y{!o zXjIIIvySR=o(Z=OBz&R@znbXW#&g>H@G%$XO^gvg$Of&;xgxhuq;=81BZjE?5W}sGWkNE|d9* z1<+Hp3}-7kq;@B!Pl8kX$8%H4KjVDJe#!YR=i`pA z*?-CYq~ox?!x43S*m=PIFa9y#r~Qroy#Eva|LFg2pf7ME_~XHcgB?L7@Y%p`1b#U1 z?cmpfAA*Sg;n2G9h0q6ubHWGUJb|}|o(;WS02^oc*W<#@W@3Sh9qZ9f$k$?)4?D#p z_J6UxwaYArPFt5w0IAWc8Ov4c#xW>zylN4BYhS+?vct&`#=}a95r1VDWNkIeijNVc zFR({|CEC_by@8D}Fv6wWWIQXlmJ_6+FNCrwp|Rf#`IdKWvugk2m_^_$?!096J>}oe5RmciYuP#6z z>@w3Y%*bjiC!@9UG2U`=0`helF;<`AXb(rmL2o*Znpn-Jf97%?>fLFiH>G(|#mAuD zokl&OG^?C^s2i|0k`o=risn5&XJ(;_x!`afNqNu$H7WIgnGzPMsmB@LZ>BQEoZNGN zUu^-|^MJFpS*wp6&I|xBa423y-z&1#J=!>3TTuQ4Wwf@S?7Xg1W__y29@}}OHsJ7h z>EuzUi;;2{iOI=fkj$_LsK!fS2m8s%6EF^R8Cg0lpJwd%ylAK&vKuLQd3Ic63%*g^Ely1c2dE9^a<4j7yNhmqo&6w1YK!=c?9+aTKEVOQU%(ZEny zG_&$JEZw0@J5*;NY@ijaj7FTT#}5If14dK1lC0b^v_XFvFdEKE^6HH2L1=^lqXsJ} zpvlt;y=%az5lV9-%yRO;2xK-zR3lkZ5=w>5j5%iH?X{N8NC6!-mshR^V z{XS^(kC|16^MXaOSzkd$rP{yOY-UuF6;!{;Y+TSXVhUjF0Rel>1T65DlY*IyIKC9} zu^t*Sld&W#$QU$}QOl@~U4dEz^C$pR#&0%HKtc)}>bjYVZ?ji~d}bp*%gOli$-EcT zy?xlA7Zo$c@oYub;qm1)4=8ZE(fCs=z>kZ>_YB=;6Mi>J=ooY}F4j>EvY$y+(?V&o<#aR2`?&m%VBGs4fpX?<6P z9?v78?}R=X`nk}nLvx`>$R7Oj;IG5!c~ilrz+VM^J@5m8M+3+F-}3*F|L6Tr`4{{r z{Cj*~@qNhmAAA|#1>a%sKYE|>zSsMdcflL+Iy}#K-sgEEu%JIefDj-A2mwNX5coHN zKvf4WTZPEB7Y7LqU08?> z&g?OT*}e5;zzeZfF>}V@I;yacGsC)=>F%v8%}^^};F*D;gWKZtvX!Jf4ex2V<*PC;2k#2cvrQw^4U z{gD<|xI z0}Mt;4o`Cjl%HwL&JXWQ)qN3W0j4jh#@23 z%>Sc;?ybYB`N2kIM)Uj`)1?3B&(>Rq*zT}cq>!B<+}=AlEF8x17GpC5gZY09j`3mP z$Y@@>`SkyMIBpaT9D^2oM!Omgb4kE&`0X(?j8!pMQL*9pst_{8)C>OPH(ec7O+Rz^ znE^s7vuSfuFRh!u_#M1AqB=M?P|0R?AP4unu^>Ij-~8rZJ;3J4f`hh=Y0YcELskd( z|A!v_abfbGOkOzh!y~_P zS#8zI2L>JG&)%n7xc#GAa{S8}f{@!*nBAuwScE6La&FM}CdJl&iiS!XU z34Lc)^)UF@E3F4ADW&xv@pN0T$*1gl~=9u;`-9!I(|t0bm3&FWbJm>8;)Ja zH6UAob5tzE}LK7|W&jRh2D19R~fbgPgA z_IA(iHdMRaOBcL;58*sa^W7ev2i5jUUANW5|N4lE+-uscQ zFsCYBdTpnN?l(Pz_Oa}Gz2|tI)%4b_M!(PgJ9J9?1uJASM zvlr&NA#!7+-7XB5U3kSieeb@2aHHXLEM5ixT%Of(>|2gi(*#*=$Lcp6OAx|D)!Y^X z7RhfALbl~K@NnE}s#1QfEw|U6vFuLWvRj@jrF92SpI%)kublU+tJi$QS#GoE?!x(N z(amYe^JNQjL>}t9R>Ns^Ew5|0t-8}k_lSBu6#lVU>0a9CfGD@K*KU3T%%D4{^&E)P zy0z7F+t!{7mJn~VPu^J>L(Rs>F&?X7dyR8e+upM_9g7dAn{_gl8s$qp8!y!Co`qrD z!V@t5?m3Rt++xe*bBvIqYW8QXHvw#-f@We%F)_H&LW|87vtj^;o)a)96tLU2`~7BZ z7QoNxigUBp#uiRGEOn@Ew=7rs?wL8AQDX$<2*QsMG;94GkcE!)g%C3q$QMV+*TqjB}?V(3tzeVqV>|X>*s)Jt+j&^v({T?-Dw6g(x_&4tU8vK zki!K6?0a|s3I5%SwJUt9Db~wXw{mYM=d$bGY&!Q;MnN~d@tcQ6MC<&{>C+P@pmk2) zvDQf}_ct5hYObIT;IrFZvOA_}*1Fn%#o!HkgRx!txx*nS%|W6$dF?%;qA^H}2iggczU+F| z1{!NQ-V8)CezdJ^3UaKPZHuSqb?X=G+D+9K>w$u9^XbzIi{rFyq?|c%%0m1whMCsNFie&0MW-afza&iEJl34=ZPBVD{p_*dfs&# z%^Hp{#Wsm9l~o>JQ5U7K2iKKMI9Q_Yo@XH#NzYoCK|nOKPPp9aF4h6Guyqp= z*HLK8vUeRKNdE3@lir?8oNkxa$W8~1?=J@Ld=582j@5B?ts4kCaZQXznlBMOu?@Sx zwJyMHKvc(zrv@9Av^nj1yV;pLbx8yY*tD_f+P(UPIr|bDtApvWNM(J9tROhbh!JY| zn*u}X>#O0j))lUjt)0%veOUUsl9JcZgig;?Yq_3NXQCxfzLnT~is_&7(-4DwmaXAx z#>EBx;g_C0AgbV(o;)*gV%k>)?_dLRIHmoRpUeg%vEFVtE${%v1jCVru{Yw}r_cPyX-8zx}zwIv;F~K#o9;K#o9;K#o9;K#o9; zK#o9;K#o9;z|SxOW21)-jPgGtqoaoo=llPk;i=AtmLrfOkRy;IkRy;IkRy;IkRy;I zkRy;I@N~QdSV05ZDeM0?RKL8l{G=B*{379zawZe^&#|w!5_xqFI znB1IvaT3W4j{NAze?IcPBmeBkKRD7q^2(9ZM;@B^>xsXZ`0m7SPxL3=nb?>(`|#gC z{Fe{^yN6$Y_#Ym6+XY|$4#nBTZe?RiWk^eCA-I3oN`C#O=kqaY_k3UiTKgIu8{5Qql zF4l`*EIu~&cVqv4><`9%W9;VG&7YRZx(zz7e6CoiG^e`AN{Ya#$i$97VJN-syr;R@%W-k8A2Ox}ZduGn!*GZ@kHPE+|JyyPd$DE{T`h{Xx!#byV|mQ2)*ec5_)xa zWqH-@OubbsJUKbVxWd&YqK3}9ooBvWEPQElYOC34dJU&u^|}c3%FvfpuYp+RNbAcq zwq>{Txi^c2rO7E7P229^y-Tl(T($^WUPm)0uA}40sk*ae@3i_=j-bjA@pc~{r{5?R zo|&AIK%j1?ifGmy{`|=6#lkuLGs;ROkH23l7QQes#aR5`KsaV%2YTGjldlyEbH|S& z;A;m#m0PW97m082XM25q{lkylt*v9P77HhjPwh7A2=`k>no<3w^XMzZ!fF2lgPfZ$ zPrS@FO%=wi-9|DVt|R7a;6gBJ0cs|XP5^lT5P zp1N8rtQ_~T&;`6I$B$UDNK@c;9=n2mr%Yvh1DgE4M7UlG;4+o*^W!g~*~zIs!pz$B zs`*f@JKcU`$?Y6_0jMQDW8$pmi-q&(icwxe)$Bd5>bC0TMYr?R*i|1scM-gEVoHVLH`KKEkO88%hiVsOYV1H)tI6CZZv!z1g$ zi~YtYG&ra`up|VY$3osep_U26EEp{hkY1wX^Bfdd*D-%YJ%B zhR;?R>;C2@{sof9zW}-Psi(pOL4rypsivJ;SLw+H-2C+S;o^b|QK;#K{v+ z7Yl2FOp}xmRf8$vJA=+Ar?%Tz{99X|Lz#B`DXeQOB`p-%_6Jo?g@-AaIgR3%r@A6r z(S>J%{`R`HS^mmY4?5EWIN}tLnwT=(X$ClP5^Q>W3c=JdwwV0u9J6E~ZKN|%05K!6 z2htvW5|eUFO@0kh9i)Ew%@<2l2v3MYXkZ$u%oTwVJp3<4{{{-@+dD%koKxq$^@WA? zQ@&Pv7D2KhdlBW{xj4tK$R!hKsI#-5KldzutyHq=JOvbL4aa+@{GK#U1?RWU(Aiqm z4eoh?JtwQ)J`Lp+H3aI-w%vVq_JeloDd|5l3jO(_bji=jdY-CRSq_h0*DD2Fd{O^o z`k;n4rKEL`V4A`p{V_gk3Wb{Tsaaf&jG}b*9lP|6_rCD#IrXP^>(!F_+q-rA7z}b{ zDW!*tp=o;B?{#OBt2g+z@ZQt%FgEz5B9mItJYgz^NQrg62{U`0v6;TaHuz$mnza0`OrUrV!1N7a@bC!+G5zvcO3B zvvXWrDYQGip4+>)IA4*m8bXvLb$YXxXsdoX)m9zKLjCOHBB>(gsoR0E{5e<)DHbq< zE;FRntNgFFG+(sdpRwMy zw%41M4ORarl-n_}BK3Fu;n8Da3??d~$|I1jQ&XZ*eT%G&o*$o%C|3$=YOd;5%iadj zPka^#PWwtvN%+3q-|-|MeCOm*D3aj2Q4T64pepj{XJAVNy2gloG=6*vQXM8ej1p!X zqMhjW`el;!$fw1~2!BEnF1K@b5{=D;t4K>&Qg-YisbHA88qDvfWPW?j*LRvdr^;PURTL{m zdi1c^C^QbdcC!U596ugABrWmBO}k|y!Ni;PHg^fr2gQ_-^eWK5)m_KAiSlR0(MV-V z@++v8<&R%vqMlX*wvrn7V@06@&GtO#^iI_*2f{igu+mnlJ2YLoOh@H*PLINpSQ0f# zx?kA~@(frvREjaTQyM82reB|GAQOSn?^yL2DaNn^J5Vf~cwKg6Kz^7S%>Ore;ExKU zzcu>)=<}nGkB*J}w~_CR{N~8k$QMVZ5B&9k9~}6j(LcP6y=q?clR)4J2Ml*?3fpX^ z_NgWe=VNu@^W84jisJ<<=5(!CxI8(94@d-N*BY|fq*nDp+wXIm*uh~N+ohx=ZdnWkVQY=Y-T^T8Ls(h^5%J-^c&dh8|nOuVs{Kc0#32RbiZ`v{gqs7}HYe z7vW$yepLEXq*Re-1Iu1XaJ5FwetT={^m{T~BO|*#=T_6*@nDy$Y-diLLg8Jm(4+OH z2RDGb3pJoE-81iipp)U%5A%D=L1MX1d1Y;Ft+Kkfw1&}rRW`h`-STBrO(nmoy4`-Y z*+DO--Yyn41bMlzzbqwwSxo#=N&K>q_ywl4njxP;pJr}^A_#_u zoQ7cddj!GA2h6z2-XpFvAHy;%cm2DWv2ZnRS538G&cY?Qz)T5xSIodt8V-L>Z#Yr{ zb8Z@DCR3QMQD(t>t2LMPReXOUh2nJyW4!Iipb~Azzed}Rd<_7Ju6q2g(o%_h3xL9W zhnARHmOhH$aT%na_A2~EYlXPbK^-$V<@ELI$nG@a5B5J{>FMS~;{@cPHRD5$eSw_R zRj3sGmVhl5cRw~tjK1fzk;*LgJC{5%jY5ydKA0&`3NS7bQB99AN)#lR^FbiNKK|HS z=T8vl=}H41U(g@Zl?ArnA1d#}CnJK(KSdJP7hbvORLU+a2ht^6xI`{GA<_dt^jd}S z+sIt28~S7VT7{XwwF*&_X50iuL|n}%H4^Bz@b`#D4S$GPAO2dnJ!Ky#i->YdXP%&Y zVnsWZ*$i=9z|`KUCNYX!4i`zsP1)GKjTLm`t!IsLKLaLpOhPfb2u?Us2}X(Q{uzi^ z^=K__z#GdyZ2kH8YeVYek3Sa2T4Cty=Q`)H%J|KksB%`xl;2E(O)sS+t!AogXrmI- zq9U6$T%pMMDH#*F5t)f)%a+8rI;A>l=zWtUyGBA?Ccws4Nma*OVkg*i;I=^~R3unL z_ExluM4uM6xhktGdW13FVh&R25(&WR>q3ffWM{Q@nUj%K^cYYLa>9q1)lEX&PFV0;st9Y*ll)#QA&DroEk9;vd2`fpvge47)JQ0hqTo=kVjRZ z&%-S68L5_Og(HnQWkEj9;Q&;_Me=l{=Y3hKnAZHc>+d2BD$b}?N?Ki6TTFi6era3H zv!&xcQy^WTS{;yDAl`1W{*ZD>1M5k1<60x@f`^JQ=u3V zon!riJIr!29HBCyYA(~+$uqdj!-l!VY}oZC9nq4ST4l|?`aDs<3N)n1(tLjh*pXbO zJ4cpkSgUaEVg*Z<`ZDKnC?tkXBL9L(zs}WyQ(KkZTS*3;82z3mF&|pqZm0=D>S89C z#%dK2OYy1^4>HFa($6}x+q2fKX5DeY>?+ASCRjHF+1szpBBl5+Lq^tS=E+7*ZI#{{ zK76bKcWNC8Xw?IJ!l5NWlmrmrMtuiSg7CiryH;`ie)33@Sqkm~E>gGvMCX2wrXeczX@J31ed^Nn9gGe) zYrrG8m$7bLm{S!mX+g!q9DIi!?Be(dS&!?Gf6x|M&R)I7X_egBMm*}ppb!J!NpWRm z+*`!1xVxAfj1jQ)Xjd2;!|vPn6S9~=CaI^vtI9ePMGyO+4T9C00gEZ>g~@QohsOv` zYcEJ|4dV(!#6cDc*4*t5u3jnb>@tZv|GWq7MrU~mI$ZYz35`jXTPN_NQ$}Hy?K(w# z1EDAA+g;%v zKrKP!twifAPe8kUk^}K(cWBGvVohgTDAXO(bcW!*p)KWzuOH?fRd@^220CP>MBNK0 zp8Ms4>Qu5mUToiHX8rGpQNm8KY zYyoVwRG=_94#oCL2*DiXO)gxI0;;q`;Q7a@AqN=K#h+d!Jsn|o%MKJ%pes1zQp$&m z%L%#~vd{%H&025k{$(zWB$R2ydgO&rwe|+|sTXqHh4Lmd8Ee7sAB|DbxHw~=ra|X7 z9j7Y>YlyKh>KSa5w(XYS*?rY{jU%c-Q$qZw{51H;f9_d1er(n+R``dB!{09)9r=^OL;Z)U z54Rus*2te6{{6$-hvyFc-Jw4|^wFV>LlXyoaPS`=eD~n#@xL1X&L?rSo%fL=aC->+ zTJz?3Vfx!IP2rYx$6l|jf7rxj$<@Wm>hj|9JdU%)dsl&*jMfYk)Lxb5M-_WtT-8zA zOLgW?hnpOIv+&}4t-i7}zqx6zt**>(l~--pPYa7HtD6gptBdnC3!^V@9p^9}Btso9 zDd>;G@kAXir#=1|&OyAIz)_L9<}YPsk8um8@cHICD4d@^PtPAOG2&ofFM3?LXhD+5 zjd`VYvOJZ?lVO5rdVj>_9^ypQnLQl8)jjCZ}JPDz$ zyC)M&G~faoj^CE&7guF+u_iFNn%%fo7S6+8y|#yoPgke)qnsLCMQ&RzU1)T9v{7y2 z(NI$rVa4+KkEx}XKfpB~ziP+6RmJ6}ox$~1c5(mFugB*T(9t{f!4)sw!mTI2qIwa4 zyL+-N!xbfU$0nQJ^&MOWidUC2kim5q`?${N*Y)7F#)e{Gj|(1dJNmV_VH{j%trwmU z-dwV);(i)j_59ayy(-iltHRi9K|l|#yw*+jEAFl#s=CRlHoQ6R;Ht}=Fb@$`7|BD- z<#Ib*4|hkMPTlSe?yu6qTMHi|ISL$HsZcoZ8-?j#EoSONUxPfb`tWB(TpexjQO{BO z32-kGZvHYJ(0q2AKTdDKPX&Ps;ajG7`sUZ(A1{=C`S`Sl4dEu9)l`)P19zzl7PyB5 zVcqP=6+67aly`|C(*oah)e_DgwH-7MBLrepZQXJ3N@*DSrJ@9D=oj49g5(Xgg-UXh zu(=*-3-`QYZQ(8$&s=fu#P0R%J!2`bTt!LXU;A`(ym02X4jAz*RN=NE4YmdJB76yI zwVPEAH$lk-y_HmA%$h5eE}<*7nqFlIrj)6(jg4B@LX8Xc=B8KUlRE&Bwm<9kE&@_@d**q7t={!SitBFHGYjd zC;hTkHp5bHl#})0DSfiGfoo-U@6`GhJQB`~`Sp1WHvZcS>KecP`QyPDETv;mUWC1H zFEH59(s|(EjTj$H@^S{rOE9GF1<99zt9mg!kZAp;16NrGk*#htWEe%*X?+E5#%5pb zU9fUI+=%1Kt6(1!f8IDp#&>!pd0JcYYw?~%=xqaQFGCDmhiJ1@4Rx3z)zl%D)D=%< zj~>L5y!?d|1!h@uTh4lCW%&cxV5Hg_Rgwr?qS{ts2L(l(yoo1cW}Bo6E?^+Mem7?x zZOSo_9ttW`XrZ=hj8CQ5jcgf-fI$Y`U6H|)tPo5K7-$AeGJqsBi)qkM4X9}lQ0@;5 zd`L(b;Hod-*nl|M+daG6DDhCJB<7U?NOV#dphf(J2+_~aGY7x2ntQ3N4l;54z&Vcr zxd=(03gmv&WI()%wl*vr`or9A?X>BP3!UlN!jwrm#Y2qaH@Jk=(IDJ=NwNp=C14Qe zujVXFHg1@>rA@pCFxVxS3*=rwA&Z4#uq9me zY&a|Z_eg5WdQM3K4_LelKWL!nV-trJ4(?T93Bd`1R}(z0lhr3q7;P|g-Fgj|XpF&t z#Y4(MmIpR*jFO-b<8l?mwsu-AFkXEi=kY2S;hV8o1%pEsHE}U4D}m79OrRbzvtkH-1oSQywoE z{z|M(4qPD=G@&>_n$XgjFUw0ylyn?vDAR3X?>%)S#rLef5CP zh7{!@!>~Lw*BML_YmS}*VuGtEnmwa(JzX3H4QC7xgx&_h_i)|tWqhMquRER6;{2i+ zc9Iq7*o2WE9V})gIn91!`wtbog`-{v2V;-~o7g2_7N`|9G0@7}=Fl*cD!zrdBK=t` zZx&IEOzNjBGQ-_u(7VJLSDk1rr>Y<#9hcE=ZhAx`G-hcHcgPEl`Xp zo?_@B+_Wlf?q$+OlkA2nW^Ec@x^J7Nilunlw~rmT5Zvc_nN&eHz03kJyr_W^cu7kc zG^`MMglB);vye$NRr5`|+Z7hd{ zBx6~Ks}6T`@z@16Uc0p2Yu3%Ulfn`eu9~72i^dSBq)CvpE;0aF27(*Xuy3)*@VgDS z@8V{OjGg*`ht;sl0%FXC3~3l*=T_6%SEH2UzAwxyc@of~$s*q|d6pTgt zA&rrOHMQm72|k|qX}gXHVpE|u{Mopn(^q)aMEn9V%(&T4a81x?22!-MpI6sjfeUmb z?n;3hl;c4#3-(sHY@ozfa`5oAsO=G&!(E3SmKe1s=!GP$-ArzyqzP`LOlgXFEa^af zs|`Z-;-MY$npXDYdaH0<;gxW zkXQhT8?ZtZHRyxy|3}953S)nX|K$JV2;>Ol2z(L<)IJ<9e6w`&s5*VdE$WU(tEb8v zk$4_a#Zd>WA_R8eT{}iQgu&SBcqN8im|tF7+H{symYuRwTSE+o<;vpxlCxZM*0$8e zkJW|cd7QNT?vvjbFTDPTyHku0r#nEy5}`uWT(yQEG}42PmH-J14aFM{l%NpKE3@;n zI0BJBOF?=E87)&g7g2%tw|h7~?3is1ZZso;+$`EQ76lEipcs68?P!?=bU?>PPzEt3 zoL00@1%jFY+_wyK!enzb^4Bd*DH5^wTmwVT+q4)UPi--eG;V5vGV@K zJ0?&Iz)_!elyLe6S51jGY-8;ZH03^)Lwh(}?Nbn&E0{Ohrj64xW>IfmJSfDQH$ ze^IQeL0dnyi+vTgL```_5&I@gi7Dj5ihUBM#3XIGZXb78oj5^6l)R-4^;m7;N^~k` zXt8gGEA_rA`n4il36dDTE%r$uLZf20#=e;atd9Zpw8V(65Gis>_^>e6kidohY1hX- z30jyU?D!BjXD9wy5JOGez;DEA3j1L#sSTOL!;FZiitrtV7MwezFkOYn(GU-nQg-xS zNv^F%dzD=mS8z}~RzLv79EtfZih`HGh0W5!rPv+AP-lj31fRz3F=s_ztG zv?Tm%$V*D1B@wHp2_1SZO3sq-vT4F>d)4MHR#GMaGc5E3)11OPOM*Z4VNx_*S}h6q zra8WMjjf+0!5)iVH3Cba)OSnNOfk9)zw2^1+>8|v#EC-BQIAV;t3Osm!~{{mnF3#a z#+tw_IwgT+JDe$3mj!Pylqx1$y=X_v0(VftMKJ1E2@$YZooQrTt8ZZIGpu;*n}H3aQFiUbW`D)7BDpYa zajys~qGu@(I!KH4V{nvWNqMs(jEL!x@9E28#b8Of4O)1h8N*>!Ry{a`wrgytt0HCj z4UXH$H0k&MZ{o?2ZWWG5JWOIBGpk}T>YDLF!W73lq>#f#82Pr`j@d^YKvDH&tmVLR zGyXReQJn|gH>pzUdAGX8pR%(4Va%Tb?s|cvu->@-*0q=;CDr>DG@9W+S%&&!W9qF; z2T$0c0_a4Nn#N_TPSu2Bab2p9+CEZVCCqt>rxG_`Vb*}`!0L@$dJuaTS0pL-)H79a z+d;{s9=@g(SFS1ygU}g#!5%{6KqsI~4TOwHR{x%s+@Syw3O;j(*vw0+H~9sb7gIDg zH&p5gZ(p?WjQQb#Wz5y*b)8ystBK1cRENsn$X%vq=1>~W@b!h=q~=&Z!O1veQ@grz$A0FD=>!oX#%%5dMGC~O#*f-Ms-NCGv27$&Kg=~P=8V1&3RviN9{ zV(rnYyw6LJo;9_{z!nw0Hv>xIW(F^$Y_J6N5;pO1Y%uL9{KAr|;S7XUz?&OPReP+zw(Rx~}3hN=(X<8Io8E5SSVf3dC$cAR%7nk*}#7 z5k6^s&4rrHf&+_sLl$HT7~#V|b>@sm-|a2}n;-&$x{pL+@LHO2NM!qc?;zo+dPzlY z$%X{X^b*Rc$y0^ne3CK;bD_2~aG}wm58y@iYeJ-SwmpiOG*%IhgJCa7Pi&zSn6PFz z2}6+Hpy zO@}w?qCI4x1y)X=CG#RWMTA2D5@UpD_47&MMOU&OP3T5`_G$xHhcRQFLZR@M-1S!5 z>&Pvjgodhsp`Vj0zY6q_W)MKMe}F49#<#ro8CMK*YORz)t>_yhLPf^JFn`;6`MVvCVL2UTGhLe*!*Q z0@p$cnC_q$PN^H;X!HBvf^ZV_*zh(P+x8=rg!)uF6W(}qT3u7%Io|lOo>Qantc`0| zGjQ>D!)eMlMbU#ljfdQzlF$Lb@3MZT6a$0%n*`IC_A+4LyA#tKW1QGVJg8TkN3n<(97;R15w(KzGkA)1Ja{+sSv3P@u}h9g(8Jp#e>HqOq?7#sV&eT zY{k1IJ?!h&3Kvt$5;ShvUpGz`h$*<+k!4X*1Q2$0RI0|#fJkszL*?_k0UVDIqs~M6 z2QRDPGTz%3uN^{cn0hg^DH=1;j`o;^vn+%Zc=a5IVALU074OPp_b%cQ8PXVoh9OTC z*g)nE{ET6Q0Ut;U`xkxj7%t|^M?(jukjMiV%{4ygqHy*#{kilt_`$f$lYkiXi&#>b z&>Ld>E;G>Qiy_mYc`_ri5;rXMl$*G7fE{s1@pM}FM;KepMDScl#YHp~Gq`?#6wSGt zLXldg*0b4pn!{5F3N^pJ;}o#sT#vCRp&%I%K&W!vTy^obELBfdeR>$ zK?>F2`4O~mV0xS2ju|NVDksgCEeuA9>O*6er&-nsc*u{JqZG?Lo5_k_?M}UKnd~nE zAzT!?Q-r9++tl1$7e|mbqv0BnC7oq&$}*9{bdhpj@6dLH-I9qF4HjSY>%wv7fe1)0 zzLZWf08&?T`MOCdYDASb-fZ0=wY5I}7YRJv6RIo>xF;h9;`8^Qa*@JIKlx0s3ZFN~ zF@+~)Lww|^Vkg>k#-O>Mr4K^g+*YJ$J0-%Icf2GpZycm-k)fOGNEXep2ELYr6~=Ru zv}F;pl@*1iIi(HJWE>U%oXQYMVlvR7JWog!B@TJerU}rt{T(PdU+$*yh_A(>@>i~R z0a4A9r2+4K=*PUG7MHSw8BlT7US?Wu^I*y>B1NBcC=M&+$Ag6* zY*_Db(?BlU=jTB@5ZDs3BerXE8mZKpLC>Nx|RHPFX*mh?U; zrU?8N+`b4~B8*|7Pbwk@>(9FA)4a*q7RZ3EXNB!&NqiT9~Kxi!k|A(oE7q=y@c zlaN?wEd6qbcLlMOpb4o5(}`{}=A=9i0Ri(Y-VY7(aP$N*TJ>%RMrtH>0zwJ<=K75F z;>PRh+DV{A^nrT5_{gvr?G)j-P%78 z<%5tnJ2dgpJ#s$lGV59oBbtzMpT{CLBQ9f}ka}VmnfXw?lxOT^{+1yF zbWLMw?=$`KGm+uHrcY&h;i4v-M z2|Ll3(FzZJ(2Ch2h5QK+()g}KvqXTwEms64QZ{)^mhfO9-HKxyLs6O~XOy}LQ5sP_ zzLXJPO$PNk5KWl=I}kF%+YB&U%t<#x_kF@f11;(VlOXJmQ)*)~jF1LKbtHIV5ynlJ zCj?x06!-us%nfV|LEojZ7$a~;H<%H^)MU-O*nZ?5!=cG}51;SQ@2p9+BrJl+5gUoX zn$A}E=E~3V%wtWb%sg(J^d*d3Z2l7}gwFBA{7co0+2*Z@VP-o{LB^b~q5trHPKB(3 zfD^eB=7oqG-JC243q-u3VFtN_d0!|L3STw1mj~_?LaRP`DD(jB-Hmu)7!ahXp$6GE z>;i<;OR$gN|mM2=Ch^+guH6pme^|FYPvg~y1+@HIq*(Bs2JwbhJT|zHfXUo^+_yt zg-Fs+qf>BUU)L}B88>oRa>ha)rSuzWPzXs*4Y!|x$ZF7f^fm_XJ?Xyam5}}|B+2Z| zglz*l&owSNE+M>9=aMtjJBa|zVGNh}S^sa>(Y zcSl_0umKkU5+MF05u#hwr$5N2Tyi^zrl2h=vqwl^#@$7LSx_6nYmwDX*N!2W8oYK8 z1sRyG$oXf`uTX9JwHmM#k+rMQr~e-Bw_AwigzRLdFX2T*K0%bY&UThq^iM*vxe$xc zG^r0nD9AM@m5|r3T~eXhGGV03yY90dYp7jI;(`SvLqY`4Fed$WZ4Wrfhv%J5BG(EHq9Df;rBo+rHYE&YmW0D%QIiqXr|KEuUr27bwJjplfg5T z!AIvyjEMb#99H$*6+1z^M%l?1lg6k^h#(q_fk;kcropSt?`3}osA{w9kYV8o4Bgb# z7Fk_M-nlK|%-w!eIH#a91h1rZ`jZFhZun@CQyy7#Tsb9^iz>0~5=_O*m9b@~D|6tO zB;!EhW#Iu!Exqdp8$h``WB?QoXi}~xWRJE-#{4FqyEum3PTK2oOiush+fu=Np&EO7SUz&xH${m4L zmL~6tJNzCqpat>#lExhj8I;By`z`e3c+f~xmu8M>X;^+qBjEOjCjm-~s1~B|sLk~w zkR?PG%fB3s&_839wX2k!W1L2}rRFwCK{A8pWmuuXlM>DCrYp_tQ9f5*Yb-$tiSaPwU&{!`lyK;nwxax zLyb7aG!TItGs`=PM05+cKNS0#NQpjAVgFwMBc@z|`vmYvV+>#RWWxc*2&CQqXr!Gu z#u>x!@%}MJ#Dvz9lqRsUF#)GVB_@4l$U&Btb!dMxdY|qe6o7EF(L&TwBJi1(z`}sC%gJ^RfuD$%E5)?acaa0%2l0jEv56HJUsq>77{^Zym`YyK_jT> z!RAy#W`Ac<$rhAJWebR9fz+%1V9ilI$k~2MY2xgS$hjpKxt;-B>!#x>(>FzKmrU zt}FXt>=(mPM1sQfpa*bh6&7ouw7lPw%`UMCcS1Lwh9#z zi#06wiu}7Ua{mR8_S{|E5_0TXLyQf(*#Vd0Hg||J0k_ud50(1*CxEWR4>JNRv5l=@ zGTy+YJ7-thPx9tm+$Buu+~YNc+lE%U%z&1TEF^FaW=^v`rk1bW{hs+IPkJA;f4zIi zl%Er4)(AA@KQKtx2r!}e+Aju1Y7%GB8*D5CgJrCj4fS>~dKsL*nb$1BH^#{YhlC1x zq?T&O+eJ9|J8)pl^i>~t#*)%<&P)TZI_`u2W=tIhoVP2C@?-vp9Ez>q=Ib|slaSa zlAvJYa9Kh)iAx<-%tkH-N9yIBtnM;UQNbrAumPT<_vw7VHX!;G!qz+LO}loJ$ujX` zM?pj11)d$VhDv_G?FW#{K*_&A7D7qAXNvCt0h@7{2c8~1GK8=0xzVr=2?+Vr=DGy@ z))dL)`k^7@ZxR0h4|`(hruu!7G-TkaHs)7Jt9cu$HhvMZLXjHMY05y--+U*KOd!Ym zG-0YU*^c8?^d^tLvX`VdyEB%g%v2UWAd9h6=&6aEw3!JLA{e-UOeT@aaizi}*@F8T0Bh^91;8!q9 zfm1LXp>4;*b3%#2rU+$#HZH$VKpVXHfW*WqtTBSN$|;utL^hjXt|eBq0)|H|8W_SJ Lj | undefined export default { @@ -80,6 +82,27 @@ async function runBootProbe(phase: string): Promise { } } + if (phase === "seeded-wordpress") { + const runtime = await bootWordPressRuntime( + "install-from-existing-files-if-needed", + true, + true, + new Uint8Array(wordpressInstallSeed), + ) + try { + const wordpress = (await runtime.php.run({ + code: " get_option('siteurl'), 'wordpressVersion' => get_bloginfo('version')]);", + })).text.trim() + try { + return probeResponse(phase, JSON.parse(wordpress) as Record) + } catch { + throw new Error(`WordPress boot returned invalid JSON: ${wordpress}`) + } + } finally { + runtime.php.exit() + } + } + if (phase === "wordpress-files" || phase === "sqlite" || phase === "full" || phase === "streamed-sqlite" || phase === "streamed-wordpress") { const runtime = await bootWordPressRuntime( phase === "full" || phase === "streamed-wordpress" ? "install-from-existing-files" : "do-not-attempt-installing", @@ -101,10 +124,18 @@ async function bootWordPressRuntime( wordpressInstallMode: WordPressInstallMode = "install-from-existing-files", includeSqlite = true, streamWordPressFiles = false, + databaseSeed?: Uint8Array, ): Promise<{ php: PHP; wordpressVersion: string }> { const requestHandler = await bootWordPressAndRequestHandler({ createPhpRuntime, - hooks: streamWordPressFiles ? { beforeWordPressFiles: materializeWordPressServerFiles } : undefined, + dataSqlPath: DATABASE_PATH, + hooks: streamWordPressFiles || databaseSeed ? { + beforeWordPressFiles: streamWordPressFiles ? materializeWordPressServerFiles : undefined, + beforeDatabaseSetup: databaseSeed ? (php: PHP) => { + php.mkdir("/wordpress/wp-content/database") + php.writeFile(DATABASE_PATH, databaseSeed) + } : undefined, + } : undefined, maxPhpInstances: 1, phpVersion: "8.5", siteUrl: SITE_URL, @@ -141,7 +172,9 @@ async function materializeWordPressServerFiles(php: PHP): Promise<{ materialized } function isWordPressServerFile(path: string): boolean { - return /\.(?:php|json|crt|html)$/.test(path) || path.endsWith("/style.css") + return /\.(?:php|json|crt|html)$/.test(path) + || path.endsWith("/style.css") + || path.endsWith("/wp-admin/css/view-transitions.min.css") } function createPhpRuntime() { diff --git a/packages/runtime-cloudflare/wrangler.jsonc b/packages/runtime-cloudflare/wrangler.jsonc index 1af2c80e..f3ef8232 100644 --- a/packages/runtime-cloudflare/wrangler.jsonc +++ b/packages/runtime-cloudflare/wrangler.jsonc @@ -4,5 +4,8 @@ "compatibility_date": "2026-07-18", "compatibility_flags": ["nodejs_compat"], "limits": { "cpu_ms": 300000 }, - "rules": [{ "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false }] + "rules": [ + { "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false }, + { "type": "Data", "globs": ["**/*.sqlite"], "fallthrough": false } + ] } diff --git a/tests/cloudflare-runtime.test.ts b/tests/cloudflare-runtime.test.ts index 8177cf97..f88c6182 100644 --- a/tests/cloudflare-runtime.test.ts +++ b/tests/cloudflare-runtime.test.ts @@ -28,3 +28,11 @@ test("Cloudflare runtime declares the paid-plan WordPress boot CPU budget", asyn const config = JSON.parse(await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")) as { limits?: { cpu_ms?: number } } assert.equal(config.limits?.cpu_ms, 300_000) }) + +test("Cloudflare runtime packages the disposable WordPress install seed", async () => { + const config = JSON.parse(await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")) as { rules?: Array<{ type?: string; globs?: string[] }> } + const seed = await readFile(new URL("../packages/runtime-cloudflare/assets/wordpress-install-seed.sqlite", import.meta.url)) + + assert.equal(seed.subarray(0, 16).toString(), "SQLite format 3\0") + assert.ok(config.rules?.some((rule) => rule.type === "Data" && rule.globs?.includes("**/*.sqlite"))) +}) From 7b2c75bf01465e91f120b1602db321cc4ce6ab37 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 11:05:36 -0400 Subject: [PATCH 08/15] Disable WordPress bootstrap side effects --- packages/runtime-cloudflare/src/worker.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index d6501d7d..102777fd 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -128,6 +128,11 @@ async function bootWordPressRuntime( ): Promise<{ php: PHP; wordpressVersion: string }> { const requestHandler = await bootWordPressAndRequestHandler({ createPhpRuntime, + constants: { + AUTOMATIC_UPDATER_DISABLED: true, + DISABLE_WP_CRON: true, + WP_HTTP_BLOCK_EXTERNAL: true, + }, dataSqlPath: DATABASE_PATH, hooks: streamWordPressFiles || databaseSeed ? { beforeWordPressFiles: streamWordPressFiles ? materializeWordPressServerFiles : undefined, From 337d46e45136fe551127ac0058008384d3b99bc4 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 11:14:23 -0400 Subject: [PATCH 09/15] Skip installer checks for seeded WordPress --- packages/runtime-cloudflare/src/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index 102777fd..c0b3926b 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -84,7 +84,7 @@ async function runBootProbe(phase: string): Promise { if (phase === "seeded-wordpress") { const runtime = await bootWordPressRuntime( - "install-from-existing-files-if-needed", + "do-not-attempt-installing", true, true, new Uint8Array(wordpressInstallSeed), From b89c7a4f7ede2486ff67cb92af11b4b62b660b2d Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 11:30:32 -0400 Subject: [PATCH 10/15] Probe Cloudflare WordPress short bootstrap --- packages/runtime-cloudflare/src/worker.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index c0b3926b..2cbc2bfe 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -82,7 +82,7 @@ async function runBootProbe(phase: string): Promise { } } - if (phase === "seeded-wordpress") { + if (phase === "seeded-shortinit" || phase === "seeded-wordpress") { const runtime = await bootWordPressRuntime( "do-not-attempt-installing", true, @@ -91,7 +91,9 @@ async function runBootProbe(phase: string): Promise { ) try { const wordpress = (await runtime.php.run({ - code: " get_option('siteurl'), 'wordpressVersion' => get_bloginfo('version')]);", + code: phase === "seeded-shortinit" + ? " $wp_version, 'shortInit' => true]);" + : " get_option('siteurl'), 'wordpressVersion' => get_bloginfo('version')]);", })).text.trim() try { return probeResponse(phase, JSON.parse(wordpress) as Record) From f6c8c67d45dd7b62825794c13596e75fae8c9901 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 11:42:47 -0400 Subject: [PATCH 11/15] Boot Cloudflare runtime from streamed WordPress seed --- packages/runtime-cloudflare/src/worker.ts | 55 ++++++++++++++++++++--- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index 2cbc2bfe..d6448a5a 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -20,7 +20,12 @@ export default { const phase = new URL(request.url).searchParams.get("phase") if (phase) return runBootProbe(phase) - const runtime = await (bootPromise ??= bootWordPressRuntime()) + const runtime = await (bootPromise ??= bootWordPressRuntime( + "do-not-attempt-installing", + true, + true, + new Uint8Array(wordpressInstallSeed), + )) const phpVersion = (await runtime.php.run({ code: " { } } - if (phase === "seeded-shortinit" || phase === "seeded-wordpress") { + if (phase?.startsWith("seeded-")) { const runtime = await bootWordPressRuntime( "do-not-attempt-installing", true, @@ -91,14 +96,17 @@ async function runBootProbe(phase: string): Promise { ) try { const wordpress = (await runtime.php.run({ - code: phase === "seeded-shortinit" - ? " $wp_version, 'shortInit' => true]);" - : " get_option('siteurl'), 'wordpressVersion' => get_bloginfo('version')]);", + code: wordpressProbeCode(phase), })).text.trim() try { return probeResponse(phase, JSON.parse(wordpress) as Record) } catch { - throw new Error(`WordPress boot returned invalid JSON: ${wordpress}`) + return Response.json({ + schema: "wp-codebox/cloudflare-boot-probe/v1", + phase, + completed: false, + evidence: { rawPhpOutput: wordpress }, + }, { status: 500 }) } } finally { runtime.php.exit() @@ -122,6 +130,41 @@ async function runBootProbe(phase: string): Promise { return new Response(`Unknown boot probe phase: ${phase}`, { status: 400 }) } +function wordpressProbeCode(phase: string): string { + if (phase === "seeded-shortinit") { + return " $wp_version, 'shortInit' => true]);" + } + if (phase === "seeded-wordpress") { + return " get_option('siteurl'), 'wordpressVersion' => get_bloginfo('version')]);" + } + + const stops: Record = { + "seeded-includes": { needle: "/**\n * @since 3.3.0" }, + "seeded-muplugins": { needle: "if ( is_multisite() ) {\n\tms_cookie_constants();" }, + "seeded-plugins": { needle: "// Define constants which affect functionality if not already defined." }, + "seeded-theme": { needle: "// Create an instance of WP_Site_Health so that Cron events may fire." }, + "seeded-site-health-class": { needle: "WP_Site_Health::get_instance();" }, + "seeded-site-health": { needle: "// Set up current user." }, + "seeded-current-user": { needle: "/**\n * Fires after WordPress has finished loading but before any headers are sent." }, + "seeded-init": { needle: "// Check site status." }, + "seeded-wp-loaded": { needle: "do_action( 'wp_loaded' );", after: true }, + } + const stop = stops[phase] + if (!stop) throw new Error(`Unknown seeded WordPress probe phase: ${phase}`) + + return ` \\$wp_version, 'bootstrapPhase' => '${phase}']); return;\n"; +$replacement = ${stop.after ? "$needle . \"\\n\" . $stop" : "$stop . $needle"}; +file_put_contents($settings_path, str_replace($needle, $replacement, $settings)); +require '/wordpress/wp-load.php';` +} + async function bootWordPressRuntime( wordpressInstallMode: WordPressInstallMode = "install-from-existing-files", includeSqlite = true, From 6d2179e36a78fb6c782425fcc8c66482b5c4df27 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 11:56:59 -0400 Subject: [PATCH 12/15] Serialize Cloudflare WordPress state revisions --- .../src/php-wasm-universal.d.ts | 3 +- packages/runtime-cloudflare/src/worker.ts | 96 ++++++++++++++++++- packages/runtime-cloudflare/wrangler.jsonc | 11 +++ tests/cloudflare-runtime.test.ts | 10 +- 4 files changed, 117 insertions(+), 3 deletions(-) diff --git a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts index 235a181e..e70585d5 100644 --- a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts +++ b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts @@ -1,8 +1,9 @@ declare module "@php-wasm/universal" { export class PHP { constructor(runtimeId: number) - run(request: { code: string }): Promise<{ text: string }> + run(request: { code: string }): Promise<{ bytes: Uint8Array; text: string }> mkdir(path: string): void + readFileAsBuffer(path: string): Uint8Array writeFile(path: string, data: Uint8Array): void exit(code?: number): void } diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index d6448a5a..33cd4e81 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -13,11 +13,26 @@ const WORDPRESS_ARCHIVE_URL = "https://wordpress.org/latest.zip" const SQLITE_INTEGRATION_ARCHIVE_URL = "https://github.com/WordPress/sqlite-database-integration/releases/download/v2.2.23/plugin-sqlite-database-integration.zip" const SITE_URL = "https://wp-codebox-runtime.invalid" const DATABASE_PATH = "/wordpress/wp-content/database/.ht.sqlite" +const R2_DATABASE_POINTER_KEY = "sites/default/database/current.json" +const R2_DATABASE_REVISION_PREFIX = "sites/default/database/revisions" let bootPromise: Promise<{ php: PHP; wordpressVersion: string }> | undefined +interface Env { + WORDPRESS_STATE: DurableObjectNamespace + WORDPRESS_STATE_BUCKET: R2Bucket +} + export default { - async fetch(request: Request): Promise { + async fetch(request: Request, env: Env): Promise { const phase = new URL(request.url).searchParams.get("phase") + if (phase === "r2-state" || phase === "r2-mutate") { + const expectedMethod = phase === "r2-mutate" ? "POST" : "GET" + if (request.method !== expectedMethod) { + return new Response(`WordPress state ${phase === "r2-mutate" ? "mutation" : "read"} requires ${expectedMethod}.`, { status: 405 }) + } + const coordinator = env.WORDPRESS_STATE.getByName("default") + return coordinator.fetch(request) + } if (phase) return runBootProbe(phase) const runtime = await (bootPromise ??= bootWordPressRuntime( @@ -38,6 +53,85 @@ export default { }, } +interface DatabasePointer { + revision: string + databaseKey: string + persistedAt: string +} + +export class WordPressStateCoordinator implements DurableObject { + private tail: Promise = Promise.resolve() + + constructor( + private readonly state: DurableObjectState, + private readonly env: Env, + ) {} + + fetch(request: Request): Promise { + const response = this.tail.then(() => this.handleRequest(request)) + this.tail = response.then(() => undefined, () => undefined) + return response + } + + private async handleRequest(request: Request): Promise { + if (request.method === "GET") { + const pointer = await this.readPointer() + return Response.json({ + schema: "wp-codebox/cloudflare-wordpress-state/v1", + durableObjectId: this.state.id.toString(), + pointer, + }) + } + if (request.method !== "POST") return new Response("Method not allowed.", { status: 405 }) + + const pointer = await this.readPointer() + const persistedDatabase = pointer ? await this.env.WORDPRESS_STATE_BUCKET.get(pointer.databaseKey) : null + if (pointer && !persistedDatabase) { + throw new Error(`R2 database revision is missing: ${pointer.databaseKey}`) + } + + const database = persistedDatabase + ? new Uint8Array(await persistedDatabase.arrayBuffer()) + : new Uint8Array(wordpressInstallSeed) + const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, database) + try { + const mutationOutput = (await runtime.php.run({ + code: " $value, 'wordpressVersion' => get_bloginfo('version')]);", + })).text.trim() + const mutation = JSON.parse(mutationOutput) as { revisionValue: number; wordpressVersion: string } + const revision = crypto.randomUUID() + const databaseKey = `${R2_DATABASE_REVISION_PREFIX}/${revision}.sqlite` + const persistedAt = new Date().toISOString() + const databaseBytes = runtime.php.readFileAsBuffer(DATABASE_PATH) + await this.env.WORDPRESS_STATE_BUCKET.put(databaseKey, databaseBytes, { + customMetadata: { + persistedAt, + revisionValue: String(mutation.revisionValue), + wordpressVersion: mutation.wordpressVersion, + }, + }) + + const nextPointer: DatabasePointer = { revision, databaseKey, persistedAt } + await this.env.WORDPRESS_STATE_BUCKET.put(R2_DATABASE_POINTER_KEY, JSON.stringify(nextPointer), { + httpMetadata: { contentType: "application/json" }, + }) + return Response.json({ + schema: "wp-codebox/cloudflare-wordpress-mutation/v1", + source: pointer ? "r2-revision" : "packaged-seed", + ...mutation, + pointer: nextPointer, + }) + } finally { + runtime.php.exit() + } + } + + private async readPointer(): Promise { + const object = await this.env.WORDPRESS_STATE_BUCKET.get(R2_DATABASE_POINTER_KEY) + return object ? object.json() : null + } +} + async function runBootProbe(phase: string): Promise { if (phase === "wordpress-archive" || phase === "sqlite-archive") { const archive = phase === "wordpress-archive" diff --git a/packages/runtime-cloudflare/wrangler.jsonc b/packages/runtime-cloudflare/wrangler.jsonc index f3ef8232..30b52823 100644 --- a/packages/runtime-cloudflare/wrangler.jsonc +++ b/packages/runtime-cloudflare/wrangler.jsonc @@ -4,6 +4,17 @@ "compatibility_date": "2026-07-18", "compatibility_flags": ["nodejs_compat"], "limits": { "cpu_ms": 300000 }, + "r2_buckets": [ + { "binding": "WORDPRESS_STATE_BUCKET", "bucket_name": "wp-codebox-runtime-chubes" } + ], + "durable_objects": { + "bindings": [ + { "name": "WORDPRESS_STATE", "class_name": "WordPressStateCoordinator" } + ] + }, + "migrations": [ + { "tag": "v1", "new_sqlite_classes": ["WordPressStateCoordinator"] } + ], "rules": [ { "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false }, { "type": "Data", "globs": ["**/*.sqlite"], "fallthrough": false } diff --git a/tests/cloudflare-runtime.test.ts b/tests/cloudflare-runtime.test.ts index f88c6182..8cc89fcd 100644 --- a/tests/cloudflare-runtime.test.ts +++ b/tests/cloudflare-runtime.test.ts @@ -30,9 +30,17 @@ test("Cloudflare runtime declares the paid-plan WordPress boot CPU budget", asyn }) test("Cloudflare runtime packages the disposable WordPress install seed", async () => { - const config = JSON.parse(await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")) as { rules?: Array<{ type?: string; globs?: string[] }> } + const config = JSON.parse(await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")) as { + rules?: Array<{ type?: string; globs?: string[] }> + r2_buckets?: Array<{ binding?: string; bucket_name?: string }> + durable_objects?: { bindings?: Array<{ name?: string; class_name?: string }> } + migrations?: Array<{ new_sqlite_classes?: string[] }> + } const seed = await readFile(new URL("../packages/runtime-cloudflare/assets/wordpress-install-seed.sqlite", import.meta.url)) assert.equal(seed.subarray(0, 16).toString(), "SQLite format 3\0") assert.ok(config.rules?.some((rule) => rule.type === "Data" && rule.globs?.includes("**/*.sqlite"))) + assert.deepEqual(config.r2_buckets, [{ binding: "WORDPRESS_STATE_BUCKET", bucket_name: "wp-codebox-runtime-chubes" }]) + assert.deepEqual(config.durable_objects?.bindings, [{ name: "WORDPRESS_STATE", class_name: "WordPressStateCoordinator" }]) + assert.ok(config.migrations?.some((migration) => migration.new_sqlite_classes?.includes("WordPressStateCoordinator"))) }) From b8ea98657efd94657cc23afd9691cc7249bd5195 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 13:01:41 -0400 Subject: [PATCH 13/15] Probe MDI primary R2 persistence --- .../src/php-wasm-universal.d.ts | 2 + packages/runtime-cloudflare/src/worker.ts | 292 +++++++++++++++--- 2 files changed, 254 insertions(+), 40 deletions(-) diff --git a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts index e70585d5..b4670c57 100644 --- a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts +++ b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts @@ -2,6 +2,8 @@ declare module "@php-wasm/universal" { export class PHP { constructor(runtimeId: number) run(request: { code: string }): Promise<{ bytes: Uint8Array; text: string }> + isDir(path: string): boolean + listFiles(path: string): string[] mkdir(path: string): void readFileAsBuffer(path: string): Uint8Array writeFile(path: string, data: Uint8Array): void diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index 33cd4e81..e4be26b3 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -1,5 +1,5 @@ import { loadPHPRuntime, PHP } from "@php-wasm/universal" -import { decodeRemoteZip } from "@php-wasm/stream-compression" +import { decodeRemoteZip, decodeZip } from "@php-wasm/stream-compression" import { bootWordPressAndRequestHandler, type WordPressInstallMode } from "@wp-playground/wordpress" // The PHP-WASM package publishes this Emscripten loader without TypeScript declarations. // @ts-expect-error The adjacent Wasm declaration covers the compiled binary import. @@ -11,10 +11,15 @@ import wordpressInstallSeed from "../assets/wordpress-install-seed.sqlite" const PHP_VERSION = "8.5.8" const WORDPRESS_ARCHIVE_URL = "https://wordpress.org/latest.zip" const SQLITE_INTEGRATION_ARCHIVE_URL = "https://github.com/WordPress/sqlite-database-integration/releases/download/v2.2.23/plugin-sqlite-database-integration.zip" +const MARKDOWN_DATABASE_INTEGRATION_REVISION = "af451895a9429895e3ad4d9b4e073bfd88873745" +const MARKDOWN_DATABASE_INTEGRATION_ARCHIVE_URL = `https://codeload.github.com/Automattic/markdown-database-integration/zip/${MARKDOWN_DATABASE_INTEGRATION_REVISION}` const SITE_URL = "https://wp-codebox-runtime.invalid" const DATABASE_PATH = "/wordpress/wp-content/database/.ht.sqlite" -const R2_DATABASE_POINTER_KEY = "sites/default/database/current.json" -const R2_DATABASE_REVISION_PREFIX = "sites/default/database/revisions" +const MARKDOWN_ROOT = "/wordpress/wp-content/markdown" +const MARKDOWN_INDEX_PATH = "/tmp/markdown-index.sqlite" +const R2_MARKDOWN_POINTER_KEY = "sites/default/markdown/current.json" +const R2_MARKDOWN_REVISION_PREFIX = "sites/default/markdown/revisions" +const R2_MARKDOWN_OBJECT_PREFIX = "sites/default/markdown/objects" let bootPromise: Promise<{ php: PHP; wordpressVersion: string }> | undefined interface Env { @@ -53,12 +58,28 @@ export default { }, } -interface DatabasePointer { +interface MarkdownPointer { revision: string - databaseKey: string + manifestKey: string persistedAt: string } +interface MarkdownManifestFile { + path: string + objectKey: string + sha256: string + size: number +} + +interface MarkdownManifest extends MarkdownPointer { + files: MarkdownManifestFile[] +} + +interface RuntimeFile { + path: string + bytes: Uint8Array +} + export class WordPressStateCoordinator implements DurableObject { private tail: Promise = Promise.resolve() @@ -68,7 +89,13 @@ export class WordPressStateCoordinator implements DurableObject { ) {} fetch(request: Request): Promise { - const response = this.tail.then(() => this.handleRequest(request)) + const response = this.tail + .then(() => this.handleRequest(request)) + .catch((error: unknown) => Response.json({ + schema: "wp-codebox/cloudflare-wordpress-state-error/v1", + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }, { status: 500 })) this.tail = response.then(() => undefined, () => undefined) return response } @@ -85,40 +112,21 @@ export class WordPressStateCoordinator implements DurableObject { if (request.method !== "POST") return new Response("Method not allowed.", { status: 405 }) const pointer = await this.readPointer() - const persistedDatabase = pointer ? await this.env.WORDPRESS_STATE_BUCKET.get(pointer.databaseKey) : null - if (pointer && !persistedDatabase) { - throw new Error(`R2 database revision is missing: ${pointer.databaseKey}`) - } - - const database = persistedDatabase - ? new Uint8Array(await persistedDatabase.arrayBuffer()) - : new Uint8Array(wordpressInstallSeed) - const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, database) + const markdownFiles = pointer ? await this.readMarkdownRevision(pointer) : initialMarkdownFiles() + const runtime = await bootMarkdownWordPressRuntime(markdownFiles) try { const mutationOutput = (await runtime.php.run({ - code: " $value, 'wordpressVersion' => get_bloginfo('version')]);", + code: " 'Cloudflare R2 Proof ' . $value, 'post_name' => 'cloudflare-r2-proof-' . $value, 'post_content' => 'Persisted by MDI primary mode in R2 revision ' . $value . '.', 'post_status' => 'publish', 'post_type' => 'post'], true); if (is_wp_error($post_id)) { throw new Exception($post_id->get_error_message()); } update_option('wp_codebox_mdi_revision', $value); echo json_encode(['revisionValue' => $value, 'previousPostFound' => $previous_found, 'postId' => $post_id, 'wordpressVersion' => get_bloginfo('version')]);", })).text.trim() - const mutation = JSON.parse(mutationOutput) as { revisionValue: number; wordpressVersion: string } - const revision = crypto.randomUUID() - const databaseKey = `${R2_DATABASE_REVISION_PREFIX}/${revision}.sqlite` - const persistedAt = new Date().toISOString() - const databaseBytes = runtime.php.readFileAsBuffer(DATABASE_PATH) - await this.env.WORDPRESS_STATE_BUCKET.put(databaseKey, databaseBytes, { - customMetadata: { - persistedAt, - revisionValue: String(mutation.revisionValue), - wordpressVersion: mutation.wordpressVersion, - }, - }) - - const nextPointer: DatabasePointer = { revision, databaseKey, persistedAt } - await this.env.WORDPRESS_STATE_BUCKET.put(R2_DATABASE_POINTER_KEY, JSON.stringify(nextPointer), { - httpMetadata: { contentType: "application/json" }, - }) + const mutation = JSON.parse(mutationOutput) as { revisionValue: number; previousPostFound: boolean; postId: number; wordpressVersion: string } + const canonicalFiles = collectRuntimeFiles(runtime.php, MARKDOWN_ROOT) + const nextPointer = await this.persistMarkdownRevision(canonicalFiles) return Response.json({ schema: "wp-codebox/cloudflare-wordpress-mutation/v1", - source: pointer ? "r2-revision" : "packaged-seed", + source: pointer ? "r2-markdown-revision" : "packaged-markdown-seed", ...mutation, + canonicalFiles: canonicalFiles.length, + sqlitePersisted: false, pointer: nextPointer, }) } finally { @@ -126,9 +134,47 @@ export class WordPressStateCoordinator implements DurableObject { } } - private async readPointer(): Promise { - const object = await this.env.WORDPRESS_STATE_BUCKET.get(R2_DATABASE_POINTER_KEY) - return object ? object.json() : null + private async readPointer(): Promise { + const object = await this.env.WORDPRESS_STATE_BUCKET.get(R2_MARKDOWN_POINTER_KEY) + return object ? object.json() : null + } + + private async readMarkdownRevision(pointer: MarkdownPointer): Promise { + const manifestObject = await this.env.WORDPRESS_STATE_BUCKET.get(pointer.manifestKey) + if (!manifestObject) throw new Error(`R2 Markdown manifest is missing: ${pointer.manifestKey}`) + const manifest = await manifestObject.json() + const files: RuntimeFile[] = [] + for (const file of manifest.files) { + const object = await this.env.WORDPRESS_STATE_BUCKET.get(file.objectKey) + if (!object) throw new Error(`R2 Markdown object is missing: ${file.objectKey}`) + files.push({ path: file.path, bytes: new Uint8Array(await object.arrayBuffer()) }) + } + return files + } + + private async persistMarkdownRevision(files: RuntimeFile[]): Promise { + const manifestFiles: MarkdownManifestFile[] = [] + for (const file of files) { + const sha256 = await sha256Hex(file.bytes) + const objectKey = `${R2_MARKDOWN_OBJECT_PREFIX}/${sha256}` + if (!await this.env.WORDPRESS_STATE_BUCKET.head(objectKey)) { + await this.env.WORDPRESS_STATE_BUCKET.put(objectKey, file.bytes) + } + manifestFiles.push({ path: file.path, objectKey, sha256, size: file.bytes.byteLength }) + } + + const revision = crypto.randomUUID() + const manifestKey = `${R2_MARKDOWN_REVISION_PREFIX}/${revision}.json` + const persistedAt = new Date().toISOString() + const pointer: MarkdownPointer = { revision, manifestKey, persistedAt } + const manifest: MarkdownManifest = { ...pointer, files: manifestFiles } + await this.env.WORDPRESS_STATE_BUCKET.put(manifestKey, JSON.stringify(manifest), { + httpMetadata: { contentType: "application/json" }, + }) + await this.env.WORDPRESS_STATE_BUCKET.put(R2_MARKDOWN_POINTER_KEY, JSON.stringify(pointer), { + httpMetadata: { contentType: "application/json" }, + }) + return pointer } } @@ -181,6 +227,72 @@ async function runBootProbe(phase: string): Promise { } } + if (phase === "mdi-files") { + const php = new PHP(await createPhpRuntime()) + try { + const wordpress = await materializeWordPressServerFiles(php) + await materializeMarkdownDatabaseIntegration(php) + materializeRuntimeFiles(php, MARKDOWN_ROOT, initialMarkdownFiles()) + const evidence = (await php.run({ + code: " file_exists('/wordpress/wp-content/db.php'), 'plugin' => file_exists('/wordpress/wp-content/plugins/markdown-database-integration/markdown-database-integration.php'), 'siteurl' => file_exists('/wordpress/wp-content/markdown/_options/siteurl.json')]);", + })).text.trim() + return probeResponse(phase, { ...wordpress, ...JSON.parse(evidence) as Record }) + } finally { + php.exit() + } + } + + if (phase === "mdi-shortinit") { + const runtime = await bootMarkdownWordPressRuntime(initialMarkdownFiles()) + try { + const evidence = (await runtime.php.run({ + code: " $wp_version, 'markdownDropin' => defined('MARKDOWN_DB_DROPIN'), 'markdownMode' => defined('MARKDOWN_DB_MODE') ? MARKDOWN_DB_MODE : '']);", + })).text.trim() + return probeResponse(phase, JSON.parse(evidence) as Record) + } finally { + runtime.php.exit() + } + } + + if (phase === "mdi-index-artifact") { + const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles()) + try { + await runtime.php.run({ code: " $updated];" + : phase === "mdi-insert" + ? "$post_id = wp_insert_post(['post_title' => 'MDI Probe', 'post_name' => 'mdi-probe', 'post_content' => 'MDI probe body.', 'post_status' => 'publish', 'post_type' => 'post'], true); if (is_wp_error($post_id)) { throw new Exception($post_id->get_error_message()); } $result = ['postId' => $post_id];" + : "$result = [];" + const evidence = (await runtime.php.run({ + code: ` get_bloginfo('version')], $result));`, + })).text.trim() + return probeResponse(phase, JSON.parse(evidence) as Record) + } finally { + runtime.php.exit() + } + } + + if (["mdi-includes", "mdi-embed", "mdi-textdomain", "mdi-ai-client", "mdi-plugin-constants", "mdi-muplugins", "mdi-plugins", "mdi-theme", "mdi-site-health-class", "mdi-site-health", "mdi-current-user", "mdi-init", "mdi-wp-loaded"].includes(phase)) { + const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles()) + try { + const evidence = (await runtime.php.run({ code: wordpressProbeCode(phase) })).text.trim() + return probeResponse(phase, JSON.parse(evidence) as Record) + } finally { + runtime.php.exit() + } + } + if (phase?.startsWith("seeded-")) { const runtime = await bootWordPressRuntime( "do-not-attempt-installing", @@ -234,6 +346,10 @@ function wordpressProbeCode(phase: string): string { const stops: Record = { "seeded-includes": { needle: "/**\n * @since 3.3.0" }, + "seeded-embed": { needle: "/**\n * WordPress Textdomain Registry object." }, + "seeded-textdomain": { needle: "// WordPress AI Client initialization." }, + "seeded-ai-client": { needle: "// Load multisite-specific files." }, + "seeded-plugin-constants": { needle: "// Load must-use plugins." }, "seeded-muplugins": { needle: "if ( is_multisite() ) {\n\tms_cookie_constants();" }, "seeded-plugins": { needle: "// Define constants which affect functionality if not already defined." }, "seeded-theme": { needle: "// Create an instance of WP_Site_Health so that Cron events may fire." }, @@ -243,7 +359,7 @@ function wordpressProbeCode(phase: string): string { "seeded-init": { needle: "// Check site status." }, "seeded-wp-loaded": { needle: "do_action( 'wp_loaded' );", after: true }, } - const stop = stops[phase] + const stop = stops[phase.replace(/^mdi-/, "seeded-")] if (!stop) throw new Error(`Unknown seeded WordPress probe phase: ${phase}`) return ` { const requestHandler = await bootWordPressAndRequestHandler({ createPhpRuntime, constants: { AUTOMATIC_UPDATER_DISABLED: true, DISABLE_WP_CRON: true, + ...(markdownFiles ? { + MARKDOWN_DB_CONTENT_DIR: MARKDOWN_ROOT, + MARKDOWN_DB_INDEX_PATH: MARKDOWN_INDEX_PATH, + MARKDOWN_DB_MODE: "primary", + MARKDOWN_DB_STATE_DIR: MARKDOWN_ROOT, + } : {}), WP_HTTP_BLOCK_EXTERNAL: true, }, dataSqlPath: DATABASE_PATH, - hooks: streamWordPressFiles || databaseSeed ? { - beforeWordPressFiles: streamWordPressFiles ? materializeWordPressServerFiles : undefined, + hooks: streamWordPressFiles || databaseSeed || markdownFiles ? { + beforeWordPressFiles: streamWordPressFiles || markdownFiles ? async (php: PHP) => { + if (streamWordPressFiles) await materializeWordPressServerFiles(php) + if (markdownFiles) { + await materializeMarkdownDatabaseIntegration(php) + materializeRuntimeFiles(php, MARKDOWN_ROOT, markdownFiles) + if (markdownIndexSeed) php.writeFile(MARKDOWN_INDEX_PATH, markdownIndexSeed) + } + } : undefined, beforeDatabaseSetup: databaseSeed ? (php: PHP) => { php.mkdir("/wordpress/wp-content/database") php.writeFile(DATABASE_PATH, databaseSeed) @@ -293,6 +424,87 @@ async function bootWordPressRuntime( return { php, wordpressVersion } } +async function bootMarkdownWordPressRuntime(markdownFiles: RuntimeFile[]): Promise<{ php: PHP; wordpressVersion: string }> { + const indexBuilder = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, markdownFiles) + let markdownIndex: Uint8Array + try { + await indexBuilder.php.run({ + code: " ({ + path: `_options/${option.option_name}.json`, + bytes: new TextEncoder().encode(JSON.stringify(option, null, 2)), + })) +} + +function materializeRuntimeFiles(php: PHP, root: string, files: RuntimeFile[]): void { + for (const file of files) { + const destination = `${root}/${file.path}` + php.mkdir(destination.slice(0, destination.lastIndexOf("/"))) + php.writeFile(destination, file.bytes) + } +} + +function collectRuntimeFiles(php: PHP, root: string): RuntimeFile[] { + const files: RuntimeFile[] = [] + const visit = (directory: string): void => { + for (const name of php.listFiles(directory)) { + if (name === "." || name === "..") continue + const path = `${directory}/${name}` + if (php.isDir(path)) { + visit(path) + } else if (!name.includes(".tmp.") && !name.startsWith("markdown-index.sqlite")) { + files.push({ path: path.slice(root.length + 1), bytes: php.readFileAsBuffer(path) }) + } + } + } + visit(root) + return files.sort((left, right) => left.path.localeCompare(right.path)) +} + +async function sha256Hex(bytes: Uint8Array): Promise { + const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer) + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("") +} + +async function materializeMarkdownDatabaseIntegration(php: PHP): Promise { + const response = await fetch(MARKDOWN_DATABASE_INTEGRATION_ARCHIVE_URL) + if (!response.ok || !response.body) throw new Error(`Unable to fetch Markdown Database Integration: ${response.status}.`) + const stream = decodeZip(response.body) + const reader = stream.getReader() + while (true) { + const { done, value: entry } = await reader.read() + if (done) break + const relative = archiveRelativePath(entry.name) + if (relative !== "db.php" + && relative !== "markdown-database-integration.php" + && !(relative.startsWith("inc/") && relative.endsWith(".php"))) continue + const bytes = new Uint8Array(await entry.arrayBuffer()) + const destination = `/wordpress/wp-content/plugins/markdown-database-integration/${relative}` + php.mkdir(destination.slice(0, destination.lastIndexOf("/"))) + php.writeFile(destination, bytes) + if (relative === "db.php") php.writeFile("/wordpress/wp-content/db.php", bytes) + } +} + +function archiveRelativePath(path: string): string { + const separator = path.indexOf("/") + return separator === -1 ? "" : path.slice(separator + 1) +} + async function materializeWordPressServerFiles(php: PHP): Promise<{ materializedFiles: number; materializedBytes: number }> { const decoder = new TextDecoder() let materializedFiles = 0 From 97ba1f75d5f573485a11673bb12cbf03198fe427 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 15:29:43 -0400 Subject: [PATCH 14/15] Persist canonical MDI revisions in R2 --- package.json | 1 + .../markdown-database-integration-runtime.zip | Bin 0 -> 65801 bytes .../markdown-primary-bootstrap-index.sqlite | Bin 0 -> 344064 bytes packages/runtime-cloudflare/src/worker.ts | 258 +++++++++++------- packages/runtime-cloudflare/src/zip.d.ts | 4 + packages/runtime-cloudflare/wrangler.jsonc | 2 +- .../build-cloudflare-mdi-runtime-bundle.mjs | 31 +++ tests/cloudflare-runtime.test.ts | 5 + 8 files changed, 202 insertions(+), 99 deletions(-) create mode 100644 packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip create mode 100644 packages/runtime-cloudflare/assets/markdown-primary-bootstrap-index.sqlite create mode 100644 packages/runtime-cloudflare/src/zip.d.ts create mode 100644 scripts/build-cloudflare-mdi-runtime-bundle.mjs diff --git a/package.json b/package.json index 3a229d52..af303451 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "wp-codebox:source": "node bin/wp-codebox-source.mjs", "generate:browser-fanout-aggregation-runtime": "tsx scripts/generate-browser-fanout-aggregation-runtime.ts", "generate:fanout-aggregation-contract": "tsx scripts/generate-fanout-aggregation-contract-fixture.ts", + "generate:cloudflare-mdi-runtime-bundle": "node scripts/build-cloudflare-mdi-runtime-bundle.mjs", "smoke": "tsx scripts/run-smoke.ts", "test:redaction": "tsx tests/redaction.test.ts", "test:cloudflare-runtime": "tsx tests/cloudflare-runtime.test.ts && node ./node_modules/typescript/bin/tsc -p packages/runtime-cloudflare --noEmit", diff --git a/packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip b/packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip new file mode 100644 index 0000000000000000000000000000000000000000..4a4af8fdb41c510777ce5a70496d17acb26e2aaa GIT binary patch literal 65801 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�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 literal 0 HcmV?d00001 diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index e4be26b3..b670e4ba 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -6,20 +6,23 @@ import { bootWordPressAndRequestHandler, type WordPressInstallMode } from "@wp-p import { dependenciesTotalSize, init } from "../../../node_modules/@php-wasm/web-8-5/asyncify/php_8_5.js" import phpWasmModule from "../../../node_modules/@php-wasm/web-8-5/asyncify/8_5_8/php_8_5.wasm" import { CLOUDFLARE_RUNTIME_HEALTH_MARKER, CLOUDFLARE_RUNTIME_HEALTH_SCHEMA, cloudflareRuntimeHealthResponse } from "./health-envelope.js" +import markdownDatabaseIntegrationRuntime from "../assets/markdown-database-integration-runtime.zip" +import markdownPrimaryBootstrapIndex from "../assets/markdown-primary-bootstrap-index.sqlite" import wordpressInstallSeed from "../assets/wordpress-install-seed.sqlite" const PHP_VERSION = "8.5.8" const WORDPRESS_ARCHIVE_URL = "https://wordpress.org/latest.zip" const SQLITE_INTEGRATION_ARCHIVE_URL = "https://github.com/WordPress/sqlite-database-integration/releases/download/v2.2.23/plugin-sqlite-database-integration.zip" const MARKDOWN_DATABASE_INTEGRATION_REVISION = "af451895a9429895e3ad4d9b4e073bfd88873745" -const MARKDOWN_DATABASE_INTEGRATION_ARCHIVE_URL = `https://codeload.github.com/Automattic/markdown-database-integration/zip/${MARKDOWN_DATABASE_INTEGRATION_REVISION}` const SITE_URL = "https://wp-codebox-runtime.invalid" const DATABASE_PATH = "/wordpress/wp-content/database/.ht.sqlite" const MARKDOWN_ROOT = "/wordpress/wp-content/markdown" const MARKDOWN_INDEX_PATH = "/tmp/markdown-index.sqlite" +const MARKDOWN_RESOLVED_INDEX_PATH = "/tmp/markdown-index-8133b4cf3c66.sqlite" const R2_MARKDOWN_POINTER_KEY = "sites/default/markdown/current.json" const R2_MARKDOWN_REVISION_PREFIX = "sites/default/markdown/revisions" const R2_MARKDOWN_OBJECT_PREFIX = "sites/default/markdown/objects" +const MUTATION_LEASE_VERSION = "mdi-canonical-v1" let bootPromise: Promise<{ php: PHP; wordpressVersion: string }> | undefined interface Env { @@ -36,7 +39,9 @@ export default { return new Response(`WordPress state ${phase === "r2-mutate" ? "mutation" : "read"} requires ${expectedMethod}.`, { status: 405 }) } const coordinator = env.WORDPRESS_STATE.getByName("default") - return coordinator.fetch(request) + return phase === "r2-state" + ? coordinator.fetch(new Request("https://coordinator/state")) + : runSerializedMarkdownMutation(env, coordinator) } if (phase) return runBootProbe(phase) @@ -80,6 +85,17 @@ interface RuntimeFile { bytes: Uint8Array } +interface MutationLease { + token: string + version: string + baseRevision: string | null + expiresAt: number +} + +interface AcquiredLease extends MutationLease { + pointer: MarkdownPointer | null +} + export class WordPressStateCoordinator implements DurableObject { private tail: Promise = Promise.resolve() @@ -101,8 +117,9 @@ export class WordPressStateCoordinator implements DurableObject { } private async handleRequest(request: Request): Promise { - if (request.method === "GET") { - const pointer = await this.readPointer() + const action = new URL(request.url).pathname + if (request.method === "GET" && action === "/state") { + const pointer = await readMarkdownPointer(this.env.WORDPRESS_STATE_BUCKET) return Response.json({ schema: "wp-codebox/cloudflare-wordpress-state/v1", durableObjectId: this.state.id.toString(), @@ -111,71 +128,141 @@ export class WordPressStateCoordinator implements DurableObject { } if (request.method !== "POST") return new Response("Method not allowed.", { status: 405 }) - const pointer = await this.readPointer() - const markdownFiles = pointer ? await this.readMarkdownRevision(pointer) : initialMarkdownFiles() - const runtime = await bootMarkdownWordPressRuntime(markdownFiles) + if (action === "/begin") { + const existing = await this.state.storage.get("mutation-lease") + if (existing?.version === MUTATION_LEASE_VERSION && existing.expiresAt > Date.now()) { + return Response.json({ retryAfterMs: 1_000 }, { status: 409 }) + } + const pointer = await readMarkdownPointer(this.env.WORDPRESS_STATE_BUCKET) + const lease: MutationLease = { + token: crypto.randomUUID(), + version: MUTATION_LEASE_VERSION, + baseRevision: pointer?.revision ?? null, + expiresAt: Date.now() + 2 * 60_000, + } + await this.state.storage.put("mutation-lease", lease) + return Response.json({ ...lease, pointer } satisfies AcquiredLease) + } + if (action === "/commit") { + const body = await request.json<{ token: string; pointer: MarkdownPointer }>() + const lease = await this.state.storage.get("mutation-lease") + if (!lease || lease.token !== body.token) return new Response("Mutation lease is invalid.", { status: 409 }) + const current = await readMarkdownPointer(this.env.WORDPRESS_STATE_BUCKET) + if ((current?.revision ?? null) !== lease.baseRevision) return new Response("Canonical revision changed during mutation.", { status: 409 }) + await this.env.WORDPRESS_STATE_BUCKET.put(R2_MARKDOWN_POINTER_KEY, JSON.stringify(body.pointer), { + httpMetadata: { contentType: "application/json" }, + }) + await this.state.storage.delete("mutation-lease") + return Response.json(body.pointer) + } + if (action === "/abort") { + const body = await request.json<{ token: string }>() + const lease = await this.state.storage.get("mutation-lease") + if (lease?.token === body.token) { + await this.state.storage.delete("mutation-lease") + } + return new Response(null, { status: 204 }) + } + return new Response("Unknown coordinator action.", { status: 404 }) + } +} + +async function runSerializedMarkdownMutation(env: Env, coordinator: DurableObjectStub): Promise { + const lease = await acquireMutationLease(coordinator) + try { + const markdownFiles = lease.pointer + ? await readMarkdownRevision(env.WORDPRESS_STATE_BUCKET, lease.pointer) + : initialMarkdownFiles() + const runtime = await bootWordPressRuntime( + "do-not-attempt-installing", + true, + true, + undefined, + markdownFiles, + new Uint8Array(markdownPrimaryBootstrapIndex), + ) + let canonicalFiles: RuntimeFile[] + let mutation: { revisionValue: number; previousPostFound: boolean; postId: number; wordpressVersion: string } try { const mutationOutput = (await runtime.php.run({ - code: " 'Cloudflare R2 Proof ' . $value, 'post_name' => 'cloudflare-r2-proof-' . $value, 'post_content' => 'Persisted by MDI primary mode in R2 revision ' . $value . '.', 'post_status' => 'publish', 'post_type' => 'post'], true); if (is_wp_error($post_id)) { throw new Exception($post_id->get_error_message()); } update_option('wp_codebox_mdi_revision', $value); echo json_encode(['revisionValue' => $value, 'previousPostFound' => $previous_found, 'postId' => $post_id, 'wordpressVersion' => get_bloginfo('version')]);", + code: "get_var($wpdb->prepare(\"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s\", 'wp_codebox_mdi_revision')); $previous_found = 0 === $current || null !== $wpdb->get_var($wpdb->prepare(\"SELECT ID FROM {$wpdb->posts} WHERE post_name = %s\", 'cloudflare-r2-proof-' . $current)); $value = $current + 1; $post_id = (int) $wpdb->get_var(\"SELECT COALESCE(MAX(ID), 0) + 1 FROM {$wpdb->posts}\"); $now = gmdate('Y-m-d H:i:s'); $post = (object) ['ID' => $post_id, 'post_author' => 0, 'post_date' => $now, 'post_date_gmt' => $now, 'post_content' => 'Persisted by MDI primary mode in R2 revision ' . $value . '.', 'post_title' => 'Cloudflare R2 Proof ' . $value, 'post_excerpt' => '', 'post_status' => 'publish', 'comment_status' => 'closed', 'ping_status' => 'closed', 'post_password' => '', 'post_name' => 'cloudflare-r2-proof-' . $value, 'to_ping' => '', 'pinged' => '', 'post_modified' => $now, 'post_modified_gmt' => $now, 'post_content_filtered' => '', 'post_parent' => 0, 'guid' => '', 'menu_order' => 0, 'post_type' => 'post', 'post_mime_type' => '', 'comment_count' => 0]; $excluded = array_filter(array_map('trim', explode(',', MARKDOWN_DB_EXCLUDED_TYPES))); $storage = new WP_Markdown_Storage(MARKDOWN_DB_CONTENT_DIR, $excluded); $path = $storage->write_post($post); if (false === $path) { throw new Exception('Canonical Markdown post write failed.'); } $options_dir = MARKDOWN_DB_STATE_DIR . '/_options'; if (!is_dir($options_dir)) { mkdir($options_dir, 0755, true); } $option = ['option_id' => 1000, 'option_name' => 'wp_codebox_mdi_revision', 'option_value' => (string) $value, 'autoload' => 'off']; file_put_contents($options_dir . '/wp_codebox_mdi_revision.json', json_encode($option, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); echo json_encode(['revisionValue' => $value, 'previousPostFound' => $previous_found, 'postId' => $post_id, 'wordpressVersion' => $wp_version]);", })).text.trim() - const mutation = JSON.parse(mutationOutput) as { revisionValue: number; previousPostFound: boolean; postId: number; wordpressVersion: string } - const canonicalFiles = collectRuntimeFiles(runtime.php, MARKDOWN_ROOT) - const nextPointer = await this.persistMarkdownRevision(canonicalFiles) - return Response.json({ - schema: "wp-codebox/cloudflare-wordpress-mutation/v1", - source: pointer ? "r2-markdown-revision" : "packaged-markdown-seed", - ...mutation, - canonicalFiles: canonicalFiles.length, - sqlitePersisted: false, - pointer: nextPointer, - }) + mutation = JSON.parse(mutationOutput) as typeof mutation + canonicalFiles = collectRuntimeFiles(runtime.php, MARKDOWN_ROOT) } finally { runtime.php.exit() } - } - private async readPointer(): Promise { - const object = await this.env.WORDPRESS_STATE_BUCKET.get(R2_MARKDOWN_POINTER_KEY) - return object ? object.json() : null + const nextPointer = await persistMarkdownRevision(env.WORDPRESS_STATE_BUCKET, canonicalFiles) + const commit = await coordinator.fetch(new Request("https://coordinator/commit", { + method: "POST", + body: JSON.stringify({ token: lease.token, pointer: nextPointer }), + })) + if (!commit.ok) throw new Error(`Unable to promote canonical Markdown revision: ${commit.status} ${await commit.text()}`) + return Response.json({ + schema: "wp-codebox/cloudflare-wordpress-mutation/v1", + source: lease.pointer ? "r2-markdown-revision" : "packaged-markdown-seed", + ...mutation, + canonicalFiles: canonicalFiles.length, + markdownDatabaseIntegrationRevision: MARKDOWN_DATABASE_INTEGRATION_REVISION, + sqlitePersisted: false, + pointer: nextPointer, + }) + } catch (error) { + await coordinator.fetch(new Request("https://coordinator/abort", { + method: "POST", + body: JSON.stringify({ token: lease.token }), + })) + throw error } +} - private async readMarkdownRevision(pointer: MarkdownPointer): Promise { - const manifestObject = await this.env.WORDPRESS_STATE_BUCKET.get(pointer.manifestKey) - if (!manifestObject) throw new Error(`R2 Markdown manifest is missing: ${pointer.manifestKey}`) - const manifest = await manifestObject.json() - const files: RuntimeFile[] = [] - for (const file of manifest.files) { - const object = await this.env.WORDPRESS_STATE_BUCKET.get(file.objectKey) - if (!object) throw new Error(`R2 Markdown object is missing: ${file.objectKey}`) - files.push({ path: file.path, bytes: new Uint8Array(await object.arrayBuffer()) }) - } - return files +async function acquireMutationLease(coordinator: DurableObjectStub): Promise { + for (let attempt = 0; attempt < 300; attempt++) { + const response = await coordinator.fetch(new Request("https://coordinator/begin", { method: "POST" })) + if (response.ok) return response.json() + if (response.status !== 409) throw new Error(`Unable to acquire mutation lease: ${response.status} ${await response.text()}`) + await new Promise((resolve) => setTimeout(resolve, 1_000)) } + throw new Error("Timed out waiting for the WordPress mutation lease.") +} - private async persistMarkdownRevision(files: RuntimeFile[]): Promise { - const manifestFiles: MarkdownManifestFile[] = [] - for (const file of files) { - const sha256 = await sha256Hex(file.bytes) - const objectKey = `${R2_MARKDOWN_OBJECT_PREFIX}/${sha256}` - if (!await this.env.WORDPRESS_STATE_BUCKET.head(objectKey)) { - await this.env.WORDPRESS_STATE_BUCKET.put(objectKey, file.bytes) - } - manifestFiles.push({ path: file.path, objectKey, sha256, size: file.bytes.byteLength }) - } +async function readMarkdownPointer(bucket: R2Bucket): Promise { + const object = await bucket.get(R2_MARKDOWN_POINTER_KEY) + return object ? object.json() : null +} - const revision = crypto.randomUUID() - const manifestKey = `${R2_MARKDOWN_REVISION_PREFIX}/${revision}.json` - const persistedAt = new Date().toISOString() - const pointer: MarkdownPointer = { revision, manifestKey, persistedAt } - const manifest: MarkdownManifest = { ...pointer, files: manifestFiles } - await this.env.WORDPRESS_STATE_BUCKET.put(manifestKey, JSON.stringify(manifest), { - httpMetadata: { contentType: "application/json" }, - }) - await this.env.WORDPRESS_STATE_BUCKET.put(R2_MARKDOWN_POINTER_KEY, JSON.stringify(pointer), { - httpMetadata: { contentType: "application/json" }, - }) - return pointer +async function readMarkdownRevision(bucket: R2Bucket, pointer: MarkdownPointer): Promise { + const manifestObject = await bucket.get(pointer.manifestKey) + if (!manifestObject) throw new Error(`R2 Markdown manifest is missing: ${pointer.manifestKey}`) + const manifest = await manifestObject.json() + const files: RuntimeFile[] = [] + for (const file of manifest.files) { + const object = await bucket.get(file.objectKey) + if (!object) throw new Error(`R2 Markdown object is missing: ${file.objectKey}`) + files.push({ path: file.path, bytes: new Uint8Array(await object.arrayBuffer()) }) } + return files +} + +async function persistMarkdownRevision(bucket: R2Bucket, files: RuntimeFile[]): Promise { + const manifestFiles: MarkdownManifestFile[] = [] + for (const file of files) { + const sha256 = await sha256Hex(file.bytes) + const objectKey = `${R2_MARKDOWN_OBJECT_PREFIX}/${sha256}` + if (!await bucket.head(objectKey)) await bucket.put(objectKey, file.bytes) + manifestFiles.push({ path: file.path, objectKey, sha256, size: file.bytes.byteLength }) + } + + const revision = crypto.randomUUID() + const manifestKey = `${R2_MARKDOWN_REVISION_PREFIX}/${revision}.json` + const persistedAt = new Date().toISOString() + const pointer: MarkdownPointer = { revision, manifestKey, persistedAt } + const manifest: MarkdownManifest = { ...pointer, files: manifestFiles } + await bucket.put(manifestKey, JSON.stringify(manifest), { + httpMetadata: { contentType: "application/json" }, + }) + return pointer } async function runBootProbe(phase: string): Promise { @@ -234,7 +321,7 @@ async function runBootProbe(phase: string): Promise { await materializeMarkdownDatabaseIntegration(php) materializeRuntimeFiles(php, MARKDOWN_ROOT, initialMarkdownFiles()) const evidence = (await php.run({ - code: " file_exists('/wordpress/wp-content/db.php'), 'plugin' => file_exists('/wordpress/wp-content/plugins/markdown-database-integration/markdown-database-integration.php'), 'siteurl' => file_exists('/wordpress/wp-content/markdown/_options/siteurl.json')]);", + code: " file_exists('/wordpress/wp-content/db.php'), 'storage' => file_exists('/wordpress/wp-content/plugins/markdown-database-integration/inc/class-wp-markdown-storage.php'), 'siteurl' => file_exists('/wordpress/wp-content/markdown/_options/siteurl.json')]);", })).text.trim() return probeResponse(phase, { ...wordpress, ...JSON.parse(evidence) as Record }) } finally { @@ -243,7 +330,7 @@ async function runBootProbe(phase: string): Promise { } if (phase === "mdi-shortinit") { - const runtime = await bootMarkdownWordPressRuntime(initialMarkdownFiles()) + const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles(), new Uint8Array(markdownPrimaryBootstrapIndex)) try { const evidence = (await runtime.php.run({ code: " $wp_version, 'markdownDropin' => defined('MARKDOWN_DB_DROPIN'), 'markdownMode' => defined('MARKDOWN_DB_MODE') ? MARKDOWN_DB_MODE : '']);", @@ -254,20 +341,8 @@ async function runBootProbe(phase: string): Promise { } } - if (phase === "mdi-index-artifact") { - const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles()) - try { - await runtime.php.run({ code: " $updated];" @@ -283,8 +358,8 @@ async function runBootProbe(phase: string): Promise { } } - if (["mdi-includes", "mdi-embed", "mdi-textdomain", "mdi-ai-client", "mdi-plugin-constants", "mdi-muplugins", "mdi-plugins", "mdi-theme", "mdi-site-health-class", "mdi-site-health", "mdi-current-user", "mdi-init", "mdi-wp-loaded"].includes(phase)) { - const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles()) + if (["mdi-includes", "mdi-embed", "mdi-textdomain", "mdi-ai-client", "mdi-plugin-constants", "mdi-muplugins", "mdi-plugins", "mdi-globals", "mdi-theme", "mdi-site-health-class", "mdi-site-health", "mdi-current-user", "mdi-init", "mdi-wp-loaded"].includes(phase)) { + const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles(), new Uint8Array(markdownPrimaryBootstrapIndex)) try { const evidence = (await runtime.php.run({ code: wordpressProbeCode(phase) })).text.trim() return probeResponse(phase, JSON.parse(evidence) as Record) @@ -345,13 +420,14 @@ function wordpressProbeCode(phase: string): string { } const stops: Record = { - "seeded-includes": { needle: "/**\n * @since 3.3.0" }, + "seeded-includes": { needle: "add_action( 'after_setup_theme', array( wp_script_modules(), 'add_hooks' ) );" }, "seeded-embed": { needle: "/**\n * WordPress Textdomain Registry object." }, "seeded-textdomain": { needle: "// WordPress AI Client initialization." }, "seeded-ai-client": { needle: "// Load multisite-specific files." }, "seeded-plugin-constants": { needle: "// Load must-use plugins." }, "seeded-muplugins": { needle: "if ( is_multisite() ) {\n\tms_cookie_constants();" }, "seeded-plugins": { needle: "// Define constants which affect functionality if not already defined." }, + "seeded-globals": { needle: "/**\n * Fires before the theme is loaded." }, "seeded-theme": { needle: "// Create an instance of WP_Site_Health so that Cron events may fire." }, "seeded-site-health-class": { needle: "WP_Site_Health::get_instance();" }, "seeded-site-health": { needle: "// Set up current user." }, @@ -369,7 +445,7 @@ $needle = ${JSON.stringify(stop.needle)}; if (strpos($settings, $needle) === false) { throw new Exception('WordPress bootstrap probe needle not found.'); } -$stop = "echo json_encode(['wordpressVersion' => \\$wp_version, 'bootstrapPhase' => '${phase}']); return;\n"; +$stop = "echo json_encode(['wordpressVersion' => \\$wp_version, 'bootstrapPhase' => '${phase}', 'memoryBytes' => memory_get_usage(true), 'peakMemoryBytes' => memory_get_peak_usage(true), 'markdownIndexExists' => file_exists('${MARKDOWN_RESOLVED_INDEX_PATH}')]); return;\n"; $replacement = ${stop.after ? "$needle . \"\\n\" . $stop" : "$stop . $needle"}; file_put_contents($settings_path, str_replace($needle, $replacement, $settings)); require '/wordpress/wp-load.php';` @@ -390,9 +466,11 @@ async function bootWordPressRuntime( DISABLE_WP_CRON: true, ...(markdownFiles ? { MARKDOWN_DB_CONTENT_DIR: MARKDOWN_ROOT, + MARKDOWN_DB_EXCLUDED_TYPES: "revision,auto-draft,nav_menu_item,customize_changeset,oembed_cache,wp_navigation,wp_global_styles,wp_template,wp_template_part", MARKDOWN_DB_INDEX_PATH: MARKDOWN_INDEX_PATH, MARKDOWN_DB_MODE: "primary", MARKDOWN_DB_STATE_DIR: MARKDOWN_ROOT, + MARKDOWN_DB_VERSION: "0.8.3", } : {}), WP_HTTP_BLOCK_EXTERNAL: true, }, @@ -403,7 +481,7 @@ async function bootWordPressRuntime( if (markdownFiles) { await materializeMarkdownDatabaseIntegration(php) materializeRuntimeFiles(php, MARKDOWN_ROOT, markdownFiles) - if (markdownIndexSeed) php.writeFile(MARKDOWN_INDEX_PATH, markdownIndexSeed) + if (markdownIndexSeed) php.writeFile(MARKDOWN_RESOLVED_INDEX_PATH, markdownIndexSeed) } } : undefined, beforeDatabaseSetup: databaseSeed ? (php: PHP) => { @@ -424,20 +502,6 @@ async function bootWordPressRuntime( return { php, wordpressVersion } } -async function bootMarkdownWordPressRuntime(markdownFiles: RuntimeFile[]): Promise<{ php: PHP; wordpressVersion: string }> { - const indexBuilder = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, markdownFiles) - let markdownIndex: Uint8Array - try { - await indexBuilder.php.run({ - code: " { } async function materializeMarkdownDatabaseIntegration(php: PHP): Promise { - const response = await fetch(MARKDOWN_DATABASE_INTEGRATION_ARCHIVE_URL) - if (!response.ok || !response.body) throw new Error(`Unable to fetch Markdown Database Integration: ${response.status}.`) - const stream = decodeZip(response.body) + const stream = decodeZip(new Blob([markdownDatabaseIntegrationRuntime]).stream()) const reader = stream.getReader() while (true) { const { done, value: entry } = await reader.read() if (done) break - const relative = archiveRelativePath(entry.name) - if (relative !== "db.php" - && relative !== "markdown-database-integration.php" - && !(relative.startsWith("inc/") && relative.endsWith(".php"))) continue + const relative = entry.name const bytes = new Uint8Array(await entry.arrayBuffer()) const destination = `/wordpress/wp-content/plugins/markdown-database-integration/${relative}` php.mkdir(destination.slice(0, destination.lastIndexOf("/"))) @@ -528,9 +587,12 @@ async function materializeWordPressServerFiles(php: PHP): Promise<{ materialized } function isWordPressServerFile(path: string): boolean { + if (path.startsWith("wordpress/wp-admin/")) { + return path === "wordpress/wp-admin/includes/plugin.php" + || path === "wordpress/wp-admin/includes/class-wp-site-health.php" + } return /\.(?:php|json|crt|html)$/.test(path) || path.endsWith("/style.css") - || path.endsWith("/wp-admin/css/view-transitions.min.css") } function createPhpRuntime() { diff --git a/packages/runtime-cloudflare/src/zip.d.ts b/packages/runtime-cloudflare/src/zip.d.ts new file mode 100644 index 00000000..bfc8135c --- /dev/null +++ b/packages/runtime-cloudflare/src/zip.d.ts @@ -0,0 +1,4 @@ +declare module "*.zip" { + const archive: ArrayBuffer + export default archive +} diff --git a/packages/runtime-cloudflare/wrangler.jsonc b/packages/runtime-cloudflare/wrangler.jsonc index 30b52823..9072a4c7 100644 --- a/packages/runtime-cloudflare/wrangler.jsonc +++ b/packages/runtime-cloudflare/wrangler.jsonc @@ -17,6 +17,6 @@ ], "rules": [ { "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false }, - { "type": "Data", "globs": ["**/*.sqlite"], "fallthrough": false } + { "type": "Data", "globs": ["**/*.sqlite", "**/*-runtime.zip"], "fallthrough": false } ] } diff --git a/scripts/build-cloudflare-mdi-runtime-bundle.mjs b/scripts/build-cloudflare-mdi-runtime-bundle.mjs new file mode 100644 index 00000000..306ae42c --- /dev/null +++ b/scripts/build-cloudflare-mdi-runtime-bundle.mjs @@ -0,0 +1,31 @@ +import { writeFile } from "node:fs/promises" +import { decodeZip, encodeZip } from "@php-wasm/stream-compression" + +const revision = "af451895a9429895e3ad4d9b4e073bfd88873745" +const archiveUrl = `https://codeload.github.com/Automattic/markdown-database-integration/zip/${revision}` +const output = new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url) +const response = await fetch(archiveUrl) +if (!response.ok || !response.body) throw new Error(`Unable to fetch Markdown Database Integration: ${response.status}.`) + +const runtimePaths = new Set([ + "db.php", + "inc/class-wp-markdown-db.php", + "inc/class-wp-markdown-driver.php", + "inc/class-wp-markdown-frontmatter-profiles.php", + "inc/class-wp-markdown-loader.php", + "inc/class-wp-markdown-search.php", + "inc/class-wp-markdown-storage.php", + "inc/class-wp-markdown-write-engine.php", +]) +const runtimeFiles = [] +for await (const entry of decodeZip(response.body)) { + const separator = entry.name.indexOf("/") + const relative = separator === -1 ? "" : entry.name.slice(separator + 1) + if (!runtimePaths.has(relative)) continue + runtimeFiles.push(new File([await entry.arrayBuffer()], relative, { lastModified: 0 })) +} +if (runtimeFiles.length !== runtimePaths.size) throw new Error(`Expected ${runtimePaths.size} MDI runtime files, received ${runtimeFiles.length}.`) + +const archive = await new Response(encodeZip(runtimeFiles)).arrayBuffer() +await writeFile(output, new Uint8Array(archive)) +console.log(`Bundled ${runtimeFiles.length} MDI runtime files from ${revision} (${archive.byteLength} bytes).`) diff --git a/tests/cloudflare-runtime.test.ts b/tests/cloudflare-runtime.test.ts index 8cc89fcd..3af9fe97 100644 --- a/tests/cloudflare-runtime.test.ts +++ b/tests/cloudflare-runtime.test.ts @@ -37,9 +37,14 @@ test("Cloudflare runtime packages the disposable WordPress install seed", async migrations?: Array<{ new_sqlite_classes?: string[] }> } const seed = await readFile(new URL("../packages/runtime-cloudflare/assets/wordpress-install-seed.sqlite", import.meta.url)) + const markdownIndex = await readFile(new URL("../packages/runtime-cloudflare/assets/markdown-primary-bootstrap-index.sqlite", import.meta.url)) + const markdownRuntime = await readFile(new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url)) assert.equal(seed.subarray(0, 16).toString(), "SQLite format 3\0") + assert.equal(markdownIndex.subarray(0, 16).toString(), "SQLite format 3\0") + assert.equal(markdownRuntime.subarray(0, 4).toString("hex"), "504b0304") assert.ok(config.rules?.some((rule) => rule.type === "Data" && rule.globs?.includes("**/*.sqlite"))) + assert.ok(config.rules?.some((rule) => rule.type === "Data" && rule.globs?.includes("**/*-runtime.zip"))) assert.deepEqual(config.r2_buckets, [{ binding: "WORDPRESS_STATE_BUCKET", bucket_name: "wp-codebox-runtime-chubes" }]) assert.deepEqual(config.durable_objects?.bindings, [{ name: "WORDPRESS_STATE", class_name: "WordPressStateCoordinator" }]) assert.ok(config.migrations?.some((migration) => migration.new_sqlite_classes?.includes("WordPressStateCoordinator"))) From a900ae51529d15155753816e57c4e2ed91bf993b Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 18 Jul 2026 17:30:01 -0400 Subject: [PATCH 15/15] Use MDI constrained runtime for R2 revisions --- packages/runtime-cloudflare/README.md | 4 +- .../markdown-database-integration-runtime.zip | Bin 65801 -> 68589 bytes packages/runtime-cloudflare/src/worker.ts | 65 +++++++++++++++++- .../build-cloudflare-mdi-runtime-bundle.mjs | 4 +- tests/cloudflare-runtime.test.ts | 36 ++++++++++ 5 files changed, 104 insertions(+), 5 deletions(-) diff --git a/packages/runtime-cloudflare/README.md b/packages/runtime-cloudflare/README.md index b7690d32..8952e10e 100644 --- a/packages/runtime-cloudflare/README.md +++ b/packages/runtime-cloudflare/README.md @@ -2,7 +2,9 @@ This additive integration is the first acceptance gate for [wp-codebox#1838](https://github.com/Automattic/wp-codebox/issues/1838). It compiles the current PHP 8.5 Asyncify Wasm asset for workerd, boots WordPress through Playground with the current SQLite integration release, executes PHP, and returns a Codebox runtime-command-result envelope containing the stable `wp-codebox/cloudflare-runtime-health/v1` health payload. -The Worker owns Cloudflare transport and isolate lifecycle. Full WordPress initialization requires the paid-plan five-minute CPU allowance, declared as `limits.cpu_ms: 300000`; Cloudflare's fixed 128 MB isolate memory limit remains unchanged. This gate uses one Playground PHP instance per isolate; Durable Objects will serialize site mutation in a later gate. The response identifies the generic `wordpress-playground` backend and `wordpress` environment; callers do not receive Cloudflare binding or limit details. Runtime snapshots, restore, R2, serialization, and cold restoration are deferred to later gates. +The Worker owns Cloudflare transport, PHP-WASM execution, and the caller-owned disposable SQLite cache. [MDI PR #126](https://github.com/Automattic/markdown-database-integration/pull/126), pinned at `94b9f875ffb8402d5e8eb726893a12324e20f45c`, supplies the constrained public primary runtime: normal MDI driver SQL writes are explicitly flushed to deterministic relative Markdown/JSON paths. R2 stores immutable canonical revisions and the current pointer; the Durable Object owns only the persisted lease, base-revision validation, and CAS pointer promotion. SQLite is never uploaded. This preserves cold reconstruction from canonical R2 files and concurrent mutation serialization without expanding MDI's storage-only boundary. + +Existing evidence covers full WordPress initialization and canonical R2 revision behavior. This update changes the source relationship from ad hoc writes to MDI's public constrained runtime, adds source-level bundle and mutation guards, and supports local packaging verification. It does not claim a new remote deployment. ## Verification diff --git a/packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip b/packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip index 4a4af8fdb41c510777ce5a70496d17acb26e2aaa..80f0b63dd6f34f24727389b49ff5dd8cd0ba9246 100644 GIT binary patch delta 2801 zcmVNWX>DaLaA_w}n+U3djYyp&21R?-YA!3rW4 zAnH=Ff)2ZXph zfe2og8S*2Ll02tkhtE;!__q&8mh!Tssbahwx+Osi!b%AQGA1i7$!$)7l^MO?MRkx@ zN=ja`l;rS{h_4x+ms;|#Q^6hi-@7Gqu}Y zcDrd#q=d_}#7j9dW=zJ$g2}ltIy`i{o!75k!B2NYfK-B&Qmm3P8Rf=pQu` zR2r_{G=P#p&{SuD+9;oKDxu`6Ip$9l5u~ur2(dTv-f$4}zUZ0IJMzJ4(}W4hVaWG? z@+>jtO^zB9ZvE>8c0O)$Lg=6{k%WS5bUgE~m)RuXc>DK~6m+<5nPv}9p_;GsjmJzVN#a{KesL)#NAFuJI>;E(Cu_K!ZJV~(pV)uRB-LUGV%`I1N1n4(w++( zvIk1kdXwF=V!AzpthkRP?yP$d&8sBpcKRcbG@oFR>DV%)YfM7#nN%`{Xg$Y&V_Ew4 z?QzO5wLgd6hjlvZf67$PLri2Hba>bnisIfUJU=^so@ zfLl7@f;RrD(CRw#xvXOQl?eK~-A-rOMT6NzbO4=eTg5IfX{w&vZ3x0M*^-R(hRM!; znD_>3t}+Zgu{qHO#>NeC3rGQfe!%Or!Z56=8`y?ELoj!?=&e7wj;&2+ouC&D)K&Ju#X$2(gKV@x z%Sz8O2Z8W40>a~ zl}XnPWY~hZGSg~sY!r2tUI#DwD#BV8UBe4hX!W%I>+=+B^-rw|?$veO2oyx6Gh3_N zp-ia?wOu-EuG?}IF6K8Od2XDr1d46h7FxL|(}b0&pam@zaeG=Y{0^UYI~_q)ElN+h z+tpc1x4!>U6l}B21kZ$KF<{piw=Kj5;5Bi1Wg&ANU@NRzJNue* zk}=^@caKbzEXKY8Lz8@{y+ClpxIN-Ox z(^R>JiqCPL?YTkWkD3a^{P^U<$%j=z~%ds^Ak8d#xYsbI+b;7TWc|y*qAeV_2Gvf;04g4Qo-7RdAfRY4I|xzn=Y}x zZoPtkD)J{=uL+tbZKtJwrHg@ix+8i0JaZwrvzSxv`^;ud1fwvMZ#0pjqLBYb8~MwT z_2ce7X2h4)h)N9=x#uM>76VX2nb%Wm!w1W8iAP9GOTtSzW0kxCCE(Z3r=M1-9IG2p z3O;`VC5EkaV`-GRy1IcIiqT9gx!LQVjsKK?7@Bx9pC6nhmoNW3AL{?RnDPEe4-Q<4 z(uH+kz7;s=*k{T9O;_FAN}e6>s=CUs@5c&ebch2nmcQd|`8H?x->Tupd1_OS*XlJj z_ea~C7&Z=F`+Ck^e$1{U8j6Gr)e!DyQ8P}gZqtFz8^C|Q^m!iA?TUQvM=w@APcEz;l7PV zCC8sH#;z|6Y}6=$M?-UZjZJrlOFR2aBI8)6Zv>=BXqm|b=M_&gr3#6#w&($HSY-B6 z^O^M6W1GuOr-F6xmACZ}WPK}@2$`IJ5jAs6q!KwT8|(iC{tA7-w$s;bM4d)>*xqmA zW^HAh<$Bexw$;>nJ>6&qV}{+)%*Bt77?yndh~Z=Ik*G0~zRM2=BJK?j88$fXJ|~LI z6_~c5zL7@vNkxS64m6yi*PE}RPs=-!v+PhzYrMhEpl~kbZKp6ld%63l@b9O0z{RQ z;QttvMgcSeH=~o_{}`2&0W|`i-;?0~7?Vxa5Ef8N1qJ{B000R92>`|d000pQ0RR91 D!n{kF delta 94 zcmaDmou#vhWy9I7>EE~*1*R8sGKx*t;%4O9+%#3R6eK1!eItkfikUENnlZhQlTl)F n_G0eo&p^s2b2CaYNnQnuPCw4gD8a|Uz`>Bt#K6$=j{yh(+glrR diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index b670e4ba..10eca3bb 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -13,7 +13,7 @@ import wordpressInstallSeed from "../assets/wordpress-install-seed.sqlite" const PHP_VERSION = "8.5.8" const WORDPRESS_ARCHIVE_URL = "https://wordpress.org/latest.zip" const SQLITE_INTEGRATION_ARCHIVE_URL = "https://github.com/WordPress/sqlite-database-integration/releases/download/v2.2.23/plugin-sqlite-database-integration.zip" -const MARKDOWN_DATABASE_INTEGRATION_REVISION = "af451895a9429895e3ad4d9b4e073bfd88873745" +const MARKDOWN_DATABASE_INTEGRATION_REVISION = "94b9f875ffb8402d5e8eb726893a12324e20f45c" const SITE_URL = "https://wp-codebox-runtime.invalid" const DATABASE_PATH = "/wordpress/wp-content/database/.ht.sqlite" const MARKDOWN_ROOT = "/wordpress/wp-content/markdown" @@ -23,6 +23,43 @@ const R2_MARKDOWN_POINTER_KEY = "sites/default/markdown/current.json" const R2_MARKDOWN_REVISION_PREFIX = "sites/default/markdown/revisions" const R2_MARKDOWN_OBJECT_PREFIX = "sites/default/markdown/objects" const MUTATION_LEASE_VERSION = "mdi-canonical-v1" +const SERIALIZED_MARKDOWN_MUTATION_CODE = `prefix; +$connection = new WP_SQLite_Connection(['pdo' => $GLOBALS['@pdo'], 'path' => FQDB]); +$runtime = WP_Markdown_Primary_Storage_Runtime::bootstrap( + ['content_root' => MARKDOWN_DB_CONTENT_DIR, 'state_root' => MARKDOWN_DB_STATE_DIR], + $connection, + defined('DB_NAME') && '' !== DB_NAME ? DB_NAME : 'database_name_here', + null, + true, + array_filter(array_map('trim', explode(',', MARKDOWN_DB_EXCLUDED_TYPES))), + $prefix +); +$driver = $runtime->get_driver(); +$option_rows = $driver->query("SELECT option_id, option_value FROM \`{\$prefix}options\` WHERE option_name = 'wp_codebox_mdi_revision'"); +$current = empty($option_rows) ? 0 : (int) $option_rows[0]->option_value; +$previous_rows = 0 === $current ? [] : $driver->query("SELECT ID FROM \`{\$prefix}posts\` WHERE post_name = 'cloudflare-r2-proof-$current'"); +$value = $current + 1; +$post_id_rows = $driver->query("SELECT COALESCE(MAX(ID), 0) + 1 AS post_id FROM \`{\$prefix}posts\`"); +$post_id = (int) $post_id_rows[0]->post_id; +$now = gmdate('Y-m-d H:i:s'); +$slug = 'cloudflare-r2-proof-' . $value; +$title = 'Cloudflare R2 Proof ' . $value; +$content = 'Persisted by MDI primary mode in R2 revision ' . $value . '.'; +$driver->query("INSERT INTO \`{\$prefix}posts\` (ID, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count) VALUES ($post_id, 0, '$now', '$now', '$content', '$title', '', 'publish', 'closed', 'closed', '', '$slug', '', '', '$now', '$now', '', 0, '', 0, 'post', '', 0)"); +if (empty($option_rows)) { + $driver->query("INSERT INTO \`{\$prefix}options\` (option_name, option_value, autoload) VALUES ('wp_codebox_mdi_revision', '$value', 'off')"); +} else { + $driver->query("UPDATE \`{\$prefix}options\` SET option_value = '$value', autoload = 'off' WHERE option_name = 'wp_codebox_mdi_revision'"); +} +$changes = $runtime->flush(); +echo json_encode(['revisionValue' => $value, 'previousPostFound' => !empty($previous_rows), 'postId' => $post_id, 'wordpressVersion' => $wp_version, 'canonicalChanges' => $changes]);` let bootPromise: Promise<{ php: PHP; wordpressVersion: string }> | undefined interface Env { @@ -182,12 +219,13 @@ async function runSerializedMarkdownMutation(env: Env, coordinator: DurableObjec new Uint8Array(markdownPrimaryBootstrapIndex), ) let canonicalFiles: RuntimeFile[] - let mutation: { revisionValue: number; previousPostFound: boolean; postId: number; wordpressVersion: string } + let mutation: { revisionValue: number; previousPostFound: boolean; postId: number; wordpressVersion: string; canonicalChanges: MarkdownChanges } try { const mutationOutput = (await runtime.php.run({ - code: "get_var($wpdb->prepare(\"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s\", 'wp_codebox_mdi_revision')); $previous_found = 0 === $current || null !== $wpdb->get_var($wpdb->prepare(\"SELECT ID FROM {$wpdb->posts} WHERE post_name = %s\", 'cloudflare-r2-proof-' . $current)); $value = $current + 1; $post_id = (int) $wpdb->get_var(\"SELECT COALESCE(MAX(ID), 0) + 1 FROM {$wpdb->posts}\"); $now = gmdate('Y-m-d H:i:s'); $post = (object) ['ID' => $post_id, 'post_author' => 0, 'post_date' => $now, 'post_date_gmt' => $now, 'post_content' => 'Persisted by MDI primary mode in R2 revision ' . $value . '.', 'post_title' => 'Cloudflare R2 Proof ' . $value, 'post_excerpt' => '', 'post_status' => 'publish', 'comment_status' => 'closed', 'ping_status' => 'closed', 'post_password' => '', 'post_name' => 'cloudflare-r2-proof-' . $value, 'to_ping' => '', 'pinged' => '', 'post_modified' => $now, 'post_modified_gmt' => $now, 'post_content_filtered' => '', 'post_parent' => 0, 'guid' => '', 'menu_order' => 0, 'post_type' => 'post', 'post_mime_type' => '', 'comment_count' => 0]; $excluded = array_filter(array_map('trim', explode(',', MARKDOWN_DB_EXCLUDED_TYPES))); $storage = new WP_Markdown_Storage(MARKDOWN_DB_CONTENT_DIR, $excluded); $path = $storage->write_post($post); if (false === $path) { throw new Exception('Canonical Markdown post write failed.'); } $options_dir = MARKDOWN_DB_STATE_DIR . '/_options'; if (!is_dir($options_dir)) { mkdir($options_dir, 0755, true); } $option = ['option_id' => 1000, 'option_name' => 'wp_codebox_mdi_revision', 'option_value' => (string) $value, 'autoload' => 'off']; file_put_contents($options_dir . '/wp_codebox_mdi_revision.json', json_encode($option, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); echo json_encode(['revisionValue' => $value, 'previousPostFound' => $previous_found, 'postId' => $post_id, 'wordpressVersion' => $wp_version]);", + code: SERIALIZED_MARKDOWN_MUTATION_CODE, })).text.trim() mutation = JSON.parse(mutationOutput) as typeof mutation + validateMarkdownChanges(mutation.canonicalChanges) canonicalFiles = collectRuntimeFiles(runtime.php, MARKDOWN_ROOT) } finally { runtime.php.exit() @@ -217,6 +255,27 @@ async function runSerializedMarkdownMutation(env: Env, coordinator: DurableObjec } } +interface MarkdownChanges { + created: string[] + changed: string[] + deleted: string[] +} + +function validateMarkdownChanges(changes: MarkdownChanges): void { + for (const group of [changes.created, changes.changed, changes.deleted]) { + if (!Array.isArray(group) || group.some((path) => !isCanonicalRelativePath(path))) { + throw new Error("MDI flush returned an invalid canonical path.") + } + if (group.some((path, index) => index > 0 && group[index - 1] >= path)) { + throw new Error("MDI flush returned non-deterministic canonical paths.") + } + } +} + +function isCanonicalRelativePath(path: string): boolean { + return path.length > 0 && !path.startsWith("/") && !path.split("/").includes("..") +} + async function acquireMutationLease(coordinator: DurableObjectStub): Promise { for (let attempt = 0; attempt < 300; attempt++) { const response = await coordinator.fetch(new Request("https://coordinator/begin", { method: "POST" })) diff --git a/scripts/build-cloudflare-mdi-runtime-bundle.mjs b/scripts/build-cloudflare-mdi-runtime-bundle.mjs index 306ae42c..bc4ef407 100644 --- a/scripts/build-cloudflare-mdi-runtime-bundle.mjs +++ b/scripts/build-cloudflare-mdi-runtime-bundle.mjs @@ -1,7 +1,7 @@ import { writeFile } from "node:fs/promises" import { decodeZip, encodeZip } from "@php-wasm/stream-compression" -const revision = "af451895a9429895e3ad4d9b4e073bfd88873745" +const revision = "94b9f875ffb8402d5e8eb726893a12324e20f45c" const archiveUrl = `https://codeload.github.com/Automattic/markdown-database-integration/zip/${revision}` const output = new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url) const response = await fetch(archiveUrl) @@ -13,6 +13,7 @@ const runtimePaths = new Set([ "inc/class-wp-markdown-driver.php", "inc/class-wp-markdown-frontmatter-profiles.php", "inc/class-wp-markdown-loader.php", + "inc/class-wp-markdown-primary-storage-runtime.php", "inc/class-wp-markdown-search.php", "inc/class-wp-markdown-storage.php", "inc/class-wp-markdown-write-engine.php", @@ -26,6 +27,7 @@ for await (const entry of decodeZip(response.body)) { } if (runtimeFiles.length !== runtimePaths.size) throw new Error(`Expected ${runtimePaths.size} MDI runtime files, received ${runtimeFiles.length}.`) +runtimeFiles.sort((left, right) => left.name.localeCompare(right.name)) const archive = await new Response(encodeZip(runtimeFiles)).arrayBuffer() await writeFile(output, new Uint8Array(archive)) console.log(`Bundled ${runtimeFiles.length} MDI runtime files from ${revision} (${archive.byteLength} bytes).`) diff --git a/tests/cloudflare-runtime.test.ts b/tests/cloudflare-runtime.test.ts index 3af9fe97..fb8add81 100644 --- a/tests/cloudflare-runtime.test.ts +++ b/tests/cloudflare-runtime.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict" import { readFile } from "node:fs/promises" import test from "node:test" +import { decodeZip } from "@php-wasm/stream-compression" import { RUNTIME_COMMAND_RESULT_SCHEMA } from "../packages/runtime-core/src/runtime-contracts.js" import { CLOUDFLARE_RUNTIME_HEALTH_MARKER, CLOUDFLARE_RUNTIME_HEALTH_SCHEMA, cloudflareRuntimeHealthResponse } from "../packages/runtime-cloudflare/src/health-envelope.js" @@ -49,3 +50,38 @@ test("Cloudflare runtime packages the disposable WordPress install seed", async assert.deepEqual(config.durable_objects?.bindings, [{ name: "WORDPRESS_STATE", class_name: "WordPressStateCoordinator" }]) assert.ok(config.migrations?.some((migration) => migration.new_sqlite_classes?.includes("WordPressStateCoordinator"))) }) + +test("Cloudflare runtime pins and bundles the public constrained MDI runtime", async () => { + const revision = "94b9f875ffb8402d5e8eb726893a12324e20f45c" + const generator = await readFile(new URL("../scripts/build-cloudflare-mdi-runtime-bundle.mjs", import.meta.url), "utf8") + const worker = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8") + const runtime = await readFile(new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url)) + const names: string[] = [] + for await (const entry of decodeZip(new Blob([runtime]).stream())) names.push(entry.name) + + assert.match(generator, new RegExp(`const revision = "${revision}"`)) + assert.match(worker, new RegExp(`MARKDOWN_DATABASE_INTEGRATION_REVISION = "${revision}"`)) + assert.deepEqual(names.sort(), [ + "db.php", + "inc/class-wp-markdown-db.php", + "inc/class-wp-markdown-driver.php", + "inc/class-wp-markdown-frontmatter-profiles.php", + "inc/class-wp-markdown-loader.php", + "inc/class-wp-markdown-primary-storage-runtime.php", + "inc/class-wp-markdown-search.php", + "inc/class-wp-markdown-storage.php", + "inc/class-wp-markdown-write-engine.php", + ]) +}) + +test("serialized Cloudflare mutations use the public MDI runtime and its flush paths", async () => { + const source = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8") + const mutation = source.slice(source.indexOf("const SERIALIZED_MARKDOWN_MUTATION_CODE"), source.indexOf("let bootPromise")) + + assert.match(mutation, /WP_Markdown_Primary_Storage_Runtime::bootstrap/) + assert.match(mutation, /new WP_SQLite_Connection\(\['pdo' => \$GLOBALS\['@pdo'\], 'path' => FQDB\]\)/) + assert.match(mutation, /\$runtime->get_driver\(\)/) + assert.match(mutation, /\$runtime->flush\(\)/) + assert.doesNotMatch(mutation, /\$wpdb->dbh|\{\$\{prefix\}|WP_Markdown_Storage|write_post|file_put_contents|wp_codebox_mdi_revision\.json/) + assert.match(source, /validateMarkdownChanges\(mutation\.canonicalChanges\)/) +})