diff --git a/.gitignore b/.gitignore index 33803f22..fa5db7b3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ scripts/uploads/ *.tsbuildinfo .DS_Store .wrangler/ +# Disposable local Cloudflare/Miniflare state and Python bytecode. +packages/runtime-cloudflare/.bricks-local-state-*/ +packages/runtime-cloudflare/scripts/__pycache__/ +# Generated local native API credentials/config; export only compact receipts. +packages/runtime-cloudflare/outputs/native-api-prepared*/ diff --git a/outputs/bricks-native-api-local-rehearsal/README.md b/outputs/bricks-native-api-local-rehearsal/README.md new file mode 100644 index 00000000..22eb6577 --- /dev/null +++ b/outputs/bricks-native-api-local-rehearsal/README.md @@ -0,0 +1,11 @@ +# Native Bricks API local rehearsal + +Actual Build producer and writer at `db6717f419bfd848296db4eb193b77ff91db4e0a` called the local Cloudflare Worker, D1 and R2 emulator. The Worker ran PHP-WASM 8.5 and the original agency-provided Bricks 2.4-rc ZIP, SHA-256 `a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887`. Every Bricks vendor file in this allocation was extracted directly from that ZIP. The SQLite alias correction is a separate checked-in runtime materialization patch. + +The source allocation started with no native documents or uploads. Provision committed revision 2, revision committed 3, a stale base returned HTTP 409, and restore committed NEW revision 4 with the original native document hashes and design-system version. Each operation passed the real Build writer terminal-receipt validator, byte-equivalent idempotent replay, operation polling, and public heading observation. `receipt.json` contains the exact requests and receipts. `artifact.json` is the staged original native artifact. + +After stopping and restarting Wrangler, `cold-reopen.json` confirms the saved operation receipt, restored native heading, loaded images, and desktop/mobile rendering. Screenshots were inspected. This is a deliberately minimal API fixture with two one-pixel image instances, not a customer design or a visual parity acceptance result. The authored font-family CSS is present; the fixture does not bundle the requested webfont binaries. + +All native receipts remain `draft`, with null protected preview, false editor qualification, and all five client-acceptance steps `not_run`. No Bricks license activation or actual visual-editor acceptance occurred. Programmatic native save and rollback proof does not replace the licensed editor transaction. + +Validation: package TypeScript build, seven focused artifact/admission tests, actual local PHP integration, and existing canary Wrangler deployment dry run passed. No remote deployment or customer site mutation was performed for this local API proof. diff --git a/outputs/bricks-native-api-local-rehearsal/artifact.json b/outputs/bricks-native-api-local-rehearsal/artifact.json new file mode 100644 index 00000000..ae441ff0 --- /dev/null +++ b/outputs/bricks-native-api-local-rehearsal/artifact.json @@ -0,0 +1 @@ +{"assets":[{"alt_text":"Bright and Kind Cleaning","bytes":68,"content_base64":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=","filename":"bright-kind.png","logical_id":"brand-mark","mime_type":"image/png","sha256":"431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460"}],"design_system":{"global_classes":[{"id":"section-shell","name":"Section shell","settings":{"_padding":{"bottom":"clamp(4rem,8vw,9rem)","left":"2rem","right":"2rem","top":"clamp(4rem,8vw,9rem)"}},"sha256":"7cad43bd6e7652de1c992d4d34969f4d6d4e4e80196bead2a1af6e7cd2fd1029"}],"global_variables":[{"id":"space-l","name":"Space L","sha256":"f1737a9fed1c8662fc016baff35f6cad6fd4535da2b70ead09a28edf70a094ac","type":"string","value":"4rem"}],"palette":[{"id":"brand","name":"Kind Blue","value":"#2B6F8A"},{"id":"accent","name":"Warm Gold","value":"#F0B44D"}],"theme_style":{"settings":{},"sha256":"61600e5912f22f7566b4371b4d0e0b89747ffbde3853f6435b63a441c5655a1a"},"typography":{"body_font_family":"Inter","heading_font_family":"Fraunces","root_font_size":"62.5%"},"version":"design-system-1"},"documents":{"pages":[{"elements":[{"children":[{"children":[{"children":[],"id":"hero-heading","name":"heading","settings":{"tag":"h1","text":"A brighter home"}},{"children":[],"id":"hero-copy","name":"text-basic","settings":{"tag":"p","text":"A clean home, handled with care."}},{"children":[],"id":"hero-image","name":"image","settings":{"image":{"asset_ref":"brand-mark","size":"full"}}}],"id":"hero","name":"section","settings":{"_cssGlobalClasses":["section-shell"]}},{"children":[{"children":[],"id":"services-heading","name":"heading","settings":{"tag":"h2","text":"Cleaning services"}},{"children":[],"id":"services-copy","name":"text-basic","settings":{"tag":"p","text":"Recurring, deep, and move-out cleaning."}}],"id":"services","name":"section","settings":{"_cssGlobalClasses":["section-shell"]}},{"children":[{"children":[],"id":"trust-heading","name":"heading","settings":{"tag":"h2","text":"Kind people, careful work"}},{"children":[],"id":"trust-copy","name":"text-basic","settings":{"tag":"p","text":"Local professionals who respect your space."}}],"id":"trust","name":"section","settings":{"_cssGlobalClasses":["section-shell"]}},{"children":[{"children":[],"id":"contact-heading","name":"heading","settings":{"tag":"h2","text":"Request an estimate"}},{"children":[],"id":"contact-copy","name":"text-basic","settings":{"tag":"p","text":"Tell us what you need and we will prepare an estimate."}}],"id":"contact","name":"section","settings":{"_cssGlobalClasses":["section-shell"]}}],"id":"home-main","name":"container","settings":{"tag":"main"}}],"logical_id":"home","sha256":"1ee9a15f673e4952905db62eb8bd6c9d622507283668c9af2e3ccb01047b834e","slug":"home","status":"publish","title":"Bright and Kind Cleaning"}],"templates":[{"conditions":[{"main":"any"}],"elements":[{"children":[{"children":[],"id":"header-logo","name":"image","settings":{"image":{"asset_ref":"brand-mark","size":"full"}}},{"children":[],"id":"header-name","name":"text-basic","settings":{"tag":"span","text":"Bright and Kind Cleaning"}}],"id":"header-shell","name":"section","settings":{"tag":"header"}}],"logical_id":"site-header","sha256":"ce05a59acf2c231e9ec75e8ee9fac41ce32bde043765ea269449f2ccfafebc7b","status":"publish","title":"Site Header","type":"header"},{"conditions":[{"main":"any"}],"elements":[{"children":[{"children":[],"id":"footer-copy","name":"text-basic","settings":{"tag":"p","text":"Bright and Kind Cleaning"}}],"id":"footer-shell","name":"section","settings":{"tag":"footer"}}],"logical_id":"site-footer","sha256":"9e355a90b0b11ac633abae4116c291e59c3cff8776ebce15513197bcfac2758d","status":"publish","title":"Site Footer","type":"footer"}]},"editing":{"acceptance_sequence":["edit","publish","public_observation","restore","reopen"],"granularity":"native_bricks_elements","target_role":{"required_capabilities":["bricks_edit_content"],"role_slug":"editor"}},"evidence_inputs":{"authorized_asset_hashes":["431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460"],"creative_provenance":{"canonicalCommit":"db6717f419bfd848296db4eb193b77ff91db4e0a","catalogSha256":"3333333333333333333333333333333333333333333333333333333333333333","guideSha256":"2222222222222222222222222222222222222222222222222222222222222222","references":[{"id":"bright-kind-cleaning","sha256":"4444444444444444444444444444444444444444444444444444444444444444"}],"version":"2026-09-10-v1"},"dossier":{"revision":3,"sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}},"runtime":{"sha256":"a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887","version":"2.4-rc"},"schema":"wp-codebox/bricks-site-artifact/v1","site":{"customer_id":"customer_417","site_id":"default","starter_id":"starter_native_service_v1","title":"Bright and Kind Cleaning"}} \ No newline at end of file diff --git a/outputs/bricks-native-api-local-rehearsal/cold-reopen.json b/outputs/bricks-native-api-local-rehearsal/cold-reopen.json new file mode 100644 index 00000000..fa155ed8 --- /dev/null +++ b/outputs/bricks-native-api-local-rehearsal/cold-reopen.json @@ -0,0 +1,62 @@ +{ + "status": "cold-native-receipt-and-public-render-passed", + "origin": "http://127.0.0.1:8812", + "restored_revision": { + "receipt_id": "native_034982672dcf4ec899496b92fc5c3da8", + "canonical_state_version": 4, + "canonical_state_revision": "d207bbb4-18f3-489b-820d-c1271fe14129", + "canonical_manifest_key": "sites/default/markdown/revisions/d207bbb4-18f3-489b-820d-c1271fe14129.json", + "canonical_persisted_at": "2026-09-10T18:50:09.925Z", + "artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1" + }, + "observations": [ + { + "viewport": "desktop", + "status": 200, + "heading": "A brighter home", + "nativeElements": 19, + "images": [ + { + "src": "http://127.0.0.1:8812/wp-content/uploads/2026/09/bright-kind.png", + "loaded": true, + "width": 1 + }, + { + "src": "http://127.0.0.1:8812/wp-content/uploads/2026/09/bright-kind.png", + "loaded": true, + "width": 1 + } + ], + "overflow": false, + "screenshot_sha256": "178aff9e141fedc73031db6afa9f86ae12abc62484a69364251276c85c42af7f" + }, + { + "viewport": "mobile", + "status": 200, + "heading": "A brighter home", + "nativeElements": 19, + "images": [ + { + "src": "http://127.0.0.1:8812/wp-content/uploads/2026/09/bright-kind.png", + "loaded": true, + "width": 1 + }, + { + "src": "http://127.0.0.1:8812/wp-content/uploads/2026/09/bright-kind.png", + "loaded": true, + "width": 1 + } + ], + "overflow": false, + "screenshot_sha256": "48ba45100bcf35061938707ad430093f425d506b5037ce637472ef0baff8ff4a" + } + ], + "qualification": "Programmatic native API proof only; licensed visual editor not run." +} diff --git a/outputs/bricks-native-api-local-rehearsal/desktop.png b/outputs/bricks-native-api-local-rehearsal/desktop.png new file mode 100644 index 00000000..e910548a Binary files /dev/null and b/outputs/bricks-native-api-local-rehearsal/desktop.png differ diff --git a/outputs/bricks-native-api-local-rehearsal/mobile.png b/outputs/bricks-native-api-local-rehearsal/mobile.png new file mode 100644 index 00000000..cb9f38c5 Binary files /dev/null and b/outputs/bricks-native-api-local-rehearsal/mobile.png differ diff --git a/outputs/bricks-native-api-local-rehearsal/receipt.json b/outputs/bricks-native-api-local-rehearsal/receipt.json new file mode 100644 index 00000000..394b0e0a --- /dev/null +++ b/outputs/bricks-native-api-local-rehearsal/receipt.json @@ -0,0 +1,772 @@ +{ + "status": "real-local-native-provision-revise-restore-passed", + "buildCommit": "db6717f419bfd848296db4eb193b77ff91db4e0a", + "origin": "http://127.0.0.1:8812", + "staleRejected": true, + "results": [ + { + "action": "provision", + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "build_native_v3_provision", + "idempotency_key": "build_native_v3_provision", + "site_id": "default", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "db6717f419bfd848296db4eb193b77ff91db4e0a", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": null, + "canonical_state_version": null, + "canonical_state_revision": null, + "canonical_manifest_key": null, + "canonical_persisted_at": null, + "artifact_sha256": null, + "native_document_hashes": null, + "design_system_version": null + }, + "rollback_receipt": { + "required": true, + "receipt_id": null, + "canonical_state_version": null, + "canonical_state_revision": null, + "canonical_manifest_key": null, + "canonical_persisted_at": null, + "artifact_sha256": null, + "native_document_hashes": null, + "design_system_version": null + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "operation": { + "id": "build_native_v3_provision", + "siteId": "default", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "provision", + "operation_id": "build_native_v3_provision", + "target_request_sha256": "da5ac9204693475e2bb258c02c1d5385ae18aa256da6780002dd270ba9d998a3", + "site_id": "default", + "customer_id": "customer_417", + "state": "draft", + "site_allocation": { + "allocation_id": "default", + "canonical_state_revision": "5a401507-02c1-4e8c-872c-cc214b4076e8" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "db6717f419bfd848296db4eb193b77ff91db4e0a", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "requested", + "url": null + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "local-native-api-rehearsal", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "50ef8322ff945c280aad077c4494389be26dd0a052f9941442cd1d67166f1d8f" + }, + "revision_receipt": { + "receipt_id": "native_28a13bbf3a9341de94f2a7c1a924e480", + "canonical_state_version": 2, + "canonical_state_revision": "5a401507-02c1-4e8c-872c-cc214b4076e8", + "canonical_manifest_key": "sites/default/markdown/revisions/5a401507-02c1-4e8c-872c-cc214b4076e8.json", + "canonical_persisted_at": "2026-09-10T18:50:04.108Z", + "artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": null, + "canonical_state_version": null, + "canonical_state_revision": null, + "canonical_manifest_key": null, + "canonical_persisted_at": null, + "artifact_sha256": null, + "native_document_hashes": null, + "design_system_version": null + }, + "restoration": null + } + }, + "public_heading_observed": "A brighter home", + "idempotent_replay": true, + "poll_verified": true + }, + { + "action": "revise", + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "build_native_v3_revise", + "idempotency_key": "build_native_v3_revise", + "site_id": "default", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "29982ab5624346955b0d935f9febee64429ee6ebd17f2905639a3ab0a41b05db" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "db6717f419bfd848296db4eb193b77ff91db4e0a", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": "native_28a13bbf3a9341de94f2a7c1a924e480", + "canonical_state_version": 2, + "canonical_state_revision": "5a401507-02c1-4e8c-872c-cc214b4076e8", + "canonical_manifest_key": "sites/default/markdown/revisions/5a401507-02c1-4e8c-872c-cc214b4076e8.json", + "canonical_persisted_at": "2026-09-10T18:50:04.108Z", + "artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "required": true, + "receipt_id": "native_28a13bbf3a9341de94f2a7c1a924e480", + "canonical_state_version": 2, + "canonical_state_revision": "5a401507-02c1-4e8c-872c-cc214b4076e8", + "canonical_manifest_key": "sites/default/markdown/revisions/5a401507-02c1-4e8c-872c-cc214b4076e8.json", + "canonical_persisted_at": "2026-09-10T18:50:04.108Z", + "artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1" + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "operation": { + "id": "build_native_v3_revise", + "siteId": "default", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "revise", + "operation_id": "build_native_v3_revise", + "target_request_sha256": "1cc213485fb80ff3649ab85a6dda4e02ef9bdc1070cc880000bdc4064d38f5a9", + "site_id": "default", + "customer_id": "customer_417", + "state": "draft", + "site_allocation": { + "allocation_id": "default", + "canonical_state_revision": "1f3f895f-9568-444d-8a8c-c2a820f08026" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "f36333b5ab85e5bba08bc5732036c01d6fd06438b30245e15ca684de17664c82", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "c88dab5d9acbb6be26773209f7b6a48b25bfde34e29bded1cd5d17d63eff1961" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "29982ab5624346955b0d935f9febee64429ee6ebd17f2905639a3ab0a41b05db", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "db6717f419bfd848296db4eb193b77ff91db4e0a", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "requested", + "url": null + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "local-native-api-rehearsal", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "29982ab5624346955b0d935f9febee64429ee6ebd17f2905639a3ab0a41b05db" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "5e5595172f152e2ef353741506cae0005fa0f17cf6655075e26c9cf0ca0f0dc6" + }, + "revision_receipt": { + "receipt_id": "native_6056be701de2437c976fbd1a7477b44b", + "canonical_state_version": 3, + "canonical_state_revision": "1f3f895f-9568-444d-8a8c-c2a820f08026", + "canonical_manifest_key": "sites/default/markdown/revisions/1f3f895f-9568-444d-8a8c-c2a820f08026.json", + "canonical_persisted_at": "2026-09-10T18:50:07.213Z", + "artifact_sha256": "29982ab5624346955b0d935f9febee64429ee6ebd17f2905639a3ab0a41b05db", + "native_document_hashes": [ + "f36333b5ab85e5bba08bc5732036c01d6fd06438b30245e15ca684de17664c82", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "c88dab5d9acbb6be26773209f7b6a48b25bfde34e29bded1cd5d17d63eff1961" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": "native_28a13bbf3a9341de94f2a7c1a924e480", + "canonical_state_version": 2, + "canonical_state_revision": "5a401507-02c1-4e8c-872c-cc214b4076e8", + "canonical_manifest_key": "sites/default/markdown/revisions/5a401507-02c1-4e8c-872c-cc214b4076e8.json", + "canonical_persisted_at": "2026-09-10T18:50:04.108Z", + "artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1" + }, + "restoration": null + } + }, + "public_heading_observed": "Native API revised heading", + "idempotent_replay": true, + "poll_verified": true + }, + { + "action": "restore", + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "build_native_v3_restore", + "idempotency_key": "build_native_v3_restore", + "site_id": "default", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "db6717f419bfd848296db4eb193b77ff91db4e0a", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": "native_6056be701de2437c976fbd1a7477b44b", + "canonical_state_version": 3, + "canonical_state_revision": "1f3f895f-9568-444d-8a8c-c2a820f08026", + "canonical_manifest_key": "sites/default/markdown/revisions/1f3f895f-9568-444d-8a8c-c2a820f08026.json", + "canonical_persisted_at": "2026-09-10T18:50:07.213Z", + "artifact_sha256": "29982ab5624346955b0d935f9febee64429ee6ebd17f2905639a3ab0a41b05db", + "native_document_hashes": [ + "f36333b5ab85e5bba08bc5732036c01d6fd06438b30245e15ca684de17664c82", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "c88dab5d9acbb6be26773209f7b6a48b25bfde34e29bded1cd5d17d63eff1961" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "required": true, + "receipt_id": "native_28a13bbf3a9341de94f2a7c1a924e480", + "canonical_state_version": 2, + "canonical_state_revision": "5a401507-02c1-4e8c-872c-cc214b4076e8", + "canonical_manifest_key": "sites/default/markdown/revisions/5a401507-02c1-4e8c-872c-cc214b4076e8.json", + "canonical_persisted_at": "2026-09-10T18:50:04.108Z", + "artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1" + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "operation": { + "id": "build_native_v3_restore", + "siteId": "default", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "restore", + "operation_id": "build_native_v3_restore", + "target_request_sha256": "3b87260f155ba5874928718c6a32551be19d8cc57574c0d642ef5e714dd1e0aa", + "site_id": "default", + "customer_id": "customer_417", + "state": "draft", + "site_allocation": { + "allocation_id": "default", + "canonical_state_revision": "d207bbb4-18f3-489b-820d-c1271fe14129" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "db6717f419bfd848296db4eb193b77ff91db4e0a", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "requested", + "url": null + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "local-native-api-rehearsal", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "7239c4b02419a5a6da0bb28e2c297ea8ce39ae5d554995a4883398e3ed8a2ed1" + }, + "revision_receipt": { + "receipt_id": "native_034982672dcf4ec899496b92fc5c3da8", + "canonical_state_version": 4, + "canonical_state_revision": "d207bbb4-18f3-489b-820d-c1271fe14129", + "canonical_manifest_key": "sites/default/markdown/revisions/d207bbb4-18f3-489b-820d-c1271fe14129.json", + "canonical_persisted_at": "2026-09-10T18:50:09.925Z", + "artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": "native_6056be701de2437c976fbd1a7477b44b", + "canonical_state_version": 3, + "canonical_state_revision": "1f3f895f-9568-444d-8a8c-c2a820f08026", + "canonical_manifest_key": "sites/default/markdown/revisions/1f3f895f-9568-444d-8a8c-c2a820f08026.json", + "canonical_persisted_at": "2026-09-10T18:50:07.213Z", + "artifact_sha256": "29982ab5624346955b0d935f9febee64429ee6ebd17f2905639a3ab0a41b05db", + "native_document_hashes": [ + "f36333b5ab85e5bba08bc5732036c01d6fd06438b30245e15ca684de17664c82", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "c88dab5d9acbb6be26773209f7b6a48b25bfde34e29bded1cd5d17d63eff1961" + ], + "design_system_version": "design-system-1" + }, + "restoration": { + "restore_receipt_id": "restore_b8943805263043a5acde7b9c4b729847", + "restored_from": { + "receipt_id": "native_28a13bbf3a9341de94f2a7c1a924e480", + "canonical_state_version": 2, + "canonical_state_revision": "5a401507-02c1-4e8c-872c-cc214b4076e8", + "canonical_manifest_key": "sites/default/markdown/revisions/5a401507-02c1-4e8c-872c-cc214b4076e8.json", + "canonical_persisted_at": "2026-09-10T18:50:04.108Z", + "artifact_sha256": "b916dfffb1a57060f939af43ba77069a0d33dbe584535fddd8de35596c2e33fb", + "native_document_hashes": [ + "cc42b781c2c2b32c78acd4e758a8c74fd955880cd84689c4a6f0c49edf970a01", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "b93203b5ad218393a954a9f15a1829628d9a2f433ce060e9b1ad120a6ebd5776" + ], + "design_system_version": "design-system-1" + }, + "pre_restore_base": { + "receipt_id": "native_6056be701de2437c976fbd1a7477b44b", + "canonical_state_version": 3, + "canonical_state_revision": "1f3f895f-9568-444d-8a8c-c2a820f08026", + "canonical_manifest_key": "sites/default/markdown/revisions/1f3f895f-9568-444d-8a8c-c2a820f08026.json", + "canonical_persisted_at": "2026-09-10T18:50:07.213Z", + "artifact_sha256": "29982ab5624346955b0d935f9febee64429ee6ebd17f2905639a3ab0a41b05db", + "native_document_hashes": [ + "f36333b5ab85e5bba08bc5732036c01d6fd06438b30245e15ca684de17664c82", + "85c48820d25bbe844b4e0dbc90b76b09bd891c48071169704b76748b8ec33c18", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "c88dab5d9acbb6be26773209f7b6a48b25bfde34e29bded1cd5d17d63eff1961" + ], + "design_system_version": "design-system-1" + } + } + } + }, + "public_heading_observed": "A brighter home", + "idempotent_replay": true, + "poll_verified": true + } + ] +} diff --git a/outputs/bricks-native-api-remote-rehearsal/README.md b/outputs/bricks-native-api-remote-rehearsal/README.md new file mode 100644 index 00000000..a998ed7d --- /dev/null +++ b/outputs/bricks-native-api-remote-rehearsal/README.md @@ -0,0 +1,23 @@ +# Remote native Bricks API fixture rehearsal + +The real Build artifact producer and writer drove the existing isolated Cloudflare Worker, D1 and R2 through native provision, revision, stale-base rejection, restore, idempotent replay, operation polling and authenticated preview observation. This is a fixture API proof, not an AI-generated customer site or licensed editor acceptance. + +- Native source: `2f851aa1` on `codex/bricks-cloudflare-native`. +- Namespace: `native-api`, isolated from the original default canary and customer resources. +- Canonical versions: provision 3, revise 4, restore 5. Restore creates a new revision with the original native content hashes. +- Final preview Worker version: `5e5b9b92-3593-494b-83be-9fda4b7348cc`. +- A fresh Worker version reconstructed the restored content from D1/R2 and rendered desktop and mobile: 19 native elements, two loaded fixture images, no horizontal overflow. Screenshots were inspected. +- Unauthorized preview returns 401, stale preview 409, preview POST 405, and the raw native hostname 404. All use private/no-store responses. +- The original provision receipt remains an immutable historical draft. Later revision and restore receipts advertise the authenticated protected preview. No receipt claims accepted/published or verified client editing. + +The failures during remote qualification were resolved by preparing a verified restore pack outside Worker requests, preserving the immutable runtime package during native mutations, releasing PHP before packing changed canonical state, and disabling OPCache file caching for reconstructed native runtimes. The final source retains normal PHP rendering; no custom HTML substitute, stdout workaround, warning-assisted editor route or license bypass is used. + +`receipt.json` contains exact native operation receipts, source/artifact hashes, actual PHP peak memory and wall observations, access checks, state pointers and the original normal deployment check. Preview aliases do not expose Worker logs, so CPU, resident isolate peak and detailed operation counts remain unmeasured here. The final artifact producer provenance is retained separately from runtime source identity. + +The fixture intentionally uses placeholder pixel media and no bundled fonts. Its sparse screenshots demonstrate native rendering and reconstruction mechanics; they are not customer design evidence. A legitimate agency-owned Bricks license is still absent, so the real visual editor and Editor-role edit/publish/observe/restore/reopen transaction remain unqualified. + +The separate `native-engine` allocation was handed to the parent and BUILD/T for the actual generated-site product flow using final Worker preview version `a4c5d612-4d6a-4599-ad19-3bd7d80b45b3`. It was empty at handoff. Existing customer static preview and public default canonical state were not changed by this rehearsal. + +Validation: package TypeScript build, 144 runtime tests, two package boundary tests, and all 14 focused native/preview tests pass. The full package command was attempted; its canonical MDI seed-regeneration test cannot run because the host has no `php` executable. The 144-test run excludes that single host-PHP check. Actual local PHP-WASM and remote Worker rehearsals ran successfully. Desktop and mobile screenshots are byte-identical before and after the final Worker version upload. + +A second empty allocation, `native-sales-rehearsal`, was subsequently prepared for the actual sales-service rehearsal and handed to its session. Its preview version is `b9d0203a-c61c-45d6-ae05-fce04c87ed69`, with seed version 1/revision `8be31b29-8405-4649-b212-3ea77e4c0660`. Both follow-up namespaces share the disposable canary resources while retaining separate canonical state and credentials. diff --git a/outputs/bricks-native-api-remote-rehearsal/artifact.json b/outputs/bricks-native-api-remote-rehearsal/artifact.json new file mode 100644 index 00000000..c0692004 --- /dev/null +++ b/outputs/bricks-native-api-remote-rehearsal/artifact.json @@ -0,0 +1 @@ +{"assets":[{"alt_text":"Bright and Kind Cleaning","bytes":68,"content_base64":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=","filename":"bright-kind.png","logical_id":"brand-mark","mime_type":"image/png","sha256":"431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460"}],"design_system":{"global_classes":[{"id":"section-shell","name":"Section shell","settings":{"_padding":{"bottom":"clamp(4rem,8vw,9rem)","left":"2rem","right":"2rem","top":"clamp(4rem,8vw,9rem)"}},"sha256":"7cad43bd6e7652de1c992d4d34969f4d6d4e4e80196bead2a1af6e7cd2fd1029"}],"global_variables":[{"id":"space-l","name":"Space L","sha256":"f1737a9fed1c8662fc016baff35f6cad6fd4535da2b70ead09a28edf70a094ac","type":"string","value":"4rem"}],"palette":[{"id":"brand","name":"Kind Blue","value":"#2B6F8A"},{"id":"accent","name":"Warm Gold","value":"#F0B44D"}],"theme_style":{"settings":{},"sha256":"61600e5912f22f7566b4371b4d0e0b89747ffbde3853f6435b63a441c5655a1a"},"typography":{"body_font_family":"Inter","heading_font_family":"Fraunces","root_font_size":"62.5%"},"version":"design-system-1"},"documents":{"pages":[{"elements":[{"children":[{"children":[{"children":[],"id":"hero-heading","name":"heading","settings":{"tag":"h1","text":"A brighter home"}},{"children":[],"id":"hero-copy","name":"text-basic","settings":{"tag":"p","text":"A clean home, handled with care."}},{"children":[],"id":"hero-image","name":"image","settings":{"image":{"asset_ref":"brand-mark","size":"full"}}}],"id":"hero","name":"section","settings":{"_cssGlobalClasses":["section-shell"]}},{"children":[{"children":[],"id":"services-heading","name":"heading","settings":{"tag":"h2","text":"Cleaning services"}},{"children":[],"id":"services-copy","name":"text-basic","settings":{"tag":"p","text":"Recurring, deep, and move-out cleaning."}}],"id":"services","name":"section","settings":{"_cssGlobalClasses":["section-shell"]}},{"children":[{"children":[],"id":"trust-heading","name":"heading","settings":{"tag":"h2","text":"Kind people, careful work"}},{"children":[],"id":"trust-copy","name":"text-basic","settings":{"tag":"p","text":"Local professionals who respect your space."}}],"id":"trust","name":"section","settings":{"_cssGlobalClasses":["section-shell"]}},{"children":[{"children":[],"id":"contact-heading","name":"heading","settings":{"tag":"h2","text":"Request an estimate"}},{"children":[],"id":"contact-copy","name":"text-basic","settings":{"tag":"p","text":"Tell us what you need and we will prepare an estimate."}}],"id":"contact","name":"section","settings":{"_cssGlobalClasses":["section-shell"]}}],"id":"home-main","name":"container","settings":{"tag":"main"}}],"logical_id":"home","sha256":"1ee9a15f673e4952905db62eb8bd6c9d622507283668c9af2e3ccb01047b834e","slug":"home","status":"publish","title":"Bright and Kind Cleaning"}],"templates":[{"conditions":[{"main":"any"}],"elements":[{"children":[{"children":[],"id":"header-logo","name":"image","settings":{"image":{"asset_ref":"brand-mark","size":"full"}}},{"children":[],"id":"header-name","name":"text-basic","settings":{"tag":"span","text":"Bright and Kind Cleaning"}}],"id":"header-shell","name":"section","settings":{"tag":"header"}}],"logical_id":"site-header","sha256":"ce05a59acf2c231e9ec75e8ee9fac41ce32bde043765ea269449f2ccfafebc7b","status":"publish","title":"Site Header","type":"header"},{"conditions":[{"main":"any"}],"elements":[{"children":[{"children":[],"id":"footer-copy","name":"text-basic","settings":{"tag":"p","text":"Bright and Kind Cleaning"}}],"id":"footer-shell","name":"section","settings":{"tag":"footer"}}],"logical_id":"site-footer","sha256":"9e355a90b0b11ac633abae4116c291e59c3cff8776ebce15513197bcfac2758d","status":"publish","title":"Site Footer","type":"footer"}]},"editing":{"acceptance_sequence":["edit","publish","public_observation","restore","reopen"],"granularity":"native_bricks_elements","target_role":{"required_capabilities":["bricks_edit_content"],"role_slug":"editor"}},"evidence_inputs":{"authorized_asset_hashes":["431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460"],"creative_provenance":{"canonicalCommit":"4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21","catalogSha256":"3333333333333333333333333333333333333333333333333333333333333333","guideSha256":"2222222222222222222222222222222222222222222222222222222222222222","references":[{"id":"bright-kind-cleaning","sha256":"4444444444444444444444444444444444444444444444444444444444444444"}],"version":"2026-09-10-v1"},"dossier":{"revision":3,"sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}},"runtime":{"sha256":"a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887","version":"2.4-rc"},"schema":"wp-codebox/bricks-site-artifact/v1","site":{"customer_id":"customer_417","site_id":"native-api","starter_id":"starter_native_service_v1","title":"Bright and Kind Cleaning"}} \ No newline at end of file diff --git a/outputs/bricks-native-api-remote-rehearsal/cold-reopen.json b/outputs/bricks-native-api-remote-rehearsal/cold-reopen.json new file mode 100644 index 00000000..34173f96 --- /dev/null +++ b/outputs/bricks-native-api-remote-rehearsal/cold-reopen.json @@ -0,0 +1,62 @@ +{ + "status": "cold-native-receipt-and-public-render-passed", + "origin": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev", + "restored_revision": { + "receipt_id": "native_1e35568c9f48487b87bbffeeeaf43b31", + "canonical_state_version": 5, + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "canonical_manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "canonical_persisted_at": "2026-09-10T19:50:26.550Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "observations": [ + { + "viewport": "desktop", + "status": 200, + "heading": "A brighter home", + "nativeElements": 19, + "images": [ + { + "src": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_1e35568c9f48487b87bbffeeeaf43b31/wp-content/uploads/2026/09/bright-kind.png", + "loaded": true, + "width": 1 + }, + { + "src": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_1e35568c9f48487b87bbffeeeaf43b31/wp-content/uploads/2026/09/bright-kind.png", + "loaded": true, + "width": 1 + } + ], + "overflow": false, + "screenshot_sha256": "178aff9e141fedc73031db6afa9f86ae12abc62484a69364251276c85c42af7f" + }, + { + "viewport": "mobile", + "status": 200, + "heading": "A brighter home", + "nativeElements": 19, + "images": [ + { + "src": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_1e35568c9f48487b87bbffeeeaf43b31/wp-content/uploads/2026/09/bright-kind.png", + "loaded": true, + "width": 1 + }, + { + "src": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_1e35568c9f48487b87bbffeeeaf43b31/wp-content/uploads/2026/09/bright-kind.png", + "loaded": true, + "width": 1 + } + ], + "overflow": false, + "screenshot_sha256": "48ba45100bcf35061938707ad430093f425d506b5037ce637472ef0baff8ff4a" + } + ], + "qualification": "Programmatic native API proof only; licensed visual editor not run." +} diff --git a/outputs/bricks-native-api-remote-rehearsal/desktop.png b/outputs/bricks-native-api-remote-rehearsal/desktop.png new file mode 100644 index 00000000..e910548a Binary files /dev/null and b/outputs/bricks-native-api-remote-rehearsal/desktop.png differ diff --git a/outputs/bricks-native-api-remote-rehearsal/mobile.png b/outputs/bricks-native-api-remote-rehearsal/mobile.png new file mode 100644 index 00000000..cb9f38c5 Binary files /dev/null and b/outputs/bricks-native-api-remote-rehearsal/mobile.png differ diff --git a/outputs/bricks-native-api-remote-rehearsal/receipt.json b/outputs/bricks-native-api-remote-rehearsal/receipt.json new file mode 100644 index 00000000..c2b46ac7 --- /dev/null +++ b/outputs/bricks-native-api-remote-rehearsal/receipt.json @@ -0,0 +1,930 @@ +{ + "schema": "wp-codebox/native-bricks-remote-api-fixture-proof/v1", + "status": "remote_api_fixture_passed", + "verified_at": "2026-09-10T19:55:24.998069+00:00", + "runtime_source_commit": "2f851aa1", + "artifact_fixture_provenance_commit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "worker": "bricks24-native-canary-20260910", + "api_origin": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev", + "final_preview_version": "5e5b9b92-3593-494b-83be-9fda4b7348cc", + "engine_preview_version": "a4c5d612-4d6a-4599-ad19-3bd7d80b45b3", + "qualification": "Actual PHP/D1/R2 API fixture rehearsal. Not generated-site design acceptance or licensed visual-editor qualification.", + "stale_base_rejected": true, + "operations": [ + { + "action": "provision", + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "build_native_v3_provision", + "idempotency_key": "build_native_v3_provision", + "site_id": "native-api", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": null, + "canonical_state_version": null, + "canonical_state_revision": null, + "canonical_manifest_key": null, + "canonical_persisted_at": null, + "artifact_sha256": null, + "native_document_hashes": null, + "design_system_version": null + }, + "rollback_receipt": { + "required": true, + "receipt_id": null, + "canonical_state_version": null, + "canonical_state_revision": null, + "canonical_manifest_key": null, + "canonical_persisted_at": null, + "artifact_sha256": null, + "native_document_hashes": null, + "design_system_version": null + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "operation": { + "id": "build_native_v3_provision", + "siteId": "native-api", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "provision", + "operation_id": "build_native_v3_provision", + "target_request_sha256": "5a9640af5eb9de5465b7e7ed319aee1a5e2aa33169f4fd6498f1e4d20304ec62", + "site_id": "native-api", + "customer_id": "customer_417", + "state": "draft", + "site_allocation": { + "allocation_id": "native-api", + "canonical_state_revision": "f31e5182-3f6c-425d-8e44-45e9c72582d8" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "requested", + "url": null + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "25138cb2-b0d1-4bb1-9907-9377e6b1750a", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "1e78085f75cc56fa31b00183441ee8c5b7058a870abbd805a193f52b7c5ad916" + }, + "revision_receipt": { + "receipt_id": "native_6301627d54854736949b9426d3800b58", + "canonical_state_version": 3, + "canonical_state_revision": "f31e5182-3f6c-425d-8e44-45e9c72582d8", + "canonical_manifest_key": "sites/native-api/markdown/revisions/f31e5182-3f6c-425d-8e44-45e9c72582d8.json", + "canonical_persisted_at": "2026-09-10T19:23:17.479Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": null, + "canonical_state_version": null, + "canonical_state_revision": null, + "canonical_manifest_key": null, + "canonical_persisted_at": null, + "artifact_sha256": null, + "native_document_hashes": null, + "design_system_version": null + }, + "restoration": null + } + }, + "public_heading_observed": "A brighter home", + "preview_url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_6301627d54854736949b9426d3800b58/", + "idempotent_replay": true, + "poll_verified": true + }, + { + "action": "revise", + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "build_native_v3_revise", + "idempotency_key": "build_native_v3_revise", + "site_id": "native-api", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": "native_6301627d54854736949b9426d3800b58", + "canonical_state_version": 3, + "canonical_state_revision": "f31e5182-3f6c-425d-8e44-45e9c72582d8", + "canonical_manifest_key": "sites/native-api/markdown/revisions/f31e5182-3f6c-425d-8e44-45e9c72582d8.json", + "canonical_persisted_at": "2026-09-10T19:23:17.479Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "required": true, + "receipt_id": "native_6301627d54854736949b9426d3800b58", + "canonical_state_version": 3, + "canonical_state_revision": "f31e5182-3f6c-425d-8e44-45e9c72582d8", + "canonical_manifest_key": "sites/native-api/markdown/revisions/f31e5182-3f6c-425d-8e44-45e9c72582d8.json", + "canonical_persisted_at": "2026-09-10T19:23:17.479Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "operation": { + "id": "build_native_v3_revise", + "siteId": "native-api", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "revise", + "operation_id": "build_native_v3_revise", + "target_request_sha256": "5d354f69d3c235824d253aee2a436aa729f390acccfe732d5d8e6340eae44d8d", + "site_id": "native-api", + "customer_id": "customer_417", + "state": "protected_preview", + "site_allocation": { + "allocation_id": "native-api", + "canonical_state_revision": "45ed8549-d275-4c5a-b5ca-1661df413ae4" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "ee89e54367301805a9c0a281882f72c7b828ab6a38b1c46c1f62370fb8b8e9cb" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "ready", + "url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_29b34efaf058427bb6dfc741058da5b0/" + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "bcfbba11-fbc2-4e3e-9e92-e0e0cc2dce5d", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "8f998bb9181708f5c0691cdc66f64f9f4d7c1d6b18d9b9b79d42ddc2e0884c1a" + }, + "revision_receipt": { + "receipt_id": "native_29b34efaf058427bb6dfc741058da5b0", + "canonical_state_version": 4, + "canonical_state_revision": "45ed8549-d275-4c5a-b5ca-1661df413ae4", + "canonical_manifest_key": "sites/native-api/markdown/revisions/45ed8549-d275-4c5a-b5ca-1661df413ae4.json", + "canonical_persisted_at": "2026-09-10T19:50:00.238Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "ee89e54367301805a9c0a281882f72c7b828ab6a38b1c46c1f62370fb8b8e9cb" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": "native_6301627d54854736949b9426d3800b58", + "canonical_state_version": 3, + "canonical_state_revision": "f31e5182-3f6c-425d-8e44-45e9c72582d8", + "canonical_manifest_key": "sites/native-api/markdown/revisions/f31e5182-3f6c-425d-8e44-45e9c72582d8.json", + "canonical_persisted_at": "2026-09-10T19:23:17.479Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "restoration": null + } + }, + "public_heading_observed": "Native API revised heading", + "preview_url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_29b34efaf058427bb6dfc741058da5b0/", + "idempotent_replay": true, + "poll_verified": true + }, + { + "action": "restore", + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "build_native_v3_restore", + "idempotency_key": "build_native_v3_restore", + "site_id": "native-api", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": "native_29b34efaf058427bb6dfc741058da5b0", + "canonical_state_version": 4, + "canonical_state_revision": "45ed8549-d275-4c5a-b5ca-1661df413ae4", + "canonical_manifest_key": "sites/native-api/markdown/revisions/45ed8549-d275-4c5a-b5ca-1661df413ae4.json", + "canonical_persisted_at": "2026-09-10T19:50:00.238Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "ee89e54367301805a9c0a281882f72c7b828ab6a38b1c46c1f62370fb8b8e9cb" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "required": true, + "receipt_id": "native_6301627d54854736949b9426d3800b58", + "canonical_state_version": 3, + "canonical_state_revision": "f31e5182-3f6c-425d-8e44-45e9c72582d8", + "canonical_manifest_key": "sites/native-api/markdown/revisions/f31e5182-3f6c-425d-8e44-45e9c72582d8.json", + "canonical_persisted_at": "2026-09-10T19:23:17.479Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "operation": { + "id": "build_native_v3_restore", + "siteId": "native-api", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "restore", + "operation_id": "build_native_v3_restore", + "target_request_sha256": "4c74609fd8f4a3c9481731e64c92f50adc9d06ab15bb5dd0d7eaa53fd9a6ef9f", + "site_id": "native-api", + "customer_id": "customer_417", + "state": "protected_preview", + "site_allocation": { + "allocation_id": "native-api", + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "ready", + "url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_1e35568c9f48487b87bbffeeeaf43b31/" + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "bcfbba11-fbc2-4e3e-9e92-e0e0cc2dce5d", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "d48202f775f14c14bd3d69bdfedc1d55d4b5ef27633589473691d6aece9b3aa9" + }, + "revision_receipt": { + "receipt_id": "native_1e35568c9f48487b87bbffeeeaf43b31", + "canonical_state_version": 5, + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "canonical_manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "canonical_persisted_at": "2026-09-10T19:50:26.550Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": "native_29b34efaf058427bb6dfc741058da5b0", + "canonical_state_version": 4, + "canonical_state_revision": "45ed8549-d275-4c5a-b5ca-1661df413ae4", + "canonical_manifest_key": "sites/native-api/markdown/revisions/45ed8549-d275-4c5a-b5ca-1661df413ae4.json", + "canonical_persisted_at": "2026-09-10T19:50:00.238Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "ee89e54367301805a9c0a281882f72c7b828ab6a38b1c46c1f62370fb8b8e9cb" + ], + "design_system_version": "design-system-1" + }, + "restoration": { + "restore_receipt_id": "restore_4f453c3f21a44bada9a75e549636d6b5", + "restored_from": { + "receipt_id": "native_6301627d54854736949b9426d3800b58", + "canonical_state_version": 3, + "canonical_state_revision": "f31e5182-3f6c-425d-8e44-45e9c72582d8", + "canonical_manifest_key": "sites/native-api/markdown/revisions/f31e5182-3f6c-425d-8e44-45e9c72582d8.json", + "canonical_persisted_at": "2026-09-10T19:23:17.479Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "pre_restore_base": { + "receipt_id": "native_29b34efaf058427bb6dfc741058da5b0", + "canonical_state_version": 4, + "canonical_state_revision": "45ed8549-d275-4c5a-b5ca-1661df413ae4", + "canonical_manifest_key": "sites/native-api/markdown/revisions/45ed8549-d275-4c5a-b5ca-1661df413ae4.json", + "canonical_persisted_at": "2026-09-10T19:50:00.238Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "ee89e54367301805a9c0a281882f72c7b828ab6a38b1c46c1f62370fb8b8e9cb" + ], + "design_system_version": "design-system-1" + } + } + } + }, + "public_heading_observed": "A brighter home", + "preview_url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_1e35568c9f48487b87bbffeeeaf43b31/", + "idempotent_replay": true, + "poll_verified": true + } + ], + "resource_measurements": [ + { + "action": "provision", + "sha256": "1e78085f75cc56fa31b00183441ee8c5b7058a870abbd805a193f52b7c5ad916", + "schema": "wp-codebox/native-bricks-resource-observation/v1", + "operation_id": "build_native_v3_provision", + "wall_ms": 15202, + "peak_php_bytes": 57147392, + "unmeasured": [ + "worker_cpu_ms", + "peak_isolate_bytes", + "d1_reads_writes", + "r2_operations", + "subrequests" + ] + }, + { + "action": "revise", + "sha256": "8f998bb9181708f5c0691cdc66f64f9f4d7c1d6b18d9b9b79d42ddc2e0884c1a", + "schema": "wp-codebox/native-bricks-resource-observation/v1", + "operation_id": "build_native_v3_revise", + "wall_ms": 15976, + "peak_php_bytes": 52953088, + "unmeasured": [ + "worker_cpu_ms", + "peak_isolate_bytes", + "d1_reads_writes", + "r2_operations", + "subrequests" + ] + }, + { + "action": "restore", + "sha256": "d48202f775f14c14bd3d69bdfedc1d55d4b5ef27633589473691d6aece9b3aa9", + "schema": "wp-codebox/native-bricks-resource-observation/v1", + "operation_id": "build_native_v3_restore", + "wall_ms": 7791, + "peak_php_bytes": 48758784, + "unmeasured": [ + "worker_cpu_ms", + "peak_isolate_bytes", + "d1_reads_writes", + "r2_operations", + "subrequests" + ] + } + ], + "preview_access": [ + { + "name": "raw-host", + "status": 404, + "cache_control": "private, no-store" + }, + { + "name": "no-credentials", + "status": 401, + "cache_control": "private, no-store" + }, + { + "name": "stale-preview", + "status": 409, + "cache_control": "private, no-store" + }, + { + "name": "write-preview", + "status": 405, + "cache_control": "private, no-store" + } + ], + "cold_preview_phase": { + "stage": "preview-disposed", + "revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "deployment_version": "5e5b9b92-3593-494b-83be-9fda4b7348cc", + "wall_ms": 8693 + }, + "canonical_state": [ + { + "site_id": "default", + "revision": "11378c4e-5194-4f1a-ae87-a6c9f16087c9", + "manifest_key": "sites/default/markdown/revisions/11378c4e-5194-4f1a-ae87-a6c9f16087c9.json", + "persisted_at": "2026-09-10T07:35:35.441Z", + "version": 24 + }, + { + "site_id": "native-api", + "revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "persisted_at": "2026-09-10T19:50:26.550Z", + "version": 5 + }, + { + "site_id": "native-engine", + "revision": "ee5555b1-c849-464e-83c5-064f7d359cce", + "manifest_key": "sites/native-engine/markdown/revisions/ee5555b1-c849-464e-83c5-064f7d359cce.json", + "persisted_at": "2026-09-10T19:08:39.332Z", + "version": 1 + } + ], + "normal_worker_deployment": { + "id": "9b2da5f5-53bc-4938-a294-38d0b1458f35", + "source": "wrangler", + "strategy": "percentage", + "author_email": "mavnewlife@gmail.com", + "annotations": { + "workers/triggered_by": "deployment" + }, + "versions": [ + { + "version_id": "a1e7ea0c-119f-490d-8112-f03aad1a050c", + "percentage": 100 + } + ], + "created_on": "2026-09-10T18:12:13.330467Z" + }, + "remaining": [ + "Agency Bricks license absent; actual licensed editor and Editor-role visual transaction not run.", + "Worker CPU, peak resident isolate bytes and operation counts unavailable on preview aliases; require logged canary deployment.", + "Fixture uses placeholder media and has no bundled fonts; screenshots prove native rendering mechanics, not customer design.", + "Actual product-engine generated-site flow is handed to BUILD/T using a separate empty native-engine allocation." + ], + "validation": { + "package_build": "passed", + "runtime_tests_passed": 144, + "package_boundary_tests_passed": 2, + "focused_native_preview_tests_passed": 14, + "not_run": [ + "Canonical MDI seed generator test requires unavailable host php executable." + ], + "screenshots_byte_identical_across_worker_versions": true + }, + "follow_up_allocations": [ + { + "site_id": "native-engine", + "worker_version": "a4c5d612-4d6a-4599-ad19-3bd7d80b45b3", + "empty_at_handoff": true, + "seed_version": 1, + "seed_revision": "ee5555b1-c849-464e-83c5-064f7d359cce", + "owner_after_handoff": "BUILD/T actual product engine rehearsal" + }, + { + "site_id": "native-sales-rehearsal", + "worker_version": "b9d0203a-c61c-45d6-ae05-fce04c87ed69", + "empty_at_handoff": true, + "seed_version": 1, + "seed_revision": "8be31b29-8405-4649-b212-3ea77e4c0660", + "owner_after_handoff": "OLE BLU sales-service rehearsal" + } + ] +} diff --git a/outputs/bricks-native-media-root-render/README.md b/outputs/bricks-native-media-root-render/README.md new file mode 100644 index 00000000..57a20196 --- /dev/null +++ b/outputs/bricks-native-media-root-render/README.md @@ -0,0 +1,11 @@ +# Native Bricks media rendering on Cloudflare + +The unmodified `packages/runtime-cloudflare/scripts/bricks-render-proof.mjs` passes against the normal canary root on desktop (1440px) and mobile (390px). Both render 42 native elements, four sections, and four loaded images with no horizontal overflow. Both screenshots were visually inspected. + +Commit `4f09dc8a` restores integrity-checked upload hydration, including revisions reconstructed from restore packs. Bricks checks actual file existence/readability/size; WordPress media operations and the mutation inventory also require those files. Browser image requests still stream R2 assets. A sampled original JPEG returned HTTP 200, `image/jpeg`, and `x-wp-codebox-static: r2-upload`. No health-filter override or placeholder file was used. + +Commit `0abe2512` advances the runtime page-cache namespace and applies it to both edge keys and persisted R2 render-cache keys. This prevents the unchanged canonical revision from serving the previous imageless render after a runtime upgrade. Explicit published artifacts and canonical state retain their original keys. + +Deployment: `a1e7ea0c-119f-490d-8112-f03aad1a050c`. The fresh desktop response reports `miss` / `render`; mobile reports `hit` / `edge`. The receipt records screenshot hashes, selected response headers, and test outcomes. + +This is native public-render evidence, not licensed-editor or customer-operability acceptance. The existing navy project section has dark body copy with poor contrast; it remains a native design correction. No authenticated native edit was available in this bounded repair, and no arbitrary canonical metadata change was made. The fixture's approximately 4.1 MB upload set passed; larger media inventories need separate memory qualification. diff --git a/outputs/bricks-native-media-root-render/desktop.png b/outputs/bricks-native-media-root-render/desktop.png new file mode 100644 index 00000000..8bdf081f Binary files /dev/null and b/outputs/bricks-native-media-root-render/desktop.png differ diff --git a/outputs/bricks-native-media-root-render/mobile.png b/outputs/bricks-native-media-root-render/mobile.png new file mode 100644 index 00000000..00677dbc Binary files /dev/null and b/outputs/bricks-native-media-root-render/mobile.png differ diff --git a/outputs/bricks-native-media-root-render/receipt.json b/outputs/bricks-native-media-root-render/receipt.json new file mode 100644 index 00000000..252e1c93 --- /dev/null +++ b/outputs/bricks-native-media-root-render/receipt.json @@ -0,0 +1,200 @@ +{ + "status": "rendered-desktop-and-mobile", + "origin": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev", + "results": [ + { + "name": "desktop", + "width": 1440, + "height": 1000, + "screenshot": "desktop.png", + "responseHeaders": { + "cache-control": "public, max-age=60, s-maxage=31536000", + "content-type": "text/html; charset=UTF-8", + "date": "Thu, 10 Sep 2026 18:12:30 GMT", + "x-powered-by": "PHP/8.5.8", + "x-wp-codebox-page-cache": "miss", + "x-wp-codebox-page-cache-source": "render" + }, + "evidence": { + "title": "Cloudflare WordPress Runtime \u2013 WordPress WebAssembly with canonical Markdown state", + "h1": "Restore the wood you love. Protect it for years.", + "nativeElementCount": 42, + "sections": 4, + "headerVisible": true, + "footerVisible": true, + "viewportWidth": 1440, + "scrollWidth": 1440, + "horizontalOverflow": false, + "heroColumns": "609.719px 518.281px", + "serviceColumns": "386.656px 386.672px 386.656px", + "hero": { + "x": 0, + "y": 145.328125, + "width": 1440, + "height": 827.078125 + }, + "heroCopy": { + "x": 120, + "y": 215.328125, + "width": 609.71875, + "height": 687.078125 + }, + "heroMedia": { + "x": 801.71875, + "y": 218.9375, + "width": 518.28125, + "height": 679.84375 + }, + "serviceGrid": { + "x": 120, + "y": 1313.453125, + "width": 1200, + "height": 225.390625 + }, + "cssVariables": { + "brandNavy": "#0B3A66", + "brandOrange": "#FF6A00", + "spaceM": "clamp(1.6rem, calc(0.0037037037 * (100vw - 36rem) + 1.6rem), 2rem)", + "textM": "clamp(1.6rem, calc(0.0037037037 * (100vw - 36rem) + 1.6rem), 2rem)" + }, + "images": [ + { + "alt": "Stain & Seal Pros", + "complete": true, + "naturalWidth": 1439, + "naturalHeight": 959, + "src": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/wp-content/uploads/2026/09/stain-seal-pros-logo-cfv2.png" + }, + { + "alt": "Freshly restored and stained residential deck", + "complete": true, + "naturalWidth": 1200, + "naturalHeight": 900, + "src": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/wp-content/uploads/2026/09/stain-seal-pros-deck-after-cfv2.jpg" + }, + { + "alt": "Freshly restored and stained residential deck", + "complete": true, + "naturalWidth": 1200, + "naturalHeight": 900, + "src": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/wp-content/uploads/2026/09/stain-seal-pros-deck-after-cfv2.jpg" + }, + { + "alt": "Stain & Seal Pros", + "complete": true, + "naturalWidth": 1439, + "naturalHeight": 959, + "src": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/wp-content/uploads/2026/09/stain-seal-pros-logo-cfv2.png" + } + ] + }, + "screenshotSha256": "33451581a83e43e0820e5c237e4a7ab8c094ac8c05a03bd3b4f5361465c47028" + }, + { + "name": "mobile", + "width": 390, + "height": 844, + "screenshot": "mobile.png", + "responseHeaders": { + "cache-control": "public, max-age=60, s-maxage=31536000", + "cf-cache-status": "HIT", + "content-type": "text/html; charset=UTF-8", + "date": "Thu, 10 Sep 2026 18:12:33 GMT", + "x-powered-by": "PHP/8.5.8", + "x-wp-codebox-page-cache": "hit", + "x-wp-codebox-page-cache-source": "edge" + }, + "evidence": { + "title": "Cloudflare WordPress Runtime \u2013 WordPress WebAssembly with canonical Markdown state", + "h1": "Restore the wood you love. Protect it for years.", + "nativeElementCount": 42, + "sections": 4, + "headerVisible": true, + "footerVisible": true, + "viewportWidth": 390, + "scrollWidth": 390, + "horizontalOverflow": false, + "heroColumns": "342px", + "serviceColumns": "342px", + "hero": { + "x": 0, + "y": 110.65625, + "width": 390, + "height": 859.40625 + }, + "heroCopy": { + "x": 24, + "y": 160.65625, + "width": 342, + "height": 437.640625 + }, + "heroMedia": { + "x": 24, + "y": 617.796875, + "width": 342, + "height": 282.265625 + }, + "serviceGrid": { + "x": 24, + "y": 1199.734375, + "width": 342, + "height": 579.140625 + }, + "cssVariables": { + "brandNavy": "#0B3A66", + "brandOrange": "#FF6A00", + "spaceM": "clamp(1.6rem, calc(0.0037037037 * (100vw - 36rem) + 1.6rem), 2rem)", + "textM": "clamp(1.6rem, calc(0.0037037037 * (100vw - 36rem) + 1.6rem), 2rem)" + }, + "images": [ + { + "alt": "Stain & Seal Pros", + "complete": true, + "naturalWidth": 390, + "naturalHeight": 260, + "src": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/wp-content/uploads/2026/09/stain-seal-pros-logo-cfv2-768x512.png" + }, + { + "alt": "Freshly restored and stained residential deck", + "complete": true, + "naturalWidth": 390, + "naturalHeight": 292, + "src": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/wp-content/uploads/2026/09/stain-seal-pros-deck-after-cfv2-768x576.jpg" + }, + { + "alt": "Freshly restored and stained residential deck", + "complete": true, + "naturalWidth": 390, + "naturalHeight": 292, + "src": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/wp-content/uploads/2026/09/stain-seal-pros-deck-after-cfv2-768x576.jpg" + }, + { + "alt": "Stain & Seal Pros", + "complete": true, + "naturalWidth": 390, + "naturalHeight": 260, + "src": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/wp-content/uploads/2026/09/stain-seal-pros-logo-cfv2-768x512.png" + } + ] + }, + "screenshotSha256": "865a607c8a77a79ef74154130779a244a63bc84b6fc1089a774c50ffa7755c8b" + } + ], + "generatedAt": "2026-09-10T18:12:35.302Z", + "sourceCommit": "0abe2512c43c7eca499ae71b3d689e9bc0e82931", + "deploymentVersion": "a1e7ea0c-119f-490d-8112-f03aad1a050c", + "checks": { + "packageBuild": "passed", + "canaryDryRun": "passed", + "focusedChecksPassed": 28, + "broaderChecksPassed": 37, + "broaderChecksFailed": 1, + "broaderFailure": "Existing seed reproducibility test: php executable unavailable (ENOENT)" + }, + "limits": [ + "Licensed visual editor and edit/publish/restore/reopen not tested by this proof.", + "Desktop is a fresh PHP render; mobile validates the same native HTML through the edge cache.", + "Navy project-section body text has inadequate contrast; no canonical content mutation was attempted.", + "The fixture contains approximately 4.1 MB uploads. The full 64 MB upload allowance is not qualified under Worker memory limits." + ] +} diff --git a/outputs/bricks-native-worker-telemetry/AccountWorkersInvocationsAdaptiveQuantiles.json b/outputs/bricks-native-worker-telemetry/AccountWorkersInvocationsAdaptiveQuantiles.json new file mode 100644 index 00000000..c301ecfc --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/AccountWorkersInvocationsAdaptiveQuantiles.json @@ -0,0 +1,207 @@ +{ + "data": { + "__type": { + "description": "", + "fields": [ + { + "description": "CPU time 25th percentile - microseconds", + "name": "cpuTimeP25" + }, + { + "description": "CPU time 50th percentile - microseconds", + "name": "cpuTimeP50" + }, + { + "description": "CPU time 75th percentile - microseconds", + "name": "cpuTimeP75" + }, + { + "description": "CPU time 90th percentile - microseconds", + "name": "cpuTimeP90" + }, + { + "description": "CPU time 95th percentile - microseconds", + "name": "cpuTimeP95" + }, + { + "description": "CPU time 99th percentile - microseconds", + "name": "cpuTimeP99" + }, + { + "description": "CPU time 99.9th percentile - microseconds", + "name": "cpuTimeP999" + }, + { + "description": "Duration 25th percentile - GB*s", + "name": "durationP25" + }, + { + "description": "Duration 50th percentile - GB*s", + "name": "durationP50" + }, + { + "description": "Duration 75th percentile - GB*s", + "name": "durationP75" + }, + { + "description": "Duration 90th percentile - GB*s", + "name": "durationP90" + }, + { + "description": "Duration 95th percentile - GB*s", + "name": "durationP95" + }, + { + "description": "Duration 99th percentile - GB*s", + "name": "durationP99" + }, + { + "description": "Duration 99.9th percentile - GB*s", + "name": "durationP999" + }, + { + "description": "V8 isolate memory usage 25th percentile across Worker invocations - bytes", + "name": "memoryUsageBytesP25" + }, + { + "description": "V8 isolate memory usage 50th percentile across Worker invocations - bytes", + "name": "memoryUsageBytesP50" + }, + { + "description": "V8 isolate memory usage 75th percentile across Worker invocations - bytes", + "name": "memoryUsageBytesP75" + }, + { + "description": "V8 isolate memory usage 90th percentile across Worker invocations - bytes", + "name": "memoryUsageBytesP90" + }, + { + "description": "V8 isolate memory usage 95th percentile across Worker invocations - bytes", + "name": "memoryUsageBytesP95" + }, + { + "description": "V8 isolate memory usage 99th percentile across Worker invocations - bytes", + "name": "memoryUsageBytesP99" + }, + { + "description": "V8 isolate memory usage 99.9th percentile across Worker invocations - bytes", + "name": "memoryUsageBytesP999" + }, + { + "description": "Request duration 25th percentile - microseconds", + "name": "requestDurationP25" + }, + { + "description": "Request duration 50th percentile - microseconds", + "name": "requestDurationP50" + }, + { + "description": "Request duration 75th percentile - microseconds", + "name": "requestDurationP75" + }, + { + "description": "Request duration 90th percentile - microseconds", + "name": "requestDurationP90" + }, + { + "description": "Request duration 95th percentile - microseconds", + "name": "requestDurationP95" + }, + { + "description": "Request duration 99th percentile - microseconds", + "name": "requestDurationP99" + }, + { + "description": "Request duration 99.9th percentile - microseconds", + "name": "requestDurationP999" + }, + { + "description": "Response body size 25th percentile - bytes", + "name": "responseBodySizeP25" + }, + { + "description": "Response body size 50th percentile - bytes", + "name": "responseBodySizeP50" + }, + { + "description": "Response body size 75th percentile - bytes", + "name": "responseBodySizeP75" + }, + { + "description": "Response body size 90th percentile - bytes", + "name": "responseBodySizeP90" + }, + { + "description": "Response body size 95th percentile - bytes", + "name": "responseBodySizeP95" + }, + { + "description": "Response body size 99th percentile - bytes", + "name": "responseBodySizeP99" + }, + { + "description": "Response body size 99.9th percentile - bytes", + "name": "responseBodySizeP999" + }, + { + "description": "Wall time 25th percentile - microseconds", + "name": "wallTimeP25" + }, + { + "description": "Wall time 50th percentile - microseconds", + "name": "wallTimeP50" + }, + { + "description": "Wall time 75th percentile - microseconds", + "name": "wallTimeP75" + }, + { + "description": "Wall time 90th percentile - microseconds", + "name": "wallTimeP90" + }, + { + "description": "Wall time 95th percentile - microseconds", + "name": "wallTimeP95" + }, + { + "description": "Wall time 99th percentile - microseconds", + "name": "wallTimeP99" + }, + { + "description": "Wall time 99.9th percentile - microseconds", + "name": "wallTimeP999" + }, + { + "description": "WebAssembly linear memory usage 25th percentile across Worker invocations - bytes", + "name": "wasmMemoryBytesP25" + }, + { + "description": "WebAssembly linear memory usage 50th percentile across Worker invocations - bytes", + "name": "wasmMemoryBytesP50" + }, + { + "description": "WebAssembly linear memory usage 75th percentile across Worker invocations - bytes", + "name": "wasmMemoryBytesP75" + }, + { + "description": "WebAssembly linear memory usage 90th percentile across Worker invocations - bytes", + "name": "wasmMemoryBytesP90" + }, + { + "description": "WebAssembly linear memory usage 95th percentile across Worker invocations - bytes", + "name": "wasmMemoryBytesP95" + }, + { + "description": "WebAssembly linear memory usage 99th percentile across Worker invocations - bytes", + "name": "wasmMemoryBytesP99" + }, + { + "description": "WebAssembly linear memory usage 99.9th percentile across Worker invocations - bytes", + "name": "wasmMemoryBytesP999" + } + ], + "name": "AccountWorkersInvocationsAdaptiveQuantiles" + } + }, + "errors": null +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/AccountWorkersInvocationsAdaptiveSum.json b/outputs/bricks-native-worker-telemetry/AccountWorkersInvocationsAdaptiveSum.json new file mode 100644 index 00000000..8bf77f3e --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/AccountWorkersInvocationsAdaptiveSum.json @@ -0,0 +1,47 @@ +{ + "data": { + "__type": { + "description": "", + "fields": [ + { + "description": "Sum of client disconnects", + "name": "clientDisconnects" + }, + { + "description": "Sum of cpu time in us", + "name": "cpuTimeUs" + }, + { + "description": "Sum of Duration - GB*s", + "name": "duration" + }, + { + "description": "Sum of Errors", + "name": "errors" + }, + { + "description": "Sum of Request Duration in microseconds", + "name": "requestDuration" + }, + { + "description": "Sum of Requests", + "name": "requests" + }, + { + "description": "Sum of Response Body Sizes", + "name": "responseBodySize" + }, + { + "description": "Sum of Subrequests", + "name": "subrequests" + }, + { + "description": "Sum of Wall Time", + "name": "wallTime" + } + ], + "name": "AccountWorkersInvocationsAdaptiveSum" + } + }, + "errors": null +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/README.md b/outputs/bricks-native-worker-telemetry/README.md new file mode 100644 index 00000000..2e2a97d7 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/README.md @@ -0,0 +1,50 @@ +# Native Bricks Worker telemetry + +The actual native revision, restoration, protected rendering, replay, and polling passed on the normally deployed disposable Cloudflare Worker. **The memory release gate failed.** Successful HTTP responses do not override the observed isolate memory above 128 MiB. A subsequent [system-allocator improvement](system-allocator/README.md) reduced measured memory and CPU, and passed another real revision/restore transaction; it still does not meet the memory limit. The tables below preserve the original baseline. + +Runtime source: `2f851aa136cb0a20a0bed1c456a6469c12767ba6`; deployed tree: `fd4775e974239de81092356223c78bb4836291db`; Worker version: `048eaf36-9226-4a86-860b-50e19cf558c6`. Only the completed `native-api` fixture was revised, from version 5 to 6 and restored as new monotonic version 7. Default canonical version 24 and the separate engine/sales allocations remained unchanged. Their version-preview deployments were not promoted or replaced. + +## Actual invocation measurements + +These are Cloudflare Wrangler tail CPU and wall measurements in milliseconds, not client stopwatch estimates. All ten captured invocations returned HTTP 200, outcome `ok`, no exceptions, and untruncated logs. + +| Request | Worker CPU ms | Worker wall ms | +| --- | ---: | ---: | +| Default public page, cache hit | 15 | 460 | +| Restored native preview, first measured render | 5,913 | 9,619 | +| Restored native preview, repeat | 5,067 | 8,810 | +| Existing immutable artifact staging | 29 | 976 | +| Actual native revision, version 6 | 9,525 | 20,340 | +| Revised preview with changed heading | 6,859 | 10,712 | +| Restore prior native content as version 7 | 5,840 | 9,989 | +| Restored preview with original heading | 5,440 | 8,675 | +| Idempotent restore replay | 27 | 624 | +| Terminal operation poll | 3 | 129 | + +The configured CPU limit is 300,000 ms. This bounded rehearsal stayed below that limit. Protected preview responses remained `private, no-store`, with exact canonical revision/receipt headers. Both expected headings were observed by the actual Build writer rehearsal. This is a native fixture qualification, not model-generated customer design acceptance or a licensed visual-editor transaction. + +## Memory failure + +Cloudflare GraphQL reported V8 isolate memory of **225,888,500–268,639,500 bytes** for the four native preview invocations. The corresponding WebAssembly linear memory series was **139,198,460 bytes** in every preview row. The restore invocation reported 245,154,200 bytes isolate memory and 115,998,720 bytes WASM memory. Do not add these series together or substitute the PHP allocator peak for either. + +The revision's separately persisted PHP allocator peak was 52,953,088 bytes; restore's was 48,758,784 bytes. The revision's post-execution GraphQL sample was 67,408,360 bytes with zero WASM memory. These samples are not a heap-retention profile or a complete within-request peak measurement. + +[Cloudflare's metric definition](https://developers.cloudflare.com/workers/observability/metrics-and-analytics/#memory-usage) describes whole-isolate memory. Its [memory-limit documentation](https://developers.cloudflare.com/workers/platform/limits/#memory) includes JavaScript and WebAssembly and allows in-flight requests to finish when an over-limit isolate is replaced. Therefore, HTTP 200 does not establish compliance. The exact GraphQL response and introspected metric descriptions are retained here. Resource acceptance remains failed pending a measured reduction; licensed Bricks editor acceptance remains unrun because no legitimate license is configured. + +## Other measured counts and their scope + +For `2026-09-10T20:13:00Z` through `20:18:00Z`, the shared disposable database reported 141 read queries, 10 write queries, 247 rows read, and 16 rows written. The shared bucket reported 46 GetObject and 48 PutObject operations. These are interval totals for the resources, including any operator/other-caller activity; they are not per-invocation attribution. The Worker analytics subrequest series returned zero; this does not mean there were zero D1/R2 calls. + +The restored manifest inventories 147 canonical files (38,450 bytes), 1,605 wp-content entries (76,474,629 bytes, including R2-only browser assets), and one 68-byte fixture image. Its canonical restore pack is 3,051,273 compressed bytes / 13,873,644 decoded bytes over 1,021 files. Logical inventory is not physical bucket usage or live isolate memory. + +## Evidence + +- [Receipt and before/after state](receipt.json) +- [Sanitized exact invocation measurements](invocations.json) +- [GraphQL Worker query](analytics-query.json) and [response](analytics-response.json) +- [GraphQL D1/R2 query](bindings-query.json) and [response](bindings-response.json) +- [Actual Build revision/restore requests and receipts](mutations.json) +- [PHP revision observation](revised-php-measurements.json) and [restore observation](restored-php-measurements.json) +- [Introspected memory field descriptions](AccountWorkersInvocationsAdaptiveQuantiles.json) + +The raw tail, deployment configuration, and credentials remain in the ignored private preparation directory. Only redacted invocation fields and non-secret proof inputs are exported. No customer resources were changed, and no active allocation was cleaned up. diff --git a/outputs/bricks-native-worker-telemetry/allocation-profile/README.md b/outputs/bricks-native-worker-telemetry/allocation-profile/README.md new file mode 100644 index 00000000..413c3697 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/allocation-profile/README.md @@ -0,0 +1,11 @@ +# Local allocation-stage profile + +These local PHP 8.5.8/Wrangler profiles explain the measured allocator change. Temporary diagnostic code inspected the Emscripten heap and filesystem nodes, paused at six explicit stages with the V8 debugger, and captured `Runtime.getHeapUsage`. A temporary MU plugin recorded PHP allocator counters across WordPress hooks. Both instrumentation mechanisms were removed before committing or deploying the runtime change. No remote instrumentation or license bypass was used. + +The original allocator held WASM at 67,108,864 bytes throughout archive extraction, canonical filesystem hydration, and boot. Rendering then requested heap growth to 69,210,112, 81,793,024, 98,570,240, and 119,541,760 bytes. The Emscripten loader's geometric growth yielded 139,198,464 bytes of linear memory. PHP's reported allocator peak was 50,855,936 bytes, illustrating why PHP counters alone did not describe the isolate requirement. + +The streamed JavaScript WordPress ZIP extraction experiment still ended at 139,198,464 WASM bytes and was reverted. It did not justify a production archive-path change. The filesystem figures enumerate accessible Emscripten filesystem nodes and backing buffers; they are not a heap snapshot or an attribution of all remote memory. + +Using PHP's supported `USE_ZEND_ALLOC=0` startup environment instead ended at 80,543,744 WASM bytes with the same native heading and HTTP 200. PHP's [allocator implementation](https://github.com/php/php-src/blob/master/Zend/zend_alloc.c) supports this system-malloc mode. It disables PHP's allocator accounting, so zero-valued PHP counters are reported as unavailable, not as zero consumption. The Worker continues to enforce its actual resource limits. + +The local V8 measurements and remote sampled metrics are different measurement surfaces. No arithmetic combination of these local fields is presented as a remote peak. The [subsequent real Worker evidence](../system-allocator/README.md) remains the resource decision basis. diff --git a/outputs/bricks-native-worker-telemetry/allocation-profile/php-hooks-and-growth.json b/outputs/bricks-native-worker-telemetry/allocation-profile/php-hooks-and-growth.json new file mode 100644 index 00000000..ab8bd250 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/allocation-profile/php-hooks-and-growth.json @@ -0,0 +1,38 @@ +[ + { + "schema": "native-wasm-grow-local", + "requested_bytes": 69210112 + }, + { + "schema": "native-wasm-grow-local", + "requested_bytes": 69210112 + }, + { + "schema": "native-wasm-grow-local", + "requested_bytes": 81793024 + }, + { + "schema": "native-wasm-grow-local", + "requested_bytes": 81793024 + }, + { + "schema": "native-wasm-grow-local", + "requested_bytes": 98570240 + }, + { + "schema": "native-wasm-grow-local", + "requested_bytes": 98570240 + }, + { + "schema": "native-wasm-grow-local", + "requested_bytes": 119541760 + }, + { + "schema": "native-wasm-grow-local", + "requested_bytes": 119541760 + }, + { + "schema": "native-php-profile", + "data": "{\"hook\":\"mu-plugin\",\"php_used\":30550600,\"php_allocated\":31457280,\"php_peak\":31457280}\n{\"hook\":\"plugins_loaded:-999999\",\"php_used\":31423008,\"php_allocated\":33554432,\"php_peak\":33554432}\n{\"hook\":\"plugins_loaded:999999\",\"php_used\":31962648,\"php_allocated\":33554432,\"php_peak\":33554432}\n{\"hook\":\"after_setup_theme:-999999\",\"php_used\":39647008,\"php_allocated\":42467328,\"php_peak\":42467328}\n{\"hook\":\"after_setup_theme:999999\",\"php_used\":39664728,\"php_allocated\":42467328,\"php_peak\":42467328}\n{\"hook\":\"init:-999999\",\"php_used\":39974352,\"php_allocated\":42467328,\"php_peak\":42467328}\n{\"hook\":\"init:999999\",\"php_used\":43933280,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"wp_loaded:-999999\",\"php_used\":43933312,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"wp_loaded:999999\",\"php_used\":43964064,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"wp:-999999\",\"php_used\":43959704,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"wp:999999\",\"php_used\":45782976,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"template_redirect:-999999\",\"php_used\":45795632,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"template_redirect:999999\",\"php_used\":45796288,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"get_header:-999999\",\"php_used\":45798784,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"get_header:999999\",\"php_used\":45799032,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"wp_head:-999999\",\"php_used\":45807592,\"php_allocated\":46661632,\"php_peak\":46661632}\n{\"hook\":\"wp_head:999999\",\"php_used\":48362184,\"php_allocated\":50855936,\"php_peak\":50855936}\n{\"hook\":\"wp_body_open:-999999\",\"php_used\":48357992,\"php_allocated\":50855936,\"php_peak\":50855936}\n{\"hook\":\"wp_body_open:999999\",\"php_used\":48358640,\"php_allocated\":50855936,\"php_peak\":50855936}\n{\"hook\":\"get_footer:-999999\",\"php_used\":48577816,\"php_allocated\":50855936,\"php_peak\":50855936}\n{\"hook\":\"get_footer:999999\",\"php_used\":48577984,\"php_allocated\":50855936,\"php_peak\":50855936}\n{\"hook\":\"wp_footer:-999999\",\"php_used\":48584184,\"php_allocated\":50855936,\"php_peak\":50855936}\n{\"hook\":\"wp_footer:999999\",\"php_used\":48603432,\"php_allocated\":50855936,\"php_peak\":50855936}\n{\"hook\":\"shutdown:-999999\",\"php_used\":48588928,\"php_allocated\":50855936,\"php_peak\":50855936}\n{\"hook\":\"shutdown:999999\",\"php_used\":48589176,\"php_allocated\":50855936,\"php_peak\":50855936}\n" + } +] diff --git a/outputs/bricks-native-worker-telemetry/allocation-profile/refined.json b/outputs/bricks-native-worker-telemetry/allocation-profile/refined.json new file mode 100644 index 00000000..16b782e4 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/allocation-profile/refined.json @@ -0,0 +1,92 @@ +[ + { + "schema": "native-memory-local", + "stage": "before-wordpress-archive", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 18528, + "memfs_typed_backing_bytes": 18528, + "memfs_plain_array_elements": 0, + "files": 11, + "heap": { + "usedSize": 14704452, + "totalSize": 32800768, + "embedderHeapUsedSize": 130816, + "backingStorageSize": 15873999 + } + }, + { + "schema": "native-memory-local", + "stage": "after-wordpress-archive", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 19655508, + "memfs_typed_backing_bytes": 28916123, + "memfs_plain_array_elements": 0, + "files": 521, + "heap": { + "usedSize": 25805512, + "totalSize": 57868288, + "embedderHeapUsedSize": 130816, + "backingStorageSize": 48166030 + } + }, + { + "schema": "native-memory-local", + "stage": "after-native-memfs-hydration", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 34209580, + "memfs_typed_backing_bytes": 43381572, + "memfs_plain_array_elements": 0, + "files": 1586, + "heap": { + "usedSize": 24391236, + "totalSize": 61538304, + "embedderHeapUsedSize": 644896, + "backingStorageSize": 63243306 + } + }, + { + "schema": "native-memory-local", + "stage": "native-ready", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 35039384, + "memfs_typed_backing_bytes": 44626963, + "memfs_plain_array_elements": 0, + "files": 1605, + "heap": { + "usedSize": 25436992, + "totalSize": 60702720, + "embedderHeapUsedSize": 131336, + "backingStorageSize": 64248401 + } + }, + { + "schema": "native-memory-local", + "stage": "before-frontend", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 35040088, + "memfs_typed_backing_bytes": 44627667, + "memfs_plain_array_elements": 0, + "files": 1606, + "heap": { + "usedSize": 19510360, + "totalSize": 60702720, + "embedderHeapUsedSize": 131336, + "backingStorageSize": 64249105 + } + }, + { + "schema": "native-memory-local", + "stage": "after-frontend", + "wasm_bytes": 139198464, + "memfs_logical_bytes": 35079304, + "memfs_typed_backing_bytes": 44975827, + "memfs_plain_array_elements": 0, + "files": 1607, + "heap": { + "usedSize": 22754040, + "totalSize": 60964864, + "embedderHeapUsedSize": 131336, + "backingStorageSize": 64965122 + } + } +] \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/allocation-profile/stream.json b/outputs/bricks-native-worker-telemetry/allocation-profile/stream.json new file mode 100644 index 00000000..ef50cff6 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/allocation-profile/stream.json @@ -0,0 +1,86 @@ +[ + { + "schema": "native-memory-local", + "stage": "before-wordpress-archive", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 18528, + "memfs_backing_bytes": 18528, + "files": 11, + "heap": { + "usedSize": 14619004, + "totalSize": 32538624, + "embedderHeapUsedSize": 130816, + "backingStorageSize": 15873999 + } + }, + { + "schema": "native-memory-local", + "stage": "after-wordpress-archive", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 22978831, + "memfs_backing_bytes": 22978831, + "files": 2119, + "heap": { + "usedSize": 64086080, + "totalSize": 111296512, + "embedderHeapUsedSize": 7350816, + "backingStorageSize": 38871081 + } + }, + { + "schema": "native-memory-local", + "stage": "after-native-memfs-hydration", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 37532903, + "memfs_backing_bytes": 37532903, + "files": 3184, + "heap": { + "usedSize": 28092108, + "totalSize": 110477312, + "embedderHeapUsedSize": 131376, + "backingStorageSize": 54582932 + } + }, + { + "schema": "native-memory-local", + "stage": "native-ready", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 38506382, + "memfs_backing_bytes": 38921969, + "files": 3204, + "heap": { + "usedSize": 34819056, + "totalSize": 111984640, + "embedderHeapUsedSize": 131376, + "backingStorageSize": 58900655 + } + }, + { + "schema": "native-memory-local", + "stage": "before-frontend", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 38507086, + "memfs_backing_bytes": 38922673, + "files": 3205, + "heap": { + "usedSize": 37826480, + "totalSize": 111984640, + "embedderHeapUsedSize": 131376, + "backingStorageSize": 58902063 + } + }, + { + "schema": "native-memory-local", + "stage": "after-frontend", + "wasm_bytes": 139198464, + "memfs_logical_bytes": 38546302, + "memfs_backing_bytes": 39270833, + "files": 3206, + "heap": { + "usedSize": 47902564, + "totalSize": 112771072, + "embedderHeapUsedSize": 131376, + "backingStorageSize": 60932957 + } + } +] \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/allocation-profile/system-alloc.json b/outputs/bricks-native-worker-telemetry/allocation-profile/system-alloc.json new file mode 100644 index 00000000..6777b9a2 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/allocation-profile/system-alloc.json @@ -0,0 +1,92 @@ +[ + { + "schema": "native-memory-local", + "stage": "before-wordpress-archive", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 18528, + "memfs_typed_backing_bytes": 18528, + "memfs_plain_array_elements": 0, + "files": 11, + "heap": { + "usedSize": 14671300, + "totalSize": 32276480, + "embedderHeapUsedSize": 130816, + "backingStorageSize": 15873999 + } + }, + { + "schema": "native-memory-local", + "stage": "after-wordpress-archive", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 19655508, + "memfs_typed_backing_bytes": 28916123, + "memfs_plain_array_elements": 0, + "files": 521, + "heap": { + "usedSize": 19910692, + "totalSize": 57868288, + "embedderHeapUsedSize": 130816, + "backingStorageSize": 48166030 + } + }, + { + "schema": "native-memory-local", + "stage": "after-native-memfs-hydration", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 34209580, + "memfs_typed_backing_bytes": 43381572, + "memfs_plain_array_elements": 0, + "files": 1586, + "heap": { + "usedSize": 18709660, + "totalSize": 61538304, + "embedderHeapUsedSize": 644896, + "backingStorageSize": 63243306 + } + }, + { + "schema": "native-memory-local", + "stage": "native-ready", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 35039384, + "memfs_typed_backing_bytes": 44626963, + "memfs_plain_array_elements": 0, + "files": 1605, + "heap": { + "usedSize": 24210056, + "totalSize": 60964864, + "embedderHeapUsedSize": 131336, + "backingStorageSize": 64249867 + } + }, + { + "schema": "native-memory-local", + "stage": "before-frontend", + "wasm_bytes": 67108864, + "memfs_logical_bytes": 35040088, + "memfs_typed_backing_bytes": 44627667, + "memfs_plain_array_elements": 0, + "files": 1606, + "heap": { + "usedSize": 18193768, + "totalSize": 60964864, + "embedderHeapUsedSize": 131336, + "backingStorageSize": 64250571 + } + }, + { + "schema": "native-memory-local", + "stage": "after-frontend", + "wasm_bytes": 80543744, + "memfs_logical_bytes": 35078779, + "memfs_typed_backing_bytes": 44973779, + "memfs_plain_array_elements": 0, + "files": 1607, + "heap": { + "usedSize": 21660856, + "totalSize": 60964864, + "embedderHeapUsedSize": 131336, + "backingStorageSize": 64964540 + } + } +] \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/analytics-query.json b/outputs/bricks-native-worker-telemetry/analytics-query.json new file mode 100644 index 00000000..013b91a9 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/analytics-query.json @@ -0,0 +1,3 @@ +{ + "query": "query {viewer { accounts(filter:{accountTag:\"c9a6d8fc0e01d908d3d399d12043b2fb\"}) {workersInvocationsAdaptive(limit:100,filter:{scriptName:\"bricks24-native-canary-20260910\",datetime_geq:\"2026-09-10T20:13:00Z\",datetime_leq:\"2026-09-10T20:18:00Z\"}) {sum {subrequests requests errors} quantiles {cpuTimeP50 cpuTimeP99 memoryUsageBytesP50 memoryUsageBytesP99 memoryUsageBytesP999 wasmMemoryBytesP50 wasmMemoryBytesP99 wasmMemoryBytesP999} dimensions {datetime scriptName status}} }}}" +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/analytics-response.json b/outputs/bricks-native-worker-telemetry/analytics-response.json new file mode 100644 index 00000000..b872c94b --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/analytics-response.json @@ -0,0 +1,233 @@ +{ + "data": { + "viewer": { + "accounts": [ + { + "workersInvocationsAdaptive": [ + { + "dimensions": { + "datetime": "2026-09-10T20:13:31Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 5913274, + "cpuTimeP99": 5913274, + "memoryUsageBytesP50": 234798560, + "memoryUsageBytesP99": 234798560, + "memoryUsageBytesP999": 234798560, + "wasmMemoryBytesP50": 139198460, + "wasmMemoryBytesP99": 139198460, + "wasmMemoryBytesP999": 139198460 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:15:45Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 5440020, + "cpuTimeP99": 5440020, + "memoryUsageBytesP50": 268639500, + "memoryUsageBytesP99": 268639500, + "memoryUsageBytesP999": 268639500, + "wasmMemoryBytesP50": 139198460, + "wasmMemoryBytesP99": 139198460, + "wasmMemoryBytesP999": 139198460 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:13:18Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 15540, + "cpuTimeP99": 15540, + "memoryUsageBytesP50": 18987184, + "memoryUsageBytesP99": 18987184, + "memoryUsageBytesP999": 18987184, + "wasmMemoryBytesP50": 0, + "wasmMemoryBytesP99": 0, + "wasmMemoryBytesP999": 0 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:15:24Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 6859056, + "cpuTimeP99": 6859056, + "memoryUsageBytesP50": 225888500, + "memoryUsageBytesP99": 225888500, + "memoryUsageBytesP999": 225888500, + "wasmMemoryBytesP50": 139198460, + "wasmMemoryBytesP99": 139198460, + "wasmMemoryBytesP999": 139198460 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:15:56Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 3740, + "cpuTimeP99": 3740, + "memoryUsageBytesP50": 37322056, + "memoryUsageBytesP99": 37322056, + "memoryUsageBytesP999": 37322056, + "wasmMemoryBytesP50": 0, + "wasmMemoryBytesP99": 0, + "wasmMemoryBytesP999": 0 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:15:35Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 5840581, + "cpuTimeP99": 5840581, + "memoryUsageBytesP50": 245154200, + "memoryUsageBytesP99": 245154200, + "memoryUsageBytesP999": 245154200, + "wasmMemoryBytesP50": 115998720, + "wasmMemoryBytesP99": 115998720, + "wasmMemoryBytesP999": 115998720 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:15:02Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 29415, + "cpuTimeP99": 29415, + "memoryUsageBytesP50": 15858428, + "memoryUsageBytesP99": 15858428, + "memoryUsageBytesP999": 15858428, + "wasmMemoryBytesP50": 0, + "wasmMemoryBytesP99": 0, + "wasmMemoryBytesP999": 0 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:15:03Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 9525801, + "cpuTimeP99": 9525801, + "memoryUsageBytesP50": 67408360, + "memoryUsageBytesP99": 67408360, + "memoryUsageBytesP999": 67408360, + "wasmMemoryBytesP50": 0, + "wasmMemoryBytesP99": 0, + "wasmMemoryBytesP999": 0 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:15:55Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 27277, + "cpuTimeP99": 27277, + "memoryUsageBytesP50": 37321980, + "memoryUsageBytesP99": 37321980, + "memoryUsageBytesP999": 37321980, + "wasmMemoryBytesP50": 0, + "wasmMemoryBytesP99": 0, + "wasmMemoryBytesP999": 0 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:13:41Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 5067888, + "cpuTimeP99": 5067888, + "memoryUsageBytesP50": 268057860, + "memoryUsageBytesP99": 268057860, + "memoryUsageBytesP999": 268057860, + "wasmMemoryBytesP50": 139198460, + "wasmMemoryBytesP99": 139198460, + "wasmMemoryBytesP999": 139198460 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + } + ] + } + ] + } + }, + "errors": null +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/bindings-query.json b/outputs/bricks-native-worker-telemetry/bindings-query.json new file mode 100644 index 00000000..286522b8 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/bindings-query.json @@ -0,0 +1,3 @@ +{ + "query": "query {viewer { accounts(filter:{accountTag:\"c9a6d8fc0e01d908d3d399d12043b2fb\"}) {d1AnalyticsAdaptiveGroups(limit:100,filter:{databaseId:\"fe4d6486-6cd6-4a2e-9542-4f2dd8af7d8a\",datetime_geq:\"2026-09-10T20:13:00Z\",datetime_leq:\"2026-09-10T20:18:00Z\"}) {sum {readQueries writeQueries rowsRead rowsWritten} dimensions {databaseId}} r2OperationsAdaptiveGroups(limit:100,filter:{bucketName:\"bricks24-native-canary-20260910\",datetime_geq:\"2026-09-10T20:13:00Z\",datetime_leq:\"2026-09-10T20:18:00Z\"}) {sum {requests} dimensions {actionType}} }}}" +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/bindings-response.json b/outputs/bricks-native-worker-telemetry/bindings-response.json new file mode 100644 index 00000000..d297c8fc --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/bindings-response.json @@ -0,0 +1,42 @@ +{ + "data": { + "viewer": { + "accounts": [ + { + "d1AnalyticsAdaptiveGroups": [ + { + "dimensions": { + "databaseId": "fe4d6486-6cd6-4a2e-9542-4f2dd8af7d8a" + }, + "sum": { + "readQueries": 191, + "rowsRead": 257, + "rowsWritten": 18, + "writeQueries": 12 + } + } + ], + "r2OperationsAdaptiveGroups": [ + { + "dimensions": { + "actionType": "PutObject" + }, + "sum": { + "requests": 48 + } + }, + { + "dimensions": { + "actionType": "GetObject" + }, + "sum": { + "requests": 46 + } + } + ] + } + ] + } + }, + "errors": null +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/invocations.json b/outputs/bricks-native-worker-telemetry/invocations.json new file mode 100644 index 00000000..01539187 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/invocations.json @@ -0,0 +1,162 @@ +[ + { + "eventTimestamp": 1789071198011, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/", + "method": "GET", + "status": 200, + "cpu_ms": 15, + "wall_ms": 460, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [ + [ + "{\"schema\":\"wp-codebox/cloudflare-runtime-phase-progress/v1\",\"name\":\"page-cache.state.read\",\"durationMs\":124}" + ], + [ + "{\"schema\":\"wp-codebox/cloudflare-runtime-phase-progress/v1\",\"name\":\"page-cache.lookup\",\"durationMs\":9}" + ], + [ + "{\"schema\":\"wp-codebox/cloudflare-runtime-phase-trace/v1\",\"operation\":\"read\",\"runtime\":\"not-used\",\"pageCache\":\"hit\",\"completed\":true,\"totalMs\":133,\"phases\":[{\"name\":\"page-cache.state.read\",\"durationMs\":124},{\"name\":\"page-cache.lookup\",\"durationMs\":9}]}" + ] + ] + }, + { + "eventTimestamp": 1789071211832, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_1e35568c9f48487b87bbffeeeaf43b31/", + "method": "GET", + "status": 200, + "cpu_ms": 5913, + "wall_ms": 9619, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + }, + { + "eventTimestamp": 1789071221509, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_1e35568c9f48487b87bbffeeeaf43b31/", + "method": "GET", + "status": 200, + "cpu_ms": 5067, + "wall_ms": 8810, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + }, + { + "eventTimestamp": 1789071302936, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/artifacts/REDACTED", + "method": "PUT", + "status": 200, + "cpu_ms": 29, + "wall_ms": 976, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + }, + { + "eventTimestamp": 1789071303994, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/revisions", + "method": "POST", + "status": 200, + "cpu_ms": 9525, + "wall_ms": 20340, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + }, + { + "eventTimestamp": 1789071324396, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_f269f86d0666432180b993e609b09a04/", + "method": "GET", + "status": 200, + "cpu_ms": 6859, + "wall_ms": 10712, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + }, + { + "eventTimestamp": 1789071335557, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/restores", + "method": "POST", + "status": 200, + "cpu_ms": 5840, + "wall_ms": 9989, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + }, + { + "eventTimestamp": 1789071345988, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_8a3c03c11ba24262a40041c7d29a6c84/", + "method": "GET", + "status": 200, + "cpu_ms": 5440, + "wall_ms": 8675, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + }, + { + "eventTimestamp": 1789071355130, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/restores", + "method": "POST", + "status": 200, + "cpu_ms": 27, + "wall_ms": 624, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + }, + { + "eventTimestamp": 1789071356249, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/sites/native-api/operations/native_telemetry_20260910_restore", + "method": "GET", + "status": 200, + "cpu_ms": 3, + "wall_ms": 129, + "outcome": "ok", + "scriptVersion": { + "id": "048eaf36-9226-4a86-860b-50e19cf558c6" + }, + "exceptions": [], + "truncated": false, + "phase_logs": [] + } +] diff --git a/outputs/bricks-native-worker-telemetry/mutations.json b/outputs/bricks-native-worker-telemetry/mutations.json new file mode 100644 index 00000000..b11dff72 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/mutations.json @@ -0,0 +1,592 @@ +{ + "origin": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev", + "base": { + "receipt_id": "native_1e35568c9f48487b87bbffeeeaf43b31", + "canonical_state_version": 5, + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "canonical_manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "canonical_persisted_at": "2026-09-10T19:50:26.550Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "revise": { + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "native_telemetry_20260910_revise", + "idempotency_key": "native_telemetry_20260910_revise", + "site_id": "native-api", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": "native_1e35568c9f48487b87bbffeeeaf43b31", + "canonical_state_version": 5, + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "canonical_manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "canonical_persisted_at": "2026-09-10T19:50:26.550Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "required": true, + "receipt_id": "native_1e35568c9f48487b87bbffeeeaf43b31", + "canonical_state_version": 5, + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "canonical_manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "canonical_persisted_at": "2026-09-10T19:50:26.550Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "result": { + "schema": "wp-codebox/provisioning-api/v1", + "operation": { + "id": "native_telemetry_20260910_revise", + "siteId": "native-api", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "revise", + "operation_id": "native_telemetry_20260910_revise", + "target_request_sha256": "045dce4154c038d2fac9ae821cdc967a35eaf90c1e3b8bdbf48942d7a4bb8986", + "site_id": "native-api", + "customer_id": "customer_417", + "state": "protected_preview", + "site_allocation": { + "allocation_id": "native-api", + "canonical_state_revision": "581090cd-2b48-4547-b642-0d7675d3d237" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "dc3f2eb30b731194bf4dbb5b02d1efb590e5d28fc788d54ebfdbf41ef7263bc9" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "ready", + "url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_f269f86d0666432180b993e609b09a04/" + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "048eaf36-9226-4a86-860b-50e19cf558c6", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "3e53626d9e2e3a3a66093dccb5dffc37a82cce3644fc579554dcdadc66d7a8ae" + }, + "revision_receipt": { + "receipt_id": "native_f269f86d0666432180b993e609b09a04", + "canonical_state_version": 6, + "canonical_state_revision": "581090cd-2b48-4547-b642-0d7675d3d237", + "canonical_manifest_key": "sites/native-api/markdown/revisions/581090cd-2b48-4547-b642-0d7675d3d237.json", + "canonical_persisted_at": "2026-09-10T20:15:20.307Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "dc3f2eb30b731194bf4dbb5b02d1efb590e5d28fc788d54ebfdbf41ef7263bc9" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": "native_1e35568c9f48487b87bbffeeeaf43b31", + "canonical_state_version": 5, + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "canonical_manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "canonical_persisted_at": "2026-09-10T19:50:26.550Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "restoration": null + } + } + }, + "observation": { + "path": "/v1/bricks/sites/native-api/previews/native_f269f86d0666432180b993e609b09a04/", + "status": 200, + "wall_ms": 11153, + "heading_observed": true + } + }, + "restore": { + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "native_telemetry_20260910_restore", + "idempotency_key": "native_telemetry_20260910_restore", + "site_id": "native-api", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": "native_f269f86d0666432180b993e609b09a04", + "canonical_state_version": 6, + "canonical_state_revision": "581090cd-2b48-4547-b642-0d7675d3d237", + "canonical_manifest_key": "sites/native-api/markdown/revisions/581090cd-2b48-4547-b642-0d7675d3d237.json", + "canonical_persisted_at": "2026-09-10T20:15:20.307Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "dc3f2eb30b731194bf4dbb5b02d1efb590e5d28fc788d54ebfdbf41ef7263bc9" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "required": true, + "receipt_id": "native_1e35568c9f48487b87bbffeeeaf43b31", + "canonical_state_version": 5, + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "canonical_manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "canonical_persisted_at": "2026-09-10T19:50:26.550Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "result": { + "schema": "wp-codebox/provisioning-api/v1", + "operation": { + "id": "native_telemetry_20260910_restore", + "siteId": "native-api", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "restore", + "operation_id": "native_telemetry_20260910_restore", + "target_request_sha256": "bdcf32a13846280d302579f3857ffc524c7965b17ffd77531b494ca1b2028f8a", + "site_id": "native-api", + "customer_id": "customer_417", + "state": "protected_preview", + "site_allocation": { + "allocation_id": "native-api", + "canonical_state_revision": "3255b220-cff9-465d-b58e-73859918764c" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "ready", + "url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_8a3c03c11ba24262a40041c7d29a6c84/" + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "048eaf36-9226-4a86-860b-50e19cf558c6", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "4af7c90a9b1f82f5ba2d431e66e0ac70ae64d2a606f4db49167e7a2edb4a6adb" + }, + "revision_receipt": { + "receipt_id": "native_8a3c03c11ba24262a40041c7d29a6c84", + "canonical_state_version": 7, + "canonical_state_revision": "3255b220-cff9-465d-b58e-73859918764c", + "canonical_manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "canonical_persisted_at": "2026-09-10T20:15:44.801Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": "native_f269f86d0666432180b993e609b09a04", + "canonical_state_version": 6, + "canonical_state_revision": "581090cd-2b48-4547-b642-0d7675d3d237", + "canonical_manifest_key": "sites/native-api/markdown/revisions/581090cd-2b48-4547-b642-0d7675d3d237.json", + "canonical_persisted_at": "2026-09-10T20:15:20.307Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "dc3f2eb30b731194bf4dbb5b02d1efb590e5d28fc788d54ebfdbf41ef7263bc9" + ], + "design_system_version": "design-system-1" + }, + "restoration": { + "restore_receipt_id": "restore_763a9d72cc534c4f90fea34d3e217e4c", + "restored_from": { + "receipt_id": "native_1e35568c9f48487b87bbffeeeaf43b31", + "canonical_state_version": 5, + "canonical_state_revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "canonical_manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "canonical_persisted_at": "2026-09-10T19:50:26.550Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "pre_restore_base": { + "receipt_id": "native_f269f86d0666432180b993e609b09a04", + "canonical_state_version": 6, + "canonical_state_revision": "581090cd-2b48-4547-b642-0d7675d3d237", + "canonical_manifest_key": "sites/native-api/markdown/revisions/581090cd-2b48-4547-b642-0d7675d3d237.json", + "canonical_persisted_at": "2026-09-10T20:15:20.307Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "dc3f2eb30b731194bf4dbb5b02d1efb590e5d28fc788d54ebfdbf41ef7263bc9" + ], + "design_system_version": "design-system-1" + } + } + } + } + }, + "observation": { + "path": "/v1/bricks/sites/native-api/previews/native_8a3c03c11ba24262a40041c7d29a6c84/", + "status": 200, + "wall_ms": 9142, + "heading_observed": true + } + }, + "replay_verified": true, + "poll_verified": true, + "exchanges": [ + { + "path": "/v1/bricks/artifacts/b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "method": "PUT", + "status": 200, + "wall_ms": 1189 + }, + { + "path": "/v1/bricks/sites/native-api/revisions", + "method": "POST", + "status": 200, + "wall_ms": 20406 + }, + { + "path": "/v1/bricks/sites/native-api/restores", + "method": "POST", + "status": 200, + "wall_ms": 10434 + }, + { + "path": "/v1/bricks/sites/native-api/restores", + "method": "POST", + "status": 200, + "wall_ms": 1108 + }, + { + "path": "/v1/sites/native-api/operations/native_telemetry_20260910_restore", + "method": "GET", + "status": 200, + "wall_ms": 171 + } + ] +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/receipt.json b/outputs/bricks-native-worker-telemetry/receipt.json new file mode 100644 index 00000000..54c1f7ba --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/receipt.json @@ -0,0 +1,136 @@ +{ + "schema": "wp-codebox/native-worker-telemetry-rehearsal/v1", + "observed_at": "2026-09-10T20:20:44.832231+00:00", + "source_commit": "fd4775e974239de81092356223c78bb4836291db", + "runtime_source_commit": "2f851aa136cb0a20a0bed1c456a6469c12767ba6", + "worker": "bricks24-native-canary-20260910", + "deployment_version": "048eaf36-9226-4a86-860b-50e19cf558c6", + "window": { + "start": "2026-09-10T20:13:00Z", + "end": "2026-09-10T20:18:00Z" + }, + "result": { + "functional_rehearsal": "passed", + "cpu_configured_limit_ms": 300000, + "memory_limit_bytes": 134217728, + "memory_qualification": "failed", + "licensed_editor_qualification": "not_run_missing_legitimate_license", + "production_release_qualification": "not_passed" + }, + "state_before": [ + { + "site_id": "default", + "revision": "11378c4e-5194-4f1a-ae87-a6c9f16087c9", + "manifest_key": "sites/default/markdown/revisions/11378c4e-5194-4f1a-ae87-a6c9f16087c9.json", + "persisted_at": "2026-09-10T07:35:35.441Z", + "version": 24, + "lease_token": null + }, + { + "site_id": "native-api", + "revision": "fca9766e-bfcb-4f66-9566-9535c334ce61", + "manifest_key": "sites/native-api/markdown/revisions/fca9766e-bfcb-4f66-9566-9535c334ce61.json", + "persisted_at": "2026-09-10T19:50:26.550Z", + "version": 5, + "lease_token": null + }, + { + "site_id": "native-engine", + "revision": "ee5555b1-c849-464e-83c5-064f7d359cce", + "manifest_key": "sites/native-engine/markdown/revisions/ee5555b1-c849-464e-83c5-064f7d359cce.json", + "persisted_at": "2026-09-10T19:08:39.332Z", + "version": 1, + "lease_token": null + }, + { + "site_id": "native-sales-rehearsal", + "revision": "8be31b29-8405-4649-b212-3ea77e4c0660", + "manifest_key": "sites/native-sales-rehearsal/markdown/revisions/8be31b29-8405-4649-b212-3ea77e4c0660.json", + "persisted_at": "2026-09-10T19:56:30.650Z", + "version": 1, + "lease_token": null + } + ], + "state_after": [ + { + "site_id": "default", + "revision": "11378c4e-5194-4f1a-ae87-a6c9f16087c9", + "manifest_key": "sites/default/markdown/revisions/11378c4e-5194-4f1a-ae87-a6c9f16087c9.json", + "persisted_at": "2026-09-10T07:35:35.441Z", + "version": 24, + "lease_token": null + }, + { + "site_id": "native-api", + "revision": "3255b220-cff9-465d-b58e-73859918764c", + "manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "persisted_at": "2026-09-10T20:15:44.801Z", + "version": 7, + "lease_token": null + }, + { + "site_id": "native-engine", + "revision": "ee5555b1-c849-464e-83c5-064f7d359cce", + "manifest_key": "sites/native-engine/markdown/revisions/ee5555b1-c849-464e-83c5-064f7d359cce.json", + "persisted_at": "2026-09-10T19:08:39.332Z", + "version": 1, + "lease_token": null + }, + { + "site_id": "native-sales-rehearsal", + "revision": "8be31b29-8405-4649-b212-3ea77e4c0660", + "manifest_key": "sites/native-sales-rehearsal/markdown/revisions/8be31b29-8405-4649-b212-3ea77e4c0660.json", + "persisted_at": "2026-09-10T19:56:30.650Z", + "version": 1, + "lease_token": null + } + ], + "preserved_namespaces": [ + "default", + "native-engine", + "native-sales-rehearsal" + ], + "canonical_inventory": { + "files": { + "files": 147, + "bytes": 38450 + }, + "wpContent": { + "files": 1605, + "bytes": 76474629 + }, + "uploads": { + "files": 1, + "bytes": 68 + } + }, + "restore_pack": { + "schema": "wp-codebox/cloudflare-canonical-restore-pack/v1", + "objectKey": "sites/native-api/restore-packs/d1c5bf85422fca6a04c65e1bb11cb3b54e813aa9ece6c99970ed2ec0bbe35e23.zip", + "sha256": "d1c5bf85422fca6a04c65e1bb11cb3b54e813aa9ece6c99970ed2ec0bbe35e23", + "size": 3051273, + "fileCount": 1021, + "decodedBytes": 13873644 + }, + "measurement_notes": [ + "Wrangler tail cpuTime/wallTime are milliseconds per invocation. GraphQL cpuTime quantiles are microseconds.", + "GraphQL memoryUsageBytes is V8 isolate memory; wasmMemoryBytes is WebAssembly linear memory. Neither is PHP allocator peak. The two memory series must not be added.", + "Memory is sampled for the whole isolate, not a per-request retained-heap snapshot; successful HTTP status does not pass the memory requirement.", + "D1/R2 analytics cover the shared disposable database/bucket during the stated window, including operator activity and any other callers; they are not attributed counts for an individual native invocation.", + "GraphQL subrequests=0 is recorded as returned; it is not proof of zero D1/R2 calls.", + "Stored canonical inventory is logical manifest byte accounting, not total physical bucket storage or live isolate retention.", + "No new customer resource, licensed-editor bypass, or cleanup of active allocations occurred." + ], + "files_sha256": { + "revised-php-measurements.json": "98b91ef07269db1e2ea4768c1c3351871469493a77c5c5afadc8c7792a95ca42", + "invocations.json": "236b39e36622d74df699db8e2562907be33dfda7f212de21af0ab677cfec509f", + "AccountWorkersInvocationsAdaptiveSum.json": "61fcfab00a1afebc3d377de118da1903c6e6694be55886a0a99748d2f6ca378c", + "mutations.json": "4fc235bb36a357f0ab96a63e211d5546993c5b3471c5ac94a707c76217261f3d", + "analytics-query.json": "1aa9dad4efbff3e0c1c8b31ca2b7d7d1a085070515ea5f73a8a3346b75b3776b", + "restored-php-measurements.json": "87fb0cb8e6c855793941ef0f3a774eadac41bf72bf30c675eef523a5eaf5821a", + "bindings-query.json": "ddd9474810d829b18d31ef01183fdb8abca82d9b71ee13e18e08068bc396e7a1", + "bindings-response.json": "30c4391d44ea604192c2d1747fd014ca1f04ada14768de4c79322815db947429", + "AccountWorkersInvocationsAdaptiveQuantiles.json": "d1fccdca7a7a2a3075977aba9f414e389e5c2f0e3c4025e670e77cf1a1cd01b4", + "analytics-response.json": "c88e4059e163cdc4def434f5bfa81c1a96754dd3778b0b1ca0b5506f7c8eb184" + } +} diff --git a/outputs/bricks-native-worker-telemetry/restored-php-measurements.json b/outputs/bricks-native-worker-telemetry/restored-php-measurements.json new file mode 100644 index 00000000..f9252453 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/restored-php-measurements.json @@ -0,0 +1,13 @@ +{ + "schema": "wp-codebox/native-bricks-resource-observation/v1", + "operation_id": "native_telemetry_20260910_restore", + "wall_ms": 8317, + "peak_php_bytes": 48758784, + "unmeasured": [ + "worker_cpu_ms", + "peak_isolate_bytes", + "d1_reads_writes", + "r2_operations", + "subrequests" + ] +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/revised-php-measurements.json b/outputs/bricks-native-worker-telemetry/revised-php-measurements.json new file mode 100644 index 00000000..8d9f4664 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/revised-php-measurements.json @@ -0,0 +1,13 @@ +{ + "schema": "wp-codebox/native-bricks-resource-observation/v1", + "operation_id": "native_telemetry_20260910_revise", + "wall_ms": 18975, + "peak_php_bytes": 52953088, + "unmeasured": [ + "worker_cpu_ms", + "peak_isolate_bytes", + "d1_reads_writes", + "r2_operations", + "subrequests" + ] +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/README.md b/outputs/bricks-native-worker-telemetry/system-allocator/README.md new file mode 100644 index 00000000..50e9b65a --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/README.md @@ -0,0 +1,33 @@ +# Native system-allocator follow-up + +Source `0bd5103e`, normal disposable Worker version `7657aa45-c5f2-4ac5-ab0c-783544bc3452`. The native PHP startup uses `USE_ZEND_ALLOC=0`; the default WordPress runtime keeps its original allocator. No vendor ZIP or WASM binary changed. Package build and 14 focused native API/artifact/preview tests passed. + +The actual Build writer repeated a native revision and restore, rendered both expected headings, verified replay and terminal polling, and restored the previous content as **new canonical version 9**. All ten captured invocations returned HTTP 200, outcome `ok`, and no exceptions. Default canonical version 24 and the engine/sales allocations remained unchanged. The public default page still returned 200. Active version-preview aliases were not upgraded. + +| Request | Worker CPU ms | Worker wall ms | +| --- | ---: | ---: | +| Protected preview, first render | 3,307 | 6,337 | +| Protected preview, repeat | 3,037 | 4,918 | +| Immutable artifact staging | 6 | 202 | +| Actual native revision, version 8 | 4,458 | 12,921 | +| Revised protected preview | 3,065 | 5,478 | +| Restore native content as version 9 | 3,118 | 6,269 | +| Restored protected preview | 3,134 | 5,622 | +| Restore replay | 5 | 157 | +| Operation poll | 2 | 104 | +| Default public cache hit | 10 | 375 | + +**Memory qualification still fails.** The four rendered previews report 184,273,280–192,740,510 bytes of V8 isolate memory, including 80,543,740 bytes of WebAssembly linear memory. The original allocator's corresponding preview ranges were 225,888,500–268,639,500 bytes total and 139,198,460 bytes WASM. This is a measured improvement, not compliance with the 134,217,728-byte limit. GraphQL may group multiple invocations into a timestamp row; exact per-request CPU/wall comes from the retained tail events. Its samples are not a full execution-peak or retained-object profile. + +The PHP resource receipts now explicitly identify the system allocator and record `peak_php_bytes: null`. PHP's internal counter is unavailable in this mode. Remote Worker CPU, wall, and memory are preserved separately rather than being attributed to those PHP receipts retroactively. + +The query interval's shared-resource totals are 100 D1 read queries, 12 write queries, 270 rows read, 18 rows written, 35 R2 GetObject operations and 39 PutObject operations. They include operator/other-caller activity and are not exact attribution to a single request. The GraphQL subrequest series returned zero; the positive binding counts must not be replaced by that field. The exported query contains the exact observation window. + +- [Receipt and exact before/after pointers](receipt.json) +- [Actual Build requests, operation receipts, and observations](mutations.json) +- [Ten sanitized invocation events](invocations.json) +- [GraphQL memory/CPU response](analytics-response.json) and [query](analytics-query.json) +- [Shared D1/R2 response](bindings-response.json) and [query](bindings-query.json) +- [Revision PHP observation](revised-php-measurements.json) and [restore PHP observation](restored-php-measurements.json) + +All local profiler and tail processes were stopped. Further memory reduction and the licensed visual-editor transaction remain outstanding. This native fixture rehearsal is not proof of a generated customer site or production readiness. diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/analytics-query.json b/outputs/bricks-native-worker-telemetry/system-allocator/analytics-query.json new file mode 100644 index 00000000..ae73f668 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/analytics-query.json @@ -0,0 +1,3 @@ +{ + "query": "query {viewer { accounts(filter:{accountTag:\"c9a6d8fc0e01d908d3d399d12043b2fb\"}) {workersInvocationsAdaptive(limit:100,filter:{scriptName:\"bricks24-native-canary-20260910\",datetime_geq:\"2026-09-10T20:32:00Z\",datetime_leq:\"2026-09-10T20:35:54.289808Z\"}) {sum {subrequests requests errors} quantiles {cpuTimeP50 cpuTimeP99 memoryUsageBytesP50 memoryUsageBytesP99 memoryUsageBytesP999 wasmMemoryBytesP50 wasmMemoryBytesP99 wasmMemoryBytesP999} dimensions {datetime scriptName status}} }}}" +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/analytics-response.json b/outputs/bricks-native-worker-telemetry/system-allocator/analytics-response.json new file mode 100644 index 00000000..5913ab67 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/analytics-response.json @@ -0,0 +1,167 @@ +{ + "data": { + "viewer": { + "accounts": [ + { + "workersInvocationsAdaptive": [ + { + "dimensions": { + "datetime": "2026-09-10T20:33:34Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 3065659, + "cpuTimeP99": 3065659, + "memoryUsageBytesP50": 184273280, + "memoryUsageBytesP99": 184273280, + "memoryUsageBytesP999": 184273280, + "wasmMemoryBytesP50": 80543740, + "wasmMemoryBytesP99": 80543740, + "wasmMemoryBytesP999": 80543740 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:33:21Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 6599, + "cpuTimeP99": 4458251, + "memoryUsageBytesP50": 16907004, + "memoryUsageBytesP99": 64425904, + "memoryUsageBytesP999": 64425904, + "wasmMemoryBytesP50": 0, + "wasmMemoryBytesP99": 0, + "wasmMemoryBytesP999": 0 + }, + "sum": { + "errors": 0, + "requests": 2, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:33:46Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 3134844, + "cpuTimeP99": 3134844, + "memoryUsageBytesP50": 186556240, + "memoryUsageBytesP99": 186556240, + "memoryUsageBytesP999": 186556240, + "wasmMemoryBytesP50": 80543740, + "wasmMemoryBytesP99": 80543740, + "wasmMemoryBytesP999": 80543740 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:34:43Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 10424, + "cpuTimeP99": 10424, + "memoryUsageBytesP50": 18987184, + "memoryUsageBytesP99": 18987184, + "memoryUsageBytesP999": 18987184, + "wasmMemoryBytesP50": 0, + "wasmMemoryBytesP99": 0, + "wasmMemoryBytesP999": 0 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:33:52Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 2053, + "cpuTimeP99": 5313, + "memoryUsageBytesP50": 186586000, + "memoryUsageBytesP99": 186586080, + "memoryUsageBytesP999": 186586080, + "wasmMemoryBytesP50": 80543740, + "wasmMemoryBytesP99": 80543740, + "wasmMemoryBytesP999": 80543740 + }, + "sum": { + "errors": 0, + "requests": 2, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:32:06Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 3307978, + "cpuTimeP99": 3307978, + "memoryUsageBytesP50": 192740510, + "memoryUsageBytesP99": 192740510, + "memoryUsageBytesP999": 192740510, + "wasmMemoryBytesP50": 80543740, + "wasmMemoryBytesP99": 80543740, + "wasmMemoryBytesP999": 80543740 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + }, + { + "dimensions": { + "datetime": "2026-09-10T20:32:13Z", + "scriptName": "bricks24-native-canary-20260910", + "status": "success" + }, + "quantiles": { + "cpuTimeP50": 3037437, + "cpuTimeP99": 3037437, + "memoryUsageBytesP50": 192734060, + "memoryUsageBytesP99": 192734060, + "memoryUsageBytesP999": 192734060, + "wasmMemoryBytesP50": 80543740, + "wasmMemoryBytesP99": 80543740, + "wasmMemoryBytesP999": 80543740 + }, + "sum": { + "errors": 0, + "requests": 1, + "subrequests": 0 + } + } + ] + } + ] + } + }, + "errors": null +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/bindings-query.json b/outputs/bricks-native-worker-telemetry/system-allocator/bindings-query.json new file mode 100644 index 00000000..afd9aa79 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/bindings-query.json @@ -0,0 +1,3 @@ +{ + "query": "query {viewer { accounts(filter:{accountTag:\"c9a6d8fc0e01d908d3d399d12043b2fb\"}) {d1AnalyticsAdaptiveGroups(limit:100,filter:{databaseId:\"fe4d6486-6cd6-4a2e-9542-4f2dd8af7d8a\",datetime_geq:\"2026-09-10T20:32:00Z\",datetime_leq:\"2026-09-10T20:35:54.289808Z\"}) {sum {readQueries writeQueries rowsRead rowsWritten} dimensions {databaseId}} r2OperationsAdaptiveGroups(limit:100,filter:{bucketName:\"bricks24-native-canary-20260910\",datetime_geq:\"2026-09-10T20:32:00Z\",datetime_leq:\"2026-09-10T20:35:54.289808Z\"}) {sum {requests} dimensions {actionType}} }}}" +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/bindings-response.json b/outputs/bricks-native-worker-telemetry/system-allocator/bindings-response.json new file mode 100644 index 00000000..f827dd1d --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/bindings-response.json @@ -0,0 +1,42 @@ +{ + "data": { + "viewer": { + "accounts": [ + { + "d1AnalyticsAdaptiveGroups": [ + { + "dimensions": { + "databaseId": "fe4d6486-6cd6-4a2e-9542-4f2dd8af7d8a" + }, + "sum": { + "readQueries": 100, + "rowsRead": 270, + "rowsWritten": 18, + "writeQueries": 12 + } + } + ], + "r2OperationsAdaptiveGroups": [ + { + "dimensions": { + "actionType": "PutObject" + }, + "sum": { + "requests": 39 + } + }, + { + "dimensions": { + "actionType": "GetObject" + }, + "sum": { + "requests": 35 + } + } + ] + } + ] + } + }, + "errors": null +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/invocations.json b/outputs/bricks-native-worker-telemetry/system-allocator/invocations.json new file mode 100644 index 00000000..ead9bcc0 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/invocations.json @@ -0,0 +1,142 @@ +[ + { + "eventTimestamp": 1789072326389, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_8a3c03c11ba24262a40041c7d29a6c84/", + "method": "GET", + "status": 200, + "cpu_ms": 3307, + "wall_ms": 6337, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072333069, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_8a3c03c11ba24262a40041c7d29a6c84/", + "method": "GET", + "status": 200, + "cpu_ms": 3037, + "wall_ms": 4918, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072401523, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/artifacts/REDACTED", + "method": "PUT", + "status": 200, + "cpu_ms": 6, + "wall_ms": 202, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072401770, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/revisions", + "method": "POST", + "status": 200, + "cpu_ms": 4458, + "wall_ms": 12921, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072414744, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_f5ffc62fb8b244bc9586a8d762dbdfaa/", + "method": "GET", + "status": 200, + "cpu_ms": 3065, + "wall_ms": 5478, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072420284, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/restores", + "method": "POST", + "status": 200, + "cpu_ms": 3118, + "wall_ms": 6269, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072426593, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_f0efac00d6e14f78aa657bb3fae5ec94/", + "method": "GET", + "status": 200, + "cpu_ms": 3134, + "wall_ms": 5622, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072432260, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/restores", + "method": "POST", + "status": 200, + "cpu_ms": 5, + "wall_ms": 157, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072432451, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/sites/native-api/operations/native_allocator_20260910_restore", + "method": "GET", + "status": 200, + "cpu_ms": 2, + "wall_ms": 104, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + }, + { + "eventTimestamp": 1789072483709, + "url": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev/", + "method": "GET", + "status": 200, + "cpu_ms": 10, + "wall_ms": 375, + "outcome": "ok", + "scriptVersion": { + "id": "7657aa45-c5f2-4ac5-ab0c-783544bc3452" + }, + "exceptions": [], + "truncated": false + } +] diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/mutations.json b/outputs/bricks-native-worker-telemetry/system-allocator/mutations.json new file mode 100644 index 00000000..10c23289 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/mutations.json @@ -0,0 +1,592 @@ +{ + "origin": "https://bricks24-native-canary-20260910.mavnewlife.workers.dev", + "base": { + "receipt_id": "native_8a3c03c11ba24262a40041c7d29a6c84", + "canonical_state_version": 7, + "canonical_state_revision": "3255b220-cff9-465d-b58e-73859918764c", + "canonical_manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "canonical_persisted_at": "2026-09-10T20:15:44.801Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "revise": { + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "native_allocator_20260910_revise", + "idempotency_key": "native_allocator_20260910_revise", + "site_id": "native-api", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": "native_8a3c03c11ba24262a40041c7d29a6c84", + "canonical_state_version": 7, + "canonical_state_revision": "3255b220-cff9-465d-b58e-73859918764c", + "canonical_manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "canonical_persisted_at": "2026-09-10T20:15:44.801Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "required": true, + "receipt_id": "native_8a3c03c11ba24262a40041c7d29a6c84", + "canonical_state_version": 7, + "canonical_state_revision": "3255b220-cff9-465d-b58e-73859918764c", + "canonical_manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "canonical_persisted_at": "2026-09-10T20:15:44.801Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "result": { + "schema": "wp-codebox/provisioning-api/v1", + "operation": { + "id": "native_allocator_20260910_revise", + "siteId": "native-api", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "revise", + "operation_id": "native_allocator_20260910_revise", + "target_request_sha256": "98354f783b6fe881d0115ab75dd9d37f8ab2994f521704116bad8538fe5cea86", + "site_id": "native-api", + "customer_id": "customer_417", + "state": "protected_preview", + "site_allocation": { + "allocation_id": "native-api", + "canonical_state_revision": "868780d7-4e8e-41e2-b6f2-d9f62b1e157b" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "6420605d3072252306cbe529de1a3817adc01991f83ff31d69b24108139d44d8" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "ready", + "url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_f5ffc62fb8b244bc9586a8d762dbdfaa/" + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "7657aa45-c5f2-4ac5-ab0c-783544bc3452", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "2e11396bc30b238df89c99bf0620d15373a49822788e56095136588555b5f4bc" + }, + "revision_receipt": { + "receipt_id": "native_f5ffc62fb8b244bc9586a8d762dbdfaa", + "canonical_state_version": 8, + "canonical_state_revision": "868780d7-4e8e-41e2-b6f2-d9f62b1e157b", + "canonical_manifest_key": "sites/native-api/markdown/revisions/868780d7-4e8e-41e2-b6f2-d9f62b1e157b.json", + "canonical_persisted_at": "2026-09-10T20:33:32.015Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "6420605d3072252306cbe529de1a3817adc01991f83ff31d69b24108139d44d8" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": "native_8a3c03c11ba24262a40041c7d29a6c84", + "canonical_state_version": 7, + "canonical_state_revision": "3255b220-cff9-465d-b58e-73859918764c", + "canonical_manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "canonical_persisted_at": "2026-09-10T20:15:44.801Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "restoration": null + } + } + }, + "observation": { + "path": "/v1/bricks/sites/native-api/previews/native_f5ffc62fb8b244bc9586a8d762dbdfaa/", + "status": 200, + "wall_ms": 5524, + "heading_observed": true + } + }, + "restore": { + "request": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "operation_id": "native_allocator_20260910_restore", + "idempotency_key": "native_allocator_20260910_restore", + "site_id": "native-api", + "customer_id": "customer_417", + "bricks_artifact": { + "version": "wp-codebox/bricks-site-artifact/v1", + "sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9" + }, + "runtime_artifact": { + "version": "2.4-rc", + "sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" + }, + "dossier": { + "revision": 3, + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "creative_provenance": { + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ], + "version": "2026-09-10-v1" + }, + "authorized_assets": [ + { + "sha256": "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460", + "usage_status": "approved" + } + ], + "starter_id": "starter_native_service_v1", + "editing": { + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "granularity": "native_bricks_elements", + "target_role": { + "required_capabilities": [ + "bricks_edit_content" + ], + "role_slug": "editor" + } + }, + "license_secret_ref": "BRICKS_LICENSE_KEY", + "revision_receipt": { + "receipt_id": "native_f5ffc62fb8b244bc9586a8d762dbdfaa", + "canonical_state_version": 8, + "canonical_state_revision": "868780d7-4e8e-41e2-b6f2-d9f62b1e157b", + "canonical_manifest_key": "sites/native-api/markdown/revisions/868780d7-4e8e-41e2-b6f2-d9f62b1e157b.json", + "canonical_persisted_at": "2026-09-10T20:33:32.015Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "6420605d3072252306cbe529de1a3817adc01991f83ff31d69b24108139d44d8" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "required": true, + "receipt_id": "native_8a3c03c11ba24262a40041c7d29a6c84", + "canonical_state_version": 7, + "canonical_state_revision": "3255b220-cff9-465d-b58e-73859918764c", + "canonical_manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "canonical_persisted_at": "2026-09-10T20:15:44.801Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "protected_preview": { + "state": "requested", + "access": "agency_managed", + "url": null + } + }, + "result": { + "schema": "wp-codebox/provisioning-api/v1", + "operation": { + "id": "native_allocator_20260910_restore", + "siteId": "native-api", + "state": "succeeded", + "stage": "prepared-allocation-native-documents-committed", + "progress": 100, + "retryAt": null, + "error": null, + "receipt": { + "schema_version": 1, + "target_runtime": "wordpress_bricks_cloudflare_v1", + "action": "restore", + "operation_id": "native_allocator_20260910_restore", + "target_request_sha256": "a2dab61b54ac1d137797a835c5545702a606f175f877b2ab183583a39e847eaf", + "site_id": "native-api", + "customer_id": "customer_417", + "state": "protected_preview", + "site_allocation": { + "allocation_id": "native-api", + "canonical_state_revision": "be767628-161b-4f22-b93c-094aaf79b1d0" + }, + "native_records": { + "page_ids": [ + 2 + ], + "template_ids": [ + 4, + 6 + ], + "media_ids": [ + 1 + ] + }, + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1", + "bricks_artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "dossier_hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "creative_provenance": { + "version": "2026-09-10-v1", + "canonicalCommit": "4a70be1400cbf3e32e05a7cda6a4ca35d28f1c21", + "guideSha256": "2222222222222222222222222222222222222222222222222222222222222222", + "catalogSha256": "3333333333333333333333333333333333333333333333333333333333333333", + "references": [ + { + "id": "bright-kind-cleaning", + "sha256": "4444444444444444444444444444444444444444444444444444444444444444" + } + ] + }, + "authorized_asset_hashes": [ + "431ced6916a2a21a156e38701afe55bbd7f88969fbbfc56d7fe099d47f265460" + ], + "target_role": { + "role_slug": "editor", + "required_capabilities": [ + "bricks_edit_content" + ], + "editability_verified": false + }, + "acceptance_sequence": [ + "edit", + "publish", + "public_observation", + "restore", + "reopen" + ], + "protected_preview": { + "state": "ready", + "url": "https://native-api-bricks24-native-canary-20260910.mavnewlife.workers.dev/v1/bricks/sites/native-api/previews/native_f0efac00d6e14f78aa657bb3fae5ec94/" + }, + "acceptance_results": [ + { + "step": "edit", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "publish", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "public_observation", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "restore", + "status": "not_run", + "evidence_sha256": null + }, + { + "step": "reopen", + "status": "not_run", + "evidence_sha256": null + } + ], + "acceptance_transaction_sha256": null, + "deployment": { + "version": "7657aa45-c5f2-4ac5-ab0c-783544bc3452", + "runtime_artifact_version": "2.4-rc", + "runtime_artifact_sha256": "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "artifact_hashes": [ + "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887", + "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9" + ] + }, + "evidence": { + "desktop_sha256": null, + "mobile_sha256": null, + "client_edit_transaction_sha256": null, + "resource_measurements_sha256": "dc5b4c9242c36c41deffdb5db4586e49407c1c570656c25cfc4ccb10bbaea25c" + }, + "revision_receipt": { + "receipt_id": "native_f0efac00d6e14f78aa657bb3fae5ec94", + "canonical_state_version": 9, + "canonical_state_revision": "be767628-161b-4f22-b93c-094aaf79b1d0", + "canonical_manifest_key": "sites/native-api/markdown/revisions/be767628-161b-4f22-b93c-094aaf79b1d0.json", + "canonical_persisted_at": "2026-09-10T20:33:44.683Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "rollback_receipt": { + "receipt_id": "native_f5ffc62fb8b244bc9586a8d762dbdfaa", + "canonical_state_version": 8, + "canonical_state_revision": "868780d7-4e8e-41e2-b6f2-d9f62b1e157b", + "canonical_manifest_key": "sites/native-api/markdown/revisions/868780d7-4e8e-41e2-b6f2-d9f62b1e157b.json", + "canonical_persisted_at": "2026-09-10T20:33:32.015Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "6420605d3072252306cbe529de1a3817adc01991f83ff31d69b24108139d44d8" + ], + "design_system_version": "design-system-1" + }, + "restoration": { + "restore_receipt_id": "restore_f7e5405a4d484bd0aae616b011652154", + "restored_from": { + "receipt_id": "native_8a3c03c11ba24262a40041c7d29a6c84", + "canonical_state_version": 7, + "canonical_state_revision": "3255b220-cff9-465d-b58e-73859918764c", + "canonical_manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "canonical_persisted_at": "2026-09-10T20:15:44.801Z", + "artifact_sha256": "e5544bd61426809a13ee5f6af8e99652fa5f58019d2ed55aaa305931733debd9", + "native_document_hashes": [ + "de4a6efd1bc3347cfcef1a74f3533c973e9820a9b704a92acba9837f7139ae67", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "15608f7028a1d7865c1128f80a100b4b299316e21f0e555f81b3bdd061c15930" + ], + "design_system_version": "design-system-1" + }, + "pre_restore_base": { + "receipt_id": "native_f5ffc62fb8b244bc9586a8d762dbdfaa", + "canonical_state_version": 8, + "canonical_state_revision": "868780d7-4e8e-41e2-b6f2-d9f62b1e157b", + "canonical_manifest_key": "sites/native-api/markdown/revisions/868780d7-4e8e-41e2-b6f2-d9f62b1e157b.json", + "canonical_persisted_at": "2026-09-10T20:33:32.015Z", + "artifact_sha256": "b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "native_document_hashes": [ + "0d1d5858922606e8fecfe329cbebafd30f6411f8d7be69f3bec111ef2339c27d", + "0de2fd2af8bd4b4dc97dfba98f36bcb07ebeb596801c50cadec083eac2df17b2", + "0fdaf3d80ddab36110aa06e58707728bf64305b019baa143f33a75f7bb192bb0", + "6420605d3072252306cbe529de1a3817adc01991f83ff31d69b24108139d44d8" + ], + "design_system_version": "design-system-1" + } + } + } + } + }, + "observation": { + "path": "/v1/bricks/sites/native-api/previews/native_f0efac00d6e14f78aa657bb3fae5ec94/", + "status": 200, + "wall_ms": 5656, + "heading_observed": true + } + }, + "replay_verified": true, + "poll_verified": true, + "exchanges": [ + { + "path": "/v1/bricks/artifacts/b084e18fb8010bd8d5ff23b937f42491e3d5fa7a803860c47be59028663d5c36", + "method": "PUT", + "status": 200, + "wall_ms": 321 + }, + { + "path": "/v1/bricks/sites/native-api/revisions", + "method": "POST", + "status": 200, + "wall_ms": 12970 + }, + { + "path": "/v1/bricks/sites/native-api/restores", + "method": "POST", + "status": 200, + "wall_ms": 6323 + }, + { + "path": "/v1/bricks/sites/native-api/restores", + "method": "POST", + "status": 200, + "wall_ms": 195 + }, + { + "path": "/v1/sites/native-api/operations/native_allocator_20260910_restore", + "method": "GET", + "status": 200, + "wall_ms": 137 + } + ] +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/receipt.json b/outputs/bricks-native-worker-telemetry/system-allocator/receipt.json new file mode 100644 index 00000000..0543181d --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/receipt.json @@ -0,0 +1,94 @@ +{ + "schema": "wp-codebox/native-worker-system-allocator-rehearsal/v1", + "source_commit": "0bd5103e", + "worker_version": "7657aa45-c5f2-4ac5-ab0c-783544bc3452", + "observed_at": "2026-09-10T20:36:30.540168+00:00", + "functional_result": "passed", + "memory_qualification": "failed", + "memory_limit_bytes": 134217728, + "php_allocator": "system", + "php_memory_counter": "unavailable", + "state_before": [ + { + "site_id": "default", + "revision": "11378c4e-5194-4f1a-ae87-a6c9f16087c9", + "manifest_key": "sites/default/markdown/revisions/11378c4e-5194-4f1a-ae87-a6c9f16087c9.json", + "persisted_at": "2026-09-10T07:35:35.441Z", + "version": 24, + "lease_token": null + }, + { + "site_id": "native-api", + "revision": "3255b220-cff9-465d-b58e-73859918764c", + "manifest_key": "sites/native-api/markdown/revisions/3255b220-cff9-465d-b58e-73859918764c.json", + "persisted_at": "2026-09-10T20:15:44.801Z", + "version": 7, + "lease_token": null + }, + { + "site_id": "native-engine", + "revision": "ee5555b1-c849-464e-83c5-064f7d359cce", + "manifest_key": "sites/native-engine/markdown/revisions/ee5555b1-c849-464e-83c5-064f7d359cce.json", + "persisted_at": "2026-09-10T19:08:39.332Z", + "version": 1, + "lease_token": null + }, + { + "site_id": "native-sales-rehearsal", + "revision": "8be31b29-8405-4649-b212-3ea77e4c0660", + "manifest_key": "sites/native-sales-rehearsal/markdown/revisions/8be31b29-8405-4649-b212-3ea77e4c0660.json", + "persisted_at": "2026-09-10T19:56:30.650Z", + "version": 1, + "lease_token": null + } + ], + "state_after": [ + { + "site_id": "default", + "revision": "11378c4e-5194-4f1a-ae87-a6c9f16087c9", + "manifest_key": "sites/default/markdown/revisions/11378c4e-5194-4f1a-ae87-a6c9f16087c9.json", + "persisted_at": "2026-09-10T07:35:35.441Z", + "version": 24, + "lease_token": null + }, + { + "site_id": "native-api", + "revision": "be767628-161b-4f22-b93c-094aaf79b1d0", + "manifest_key": "sites/native-api/markdown/revisions/be767628-161b-4f22-b93c-094aaf79b1d0.json", + "persisted_at": "2026-09-10T20:33:44.683Z", + "version": 9, + "lease_token": null + }, + { + "site_id": "native-engine", + "revision": "ee5555b1-c849-464e-83c5-064f7d359cce", + "manifest_key": "sites/native-engine/markdown/revisions/ee5555b1-c849-464e-83c5-064f7d359cce.json", + "persisted_at": "2026-09-10T19:08:39.332Z", + "version": 1, + "lease_token": null + }, + { + "site_id": "native-sales-rehearsal", + "revision": "8be31b29-8405-4649-b212-3ea77e4c0660", + "manifest_key": "sites/native-sales-rehearsal/markdown/revisions/8be31b29-8405-4649-b212-3ea77e4c0660.json", + "persisted_at": "2026-09-10T19:56:30.650Z", + "version": 1, + "lease_token": null + } + ], + "validation": { + "package_build": "passed", + "focused_tests_passed": 14, + "actual_native_revision_restore_render_replay_poll": "passed" + }, + "files_sha256": { + "revised-php-measurements.json": "39dc17a89e0c21f609cd74e016ec8e328615dc9a862d9e16c065f1aa0d254300", + "invocations.json": "36474ab4c0e2895ca5cea4984985cae29753e3c2ca8389d1b68938b5359850c6", + "mutations.json": "75fc720bbeeeb60768fcdffb9f3302c9c4b412fac77bc98f6edde0eaab2b5fbc", + "analytics-query.json": "f8323f9589a3273297def191551e13732de859eb7aa9e5d994981e33c819b078", + "restored-php-measurements.json": "d62eaca3866bdbe78c277b571b7a6a39c9518163b317c5837b7487f5da79aff6", + "bindings-query.json": "ab6ba954572f2f790937cf4c493938dbed872475a0e8f5b8c816483d875eba04", + "bindings-response.json": "96a5e8872232475e57518639c3755902210ede0b6f7a8e422326afba86dfbfe0", + "analytics-response.json": "2d2a0d2af88e57493757c51c84bb05749e4a85c807d9cee62c1200b41d22cc05" + } +} diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/restored-php-measurements.json b/outputs/bricks-native-worker-telemetry/system-allocator/restored-php-measurements.json new file mode 100644 index 00000000..d8b17b32 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/restored-php-measurements.json @@ -0,0 +1,15 @@ +{ + "schema": "wp-codebox/native-bricks-resource-observation/v1", + "operation_id": "native_allocator_20260910_restore", + "wall_ms": 4794, + "peak_php_bytes": null, + "php_allocator": "system", + "unmeasured": [ + "peak_php_bytes", + "worker_cpu_ms", + "peak_isolate_bytes", + "d1_reads_writes", + "r2_operations", + "subrequests" + ] +} \ No newline at end of file diff --git a/outputs/bricks-native-worker-telemetry/system-allocator/revised-php-measurements.json b/outputs/bricks-native-worker-telemetry/system-allocator/revised-php-measurements.json new file mode 100644 index 00000000..77341f24 --- /dev/null +++ b/outputs/bricks-native-worker-telemetry/system-allocator/revised-php-measurements.json @@ -0,0 +1,15 @@ +{ + "schema": "wp-codebox/native-bricks-resource-observation/v1", + "operation_id": "native_allocator_20260910_revise", + "wall_ms": 11774, + "peak_php_bytes": null, + "php_allocator": "system", + "unmeasured": [ + "peak_php_bytes", + "worker_cpu_ms", + "peak_isolate_bytes", + "d1_reads_writes", + "r2_operations", + "subrequests" + ] +} \ No newline at end of file diff --git a/packages/runtime-cloudflare/README.md b/packages/runtime-cloudflare/README.md index b125f874..262adc27 100644 --- a/packages/runtime-cloudflare/README.md +++ b/packages/runtime-cloudflare/README.md @@ -86,3 +86,39 @@ For explicitly authorized remote transport evidence, run `npm run remote-gate:pr 5. Run `npm run local-gate` and `npm run local-gate:d1` for the isolated workerd workflow through both coordinator implementations. Each gate generates and provisions all artifacts, verifies the selected backend through the state envelope, injects stable test-only admin-password, auth-secret, and operator-token values, uploads and activates a real plugin ZIP, establishes an anonymous homepage and canonical-permalink publication, and updates a published post through authenticated REST without immediate rendering. The D1 profile proves queue-driven publication before restarting the local process, then proves the immutable R2 result survives restart; local Wrangler queues themselves are process-local rather than a durability substitute for deployed Cloudflare Queues. Wrangler's scheduled-test facade suppresses local queue consumers, so the D1 gate temporarily restarts against the same persisted state for scheduled WordPress cron assertions and returns to normal queue mode for import execution. The coordinator-only Durable Object profile proves the updated canonical state through authenticated REST while preserving its previous immutable publication. Both prove bounded scheduled WordPress cron, coordinator-free R2 reads, and canonical state across restart. A final two-host phase proves isolated mutations, coordinator versions, REST collections, credentials, publications, and caches plus fail-closed unknown-host routing. The remaining coverage includes login, concurrent canonical writes, media, representative frontend/admin/editor assets, PHP diagnostics, session recovery, and a fresh login after restart. This document describes local candidate verification only. It does not claim remote deployment. + + +## Native Bricks API proof of concept + +The authenticated native routes are `PUT /v1/bricks/artifacts/{sha256}`, `POST /v1/bricks/sites`, and `POST /v1/bricks/sites/{siteId}/revisions|restores`. Poll through `GET /v1/sites/{siteId}/operations/{operationId}`. They accept the Build native contract, preserve source/artifact hashes, serialize mutations through the existing D1 lease/CAS, reject stale exact bases, and return immutable operation receipts. Restore creates a new monotonic canonical revision containing the retained target content. + +Provision initializes native documents on an **operator-prepared empty allocation** declared in `WORDPRESS_BRICKS_PREPARED_ALLOCATIONS` and `WORDPRESS_SITE_CONTEXTS`. It does not allocate Cloudflare resources or install Bricks inside a Worker request. Missing preparation returns `native_allocation_unprepared`. The route uses real Bricks vendor authoring and ownership APIs for native pages, templates, classes, variables, palettes and Theme Styles, plus WordPress media APIs. Stable external IDs are mapped consistently to Bricks six-character IDs. A template type change requires a new logical document ID. + +Prepare a fresh local state from a retained Miniflare runtime fixture and the authorized Bricks ZIP: + +```sh +python3 scripts/prepare-native-bricks-allocation.py \ + --source-state .bricks-local-state-SOURCE \ + --destination-state .bricks-local-state-native-api-NEW \ + --output outputs/native-api-prepared-NEW \ + --runtime-zip /absolute/path/bricks.zip --runtime-version 2.4-rc --port 8812 +npx wrangler dev --config outputs/native-api-prepared-NEW/wrangler.jsonc \ + --persist-to .bricks-local-state-native-api-NEW --port 8812 --local +``` + +The preparation script copies generic runtime objects, extracts all Bricks files from the supplied ZIP, seeds fresh WordPress credentials, omits prior customer documents/media/design, and creates only local files. Generated `.dev.vars`, `secrets.json`, and `credentials.json` remain private and ignored. `--site-id` and `--origin` prepare a separate namespace for an operator-controlled remote export; the script never deploys or writes remote state. + +In a second terminal, run the actual Build producer/writer against the runtime: + +```sh +NATIVE_API_ORIGIN=http://127.0.0.1:8812 npx tsx scripts/native-bricks-build-interop.mts \ + outputs/native-api-prepared-NEW /absolute/path/to/Build/source +``` + +Stop and restart Wrangler against the retained state, then capture cold receipt/render evidence: + +```sh +node scripts/native-bricks-cold-reopen.mjs outputs/native-api-prepared-NEW /absolute/evidence/directory +``` + +Local evidence is retained in `../../outputs/bricks-native-api-local-rehearsal`; the real Worker/D1/R2 API fixture proof is in `../../outputs/bricks-native-api-remote-rehearsal`. Non-default native allocations return a ready protected preview at `/v1/bricks/sites/{siteId}/previews/{receiptId}/`. It requires the owning bearer principal, current lifecycle/generation and exact current canonical revision; raw native hosts return 404 and stale previews return 409. Revision and restore retain monotonic canonical versions. Preview readiness does not imply licensed visual-editor or client-role acceptance. Native runtimes disable OPCache file caching to avoid Worker memory failures during rendering. The native target must remain disabled for customer production until the separate licensed remote canary and Build release gates pass. diff --git a/packages/runtime-cloudflare/assets/markdown-database-integration-runtime-bricks.zip b/packages/runtime-cloudflare/assets/markdown-database-integration-runtime-bricks.zip new file mode 100644 index 00000000..b4133a68 Binary files /dev/null and b/packages/runtime-cloudflare/assets/markdown-database-integration-runtime-bricks.zip differ diff --git a/packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip b/packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip index 40a5035b..e29fa553 100644 Binary files a/packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip and b/packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip differ diff --git a/packages/runtime-cloudflare/assets/mcp-adapter-0.6.1.zip b/packages/runtime-cloudflare/assets/mcp-adapter-0.6.1.zip new file mode 100644 index 00000000..c853a695 Binary files /dev/null and b/packages/runtime-cloudflare/assets/mcp-adapter-0.6.1.zip differ diff --git a/packages/runtime-cloudflare/assets/website-importer-artifact.json b/packages/runtime-cloudflare/assets/website-importer-artifact.json index cbd13bec..d096dc79 100644 --- a/packages/runtime-cloudflare/assets/website-importer-artifact.json +++ b/packages/runtime-cloudflare/assets/website-importer-artifact.json @@ -28,7 +28,7 @@ "version_constant": "STATIC_SITE_IMPORTER_VERSION" }, "abilities": { - "website-artifact-import": "static-site-importer/import" + "website-artifact-import": "static-site-importer/import-website-artifact" }, "limits": { "files": 10000, diff --git a/packages/runtime-cloudflare/assets/wordpress-runtime-artifact.json b/packages/runtime-cloudflare/assets/wordpress-runtime-artifact.json index 7d6bcc83..33e3d7ff 100644 --- a/packages/runtime-cloudflare/assets/wordpress-runtime-artifact.json +++ b/packages/runtime-cloudflare/assets/wordpress-runtime-artifact.json @@ -1,9 +1,9 @@ { "schema": "wp-codebox/wordpress-runtime-artifact/v1", - "key": "runtime/wordpress/279477d490700affba70acc43055045878083358096249314fae4e163dc37502.zip", + "key": "runtime/wordpress/3c3b69a77f2663f4c52a55762a300207158be81e032c46b9e3a9210d3ab8efc8.zip", "archive": { - "sha256": "279477d490700affba70acc43055045878083358096249314fae4e163dc37502", - "size": 5798703 + "sha256": "3c3b69a77f2663f4c52a55762a300207158be81e032c46b9e3a9210d3ab8efc8", + "size": 5810263 }, "source": { "url": "https://downloads.wordpress.org/release/wordpress-7.0.2.zip", diff --git a/packages/runtime-cloudflare/components/website-importer.json b/packages/runtime-cloudflare/components/website-importer.json index 361fd9c1..28164366 100644 --- a/packages/runtime-cloudflare/components/website-importer.json +++ b/packages/runtime-cloudflare/components/website-importer.json @@ -23,7 +23,7 @@ "version_constant": "STATIC_SITE_IMPORTER_VERSION" }, "abilities": { - "website-artifact-import": "static-site-importer/import" + "website-artifact-import": "static-site-importer/import-website-artifact" }, "limits": { "files": 10000, diff --git a/packages/runtime-cloudflare/npm-shrinkwrap.json b/packages/runtime-cloudflare/npm-shrinkwrap.json index 62b0a9d9..441da7e6 100644 --- a/packages/runtime-cloudflare/npm-shrinkwrap.json +++ b/packages/runtime-cloudflare/npm-shrinkwrap.json @@ -11,7 +11,6 @@ "@automattic/wp-codebox-core", "@php-wasm/stream-compression", "@php-wasm/universal", - "@php-wasm/web-8-5", "@wp-playground/wordpress", "patch-package" ], @@ -1267,7 +1266,6 @@ "version": "3.1.46", "resolved": "https://registry.npmjs.org/@php-wasm/web-8-5/-/web-8-5-3.1.46.tgz", "integrity": "sha512-j9wwlY6aNXuKBr4JPJzqE+vkM3Onk+byJsxLqaJs4U0gYDUBLr8CuKuVr+p13apSTUosConePYTAs0214aNxYA==", - "inBundle": true, "license": "GPL-2.0-or-later", "dependencies": { "@php-wasm/universal": "3.1.46", @@ -2407,7 +2405,6 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", - "inBundle": true, "license": "Apache-2.0" }, "node_modules/which": { diff --git a/packages/runtime-cloudflare/package.json b/packages/runtime-cloudflare/package.json index f37064a3..785152ac 100644 --- a/packages/runtime-cloudflare/package.json +++ b/packages/runtime-cloudflare/package.json @@ -62,7 +62,6 @@ "@automattic/wp-codebox-core", "@php-wasm/stream-compression", "@php-wasm/universal", - "@php-wasm/web-8-5", "@wp-playground/wordpress", "patch-package" ], diff --git a/packages/runtime-cloudflare/scripts/bricks-builder-response-debug.mjs b/packages/runtime-cloudflare/scripts/bricks-builder-response-debug.mjs new file mode 100644 index 00000000..b619fec9 --- /dev/null +++ b/packages/runtime-cloudflare/scripts/bricks-builder-response-debug.mjs @@ -0,0 +1,63 @@ +import { mkdir, writeFile } from "node:fs/promises" + +const origin = process.env.BRICKS_PROBE_ORIGIN ?? "http://127.0.0.1:8795" +const password = process.env.BRICKS_PROBE_PASSWORD ?? "bricks-cloudflare-test-password" +const existingPageId = Number.parseInt(process.env.BRICKS_BUILDER_PAGE_ID ?? "", 10) +const outputPath = process.env.BRICKS_BUILDER_OUTPUT +const cookies = [] + +function remember(response) { + for (const raw of response.headers.getSetCookie?.() ?? []) { + const [pair] = raw.split(";", 1) + const [name, value] = pair.split("=", 2) + const index = cookies.findIndex((cookie) => cookie.name === name) + if (index >= 0) cookies[index] = { name, value } + else cookies.push({ name, value }) + } +} + +async function request(path, init = {}) { + const headers = new Headers(init.headers) + if (cookies.length) headers.set("cookie", cookies.map(({ name, value }) => `${name}=${value}`).join("; ")) + const response = await fetch(new URL(path, origin), { ...init, headers, redirect: "manual" }) + remember(response) + return response +} + +await request("/wp-login.php") +await request("/wp-login.php", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ log: "admin", pwd: password, redirect_to: `${origin}/wp-admin/`, testcookie: "1", "wp-submit": "Log In" }), +}) +const dashboard = await request("/wp-admin/") +const dashboardHtml = await dashboard.text() +const restNonce = dashboardHtml.match(/"nonce":"([^"]+)"/)?.[1] +if (!restNonce) throw new Error("Missing REST nonce.") +let page +if (Number.isInteger(existingPageId) && existingPageId > 0) { + const fetched = await request(`/wp-json/wp/v2/pages/${existingPageId}?context=edit`, { headers: { "x-wp-nonce": restNonce } }) + page = await fetched.json() + if (!fetched.ok || !Number.isInteger(page.id)) throw new Error(`Could not read page: ${fetched.status} ${JSON.stringify(page).slice(0, 1000)}`) +} else { + const created = await request("/wp-json/wp/v2/pages", { + method: "POST", + headers: { "content-type": "application/json", "x-wp-nonce": restNonce }, + body: JSON.stringify({ title: "Bricks response diagnostic", status: "draft" }), + }) + page = await created.json() + if (!created.ok || !Number.isInteger(page.id)) throw new Error(`Could not create page: ${created.status} ${JSON.stringify(page).slice(0, 1000)}`) +} +let response = await request(`/?page_id=${page.id}&bricks=run`) +const redirectChain = [] +for (let hop = 0; hop < 5 && response.status >= 300 && response.status < 400 && response.headers.get("location"); hop += 1) { + redirectChain.push({ status: response.status, location: response.headers.get("location"), headers: Object.fromEntries(response.headers) }) + response = await request(response.headers.get("location")) +} +const body = await response.text() +const report = { status: response.status, headers: Object.fromEntries(response.headers), redirectChain, page: { id: page.id, title: page.title?.rendered }, body: body.slice(0, 8000) } +if (outputPath) { + await mkdir(outputPath.slice(0, outputPath.lastIndexOf("/")), { recursive: true }) + await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) +} +console.log(JSON.stringify(report)) diff --git a/packages/runtime-cloudflare/scripts/bricks-client-transaction-proof.mjs b/packages/runtime-cloudflare/scripts/bricks-client-transaction-proof.mjs new file mode 100644 index 00000000..45eebabd --- /dev/null +++ b/packages/runtime-cloudflare/scripts/bricks-client-transaction-proof.mjs @@ -0,0 +1,211 @@ +import { mkdir, writeFile } from "node:fs/promises" + +const origin = process.env.BRICKS_PROBE_ORIGIN ?? "http://127.0.0.1:8798" +const adminPassword = process.env.BRICKS_PROBE_PASSWORD ?? "bricks-cloudflare-test-password" +const clientPassword = process.env.BRICKS_CLIENT_PASSWORD ?? "bricks-client-content-editor-password" +const outputPath = process.env.BRICKS_CLIENT_OUTPUT ?? "outputs/bricks-client-transaction.json" +const pageId = Number(process.env.BRICKS_HOME_PAGE_ID ?? 13) +const username = process.env.BRICKS_CLIENT_USERNAME ?? "client_content_editor" +const changedText = process.env.BRICKS_CLIENT_CHANGED_TEXT ?? "Professional cleaning, staining, sealing, and repair for outdoor wood surfaces across Southwest Missouri. Client edit proof." +const originalText = "Professional cleaning, staining, sealing, and repair for outdoor wood surfaces across Southwest Missouri." + +class Session { + constructor(login, password) { + this.login = login + this.password = password + this.cookies = [] + } + + cookieHeader() { return this.cookies.map(({ name, value }) => `${name}=${value}`).join("; ") } + + remember(response) { + for (const raw of response.headers.getSetCookie?.() ?? []) { + const pair = raw.split(";", 1)[0] + const equals = pair.indexOf("=") + if (equals < 1) continue + const next = { name: pair.slice(0, equals), value: pair.slice(equals + 1) } + const index = this.cookies.findIndex((item) => item.name === next.name) + if (index >= 0) this.cookies[index] = next + else this.cookies.push(next) + } + } + + async request(path, init = {}) { + const headers = new Headers(init.headers) + if (this.cookies.length) headers.set("cookie", this.cookieHeader()) + const response = await fetch(new URL(path, origin), { ...init, headers, redirect: "manual" }) + this.remember(response) + return response + } + + async loginWordPress() { + const initial = await this.request("/wp-login.php") + if (!initial.ok || !/id=["']loginform["']/i.test(await initial.text())) throw new Error(`${this.login} login form failed`) + const response = await this.request("/wp-login.php", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ log: this.login, pwd: this.password, redirect_to: `${origin}/wp-admin/`, testcookie: "1", "wp-submit": "Log In" }), + }) + if (![301, 302].includes(response.status)) throw new Error(`${this.login} login failed: ${response.status}`) + const dashboard = await this.request("/wp-admin/") + const html = await dashboard.text() + if (!dashboard.ok || !/Dashboard/i.test(html)) throw new Error(`${this.login} dashboard failed: ${dashboard.status}`) + const nonceResponse = await this.request("/wp-admin/admin-ajax.php?action=rest-nonce") + const nonce = (await nonceResponse.text()).trim() + if (!nonceResponse.ok || !/^[a-f0-9]{10}$/i.test(nonce)) throw new Error(`${this.login} REST nonce endpoint failed`) + return nonce + } + + async rest(path, nonce, init = {}) { + const headers = new Headers(init.headers) + headers.set("x-wp-nonce", nonce) + const response = await this.request(path, { ...init, headers }) + const text = await response.text() + let body + try { body = JSON.parse(text) } catch { body = text } + if (!response.ok) throw new Error(`${init.method ?? "GET"} ${path} failed: ${response.status}; ${text.slice(0, 1600)}`) + return body + } + + async mcpRequest(nonce, sessionId, payload) { + const headers = { "content-type": "application/json", accept: "application/json, text/event-stream", "x-wp-nonce": nonce } + if (sessionId) headers["mcp-session-id"] = sessionId + const response = await this.request("/wp-json/mcp/mcp-adapter-default-server", { method: "POST", headers, body: JSON.stringify(payload) }) + const text = await response.text() + const body = text.trim() ? JSON.parse(text) : null + if (!response.ok || body?.error) throw new Error(`MCP request failed for ${this.login}: ${response.status}; ${text.slice(0, 1800)}`) + return { response, body } + } + + async openMcp(nonce) { + const initialized = await this.mcpRequest(nonce, null, { + jsonrpc: "2.0", id: 1, method: "initialize", + params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "bricks-client-transaction-proof", version: "1.0.0" } }, + }) + const sessionId = initialized.response.headers.get("mcp-session-id") + if (!sessionId) throw new Error("MCP initialize omitted session ID") + await this.mcpRequest(nonce, sessionId, { jsonrpc: "2.0", method: "notifications/initialized" }) + return sessionId + } + + async ability(nonce, sessionId, name, parameters, id) { + const called = await this.mcpRequest(nonce, sessionId, { + jsonrpc: "2.0", id, method: "tools/call", + params: { name: "mcp-adapter-execute-ability", arguments: { ability_name: name, parameters } }, + }) + const result = called.body?.result + if (result?.isError) throw new Error(`${this.login} ability ${name} failed: ${JSON.stringify(result).slice(0, 2200)}`) + const structured = result?.structuredContent + if (structured && typeof structured === "object") return structured + for (const item of result?.content ?? []) { + if (item?.type !== "text") continue + try { return JSON.parse(item.text) } catch {} + } + throw new Error(`${name} returned no structured result`) + } +} + +async function publicBody(marker) { + const response = await fetch(`${origin}/?${marker}=${Date.now()}`, { redirect: "manual" }) + const body = await response.text() + if (!response.ok) throw new Error(`Public observation failed: ${response.status}`) + return { body, headers: Object.fromEntries(response.headers.entries()) } +} + +async function enableAdminAbility(session, abilityName) { + const response = await session.request("/wp-admin/admin.php?page=bricks-ai") + const html = await response.text() + if (!response.ok) throw new Error(`Bricks AI settings failed: ${response.status}`) + const values = [] + let targetExists = false + for (const tag of html.match(/]*>/gi) ?? []) { + if (!/name=["']bricksMcpEnabledAbilities\[\]["']/i.test(tag)) continue + const value = tag.match(/value=["']([^"']+)["']/i)?.[1]?.replaceAll("&", "&") + if (!value) continue + if (value === abilityName) targetExists = true + if (/\bchecked\b/i.test(tag)) values.push(value) + } + if (!targetExists) throw new Error(`Bricks AI settings did not offer ${abilityName}`) + if (!values.includes(abilityName)) values.push(abilityName) + const marker = html.indexOf("var bricksData =") + const nonce = marker >= 0 ? html.slice(marker).match(/"nonce":"([^"]+)"/)?.[1] : null + if (!nonce) throw new Error("Bricks AI settings omitted its admin nonce") + const settings = new URLSearchParams({ abilitiesApi: "on" }) + for (const value of values) settings.append("bricksMcpEnabledAbilities[]", value) + const saved = await session.request("/wp-admin/admin-ajax.php", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ action: "bricks_save_ai_settings", formData: settings.toString(), nonce }), + }) + const body = await saved.json() + if (!saved.ok || !body.success) throw new Error(`Bricks rejected ${abilityName} activation: ${JSON.stringify(body).slice(0, 1200)}`) + return { abilityName, enabled: true, totalEnabled: values.length } +} + +const admin = new Session("admin", adminPassword) +const adminNonce = await admin.loginWordPress() +const permissionAbility = await enableAdminAbility(admin, "bricks/set-builder-role-access") +let users = await admin.rest(`/wp-json/wp/v2/users?search=${encodeURIComponent(username)}&context=edit`, adminNonce) +let clientUser = Array.isArray(users) ? users.find((user) => user.username === username) : null +if (!clientUser) { + clientUser = await admin.rest("/wp-json/wp/v2/users", adminNonce, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ username, name: "Client Content Editor", email: `${username}@417agency.test`, password: clientPassword, roles: ["editor"] }), + }) +} + +const adminMcp = await admin.openMcp(adminNonce) +const roleGrant = await admin.ability(adminNonce, adminMcp, "bricks/set-builder-role-access", { roleAccess: { editor: "bricks_edit_content" } }, 2) +if (!roleGrant.success) throw new Error(`Editor role grant failed: ${JSON.stringify(roleGrant)}`) + +const client = new Session(username, clientPassword) +const clientNonce = await client.loginWordPress() +const clientMcp = await client.openMcp(clientNonce) +const before = await client.ability(clientNonce, clientMcp, "bricks/get-page-elements", { postId: pageId, responseFormat: "detailed" }, 3) +if (!before.success) throw new Error(`Client page read failed: ${JSON.stringify(before)}`) +const elements = before.data?.elements ?? [] +const target = elements.find((element) => element?.name === "text-basic" && element?.settings?.text === originalText) +if (!target?.id) throw new Error("Client transaction could not locate the exact native lede element") + +const updated = await client.ability(clientNonce, clientMcp, "bricks/update-element", { + postId: pageId, + elementId: target.id, + settings: { text: changedText }, + returnElement: true, +}, 4) +if (!updated.success || !updated.data?.changed || !Number.isInteger(updated.data?.revisionId)) throw new Error(`Client native edit did not commit with revision: ${JSON.stringify(updated)}`) + +const publishAfterEdit = await client.rest(`/wp-json/wp/v2/pages/${pageId}`, clientNonce, { + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ status: "publish" }), +}) +const observedEdit = await publicBody("client-edit") +if (!observedEdit.body.includes(changedText)) throw new Error("Published page did not expose the client edit") + +const restored = await client.ability(clientNonce, clientMcp, "bricks/restore-revision", { revisionId: updated.data.revisionId, postId: pageId }, 5) +if (!restored.success || !restored.data?.restored) throw new Error(`Client restore failed: ${JSON.stringify(restored)}`) +const publishAfterRestore = await client.rest(`/wp-json/wp/v2/pages/${pageId}`, clientNonce, { + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ status: "publish" }), +}) +const observedRestore = await publicBody("client-restore") +if (!observedRestore.body.includes(originalText) || observedRestore.body.includes(changedText)) throw new Error("Published page did not expose the restored native Bricks content") + +const reopened = await client.ability(clientNonce, clientMcp, "bricks/get-page-elements", { postId: pageId, responseFormat: "detailed" }, 6) +const reopenedTarget = reopened.data?.elements?.find((element) => element?.id === target.id) +if (reopenedTarget?.settings?.text !== originalText) throw new Error("Client reopen did not return the restored element text") + +const report = { + status: "client-edit-publish-observe-restore-passed", + origin, + client: { id: clientUser.id, username, roles: clientUser.roles }, + roleGrant: roleGrant.data, + permissionAbility, + target: { postId: pageId, elementId: target.id, elementName: target.name }, + edit: { revisionId: updated.data.revisionId, changed: updated.data.changed, publishedStatus: publishAfterEdit.status, publicObserved: true }, + restore: { restored: restored.data.restored, fromRevisionId: restored.data.fromRevisionId, newRevisionId: restored.data.newRevisionId, publishedStatus: publishAfterRestore.status, publicObserved: true, reopened: true }, + publicHeaders: { edit: observedEdit.headers, restore: observedRestore.headers }, + generatedAt: new Date().toISOString(), +} +await mkdir(outputPath.slice(0, outputPath.lastIndexOf("/")), { recursive: true }) +await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify(report)) diff --git a/packages/runtime-cloudflare/scripts/bricks-cloudflare-probe.mjs b/packages/runtime-cloudflare/scripts/bricks-cloudflare-probe.mjs new file mode 100644 index 00000000..b5f31df6 --- /dev/null +++ b/packages/runtime-cloudflare/scripts/bricks-cloudflare-probe.mjs @@ -0,0 +1,174 @@ +import { readFile } from "node:fs/promises" +import { chromium } from "playwright" + +const origin = process.env.BRICKS_PROBE_ORIGIN ?? "http://127.0.0.1:8793" +const password = process.env.BRICKS_PROBE_PASSWORD ?? "bricks-cloudflare-test-password" +const themePath = process.env.BRICKS_THEME_ZIP +const trace = (stage) => { + if (process.env.BRICKS_PROBE_TRACE === "1") console.error(`[bricks-probe] ${stage}`) +} + +if (!themePath) throw new Error("BRICKS_THEME_ZIP is required.") + +const cookies = [] + +function cookieHeader() { + return cookies.map(({ name, value }) => `${name}=${value}`).join("; ") +} + +function rememberCookies(response) { + for (const raw of response.headers.getSetCookie?.() ?? []) { + const [pair] = raw.split(";", 1) + const [name, value] = pair.split("=", 2) + const index = cookies.findIndex((entry) => entry.name === name) + if (index >= 0) cookies[index] = { name, value } + else cookies.push({ name, value }) + } +} + +async function request(path, init = {}) { + const headers = new Headers(init.headers) + if (cookies.length) headers.set("cookie", cookieHeader()) + const response = await fetch(new URL(path, origin), { ...init, headers, redirect: "manual" }) + rememberCookies(response) + return response +} + +function nonce(html, name = "_wpnonce") { + return html.match(new RegExp(`name=["']${name}["'][^>]*value=["']([^"']+)["']`, "i"))?.[1] +} + +function restNonce(html) { + const value = html.match(/"nonce":"([^"]+)"/)?.[1] + if (!value) throw new Error("wp-admin did not expose a REST nonce.") + return value +} + +function bricksThemeContext(html) { + const marker = html.indexOf('id="bricks-action"') + if (marker < 0) return "" + const start = Math.max(html.lastIndexOf('
', marker), html.lastIndexOf('
', marker)) + return start < 0 ? "" : html.slice(start, marker + 1600) +} + +async function login() { + const initial = await request("/wp-login.php") + if (!initial.ok || !/]+id=["']loginform["']/i.test(await initial.text())) throw new Error("WordPress login form did not open.") + const form = new URLSearchParams({ log: "admin", pwd: password, redirect_to: `${origin}/wp-admin/`, testcookie: "1", "wp-submit": "Log In" }) + const response = await request("/wp-login.php", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }) + if (![301, 302].includes(response.status) || !response.headers.get("location")?.includes("/wp-admin/")) throw new Error(`Login failed: ${response.status}`) + const dashboard = await request("/wp-admin/") + const html = await dashboard.text() + if (!dashboard.ok || !/Dashboard/i.test(html)) throw new Error(`wp-admin did not open: ${dashboard.status}`) + return html +} + +async function installTheme() { + const upload = await request("/wp-admin/theme-install.php?browse=popular") + const uploadHtml = await upload.text() + const uploadNonce = nonce(uploadHtml) + if (!upload.ok || !uploadNonce) throw new Error(`Theme upload screen did not expose a nonce: ${upload.status}`) + + const archive = await readFile(themePath) + const form = new FormData() + form.set("_wpnonce", uploadNonce) + form.set("_wp_http_referer", "/wp-admin/theme-install.php?browse=popular") + form.set("themezip", new File([archive], "bricks.2.4-rc.zip", { type: "application/zip" })) + form.set("install-theme-submit", "Install Now") + const installed = await request("/wp-admin/update.php?action=upload-theme", { method: "POST", body: form }) + if (!installed.ok) throw new Error(`Theme upload failed: status=${installed.status}; body=${(await installed.text()).slice(0, 500)}`) +} + +async function createBricksPage(dashboardHtml) { + const title = "Stain & Seal Pros — Cloudflare Native Bricks Proof" + const response = await request("/wp-json/wp/v2/pages", { + method: "POST", + headers: { "content-type": "application/json", "x-wp-nonce": restNonce(dashboardHtml) }, + body: JSON.stringify({ title, status: "draft" }), + }) + const body = await response.text() + if (response.status !== 201) throw new Error(`Native WordPress page creation failed: ${response.status}; ${body.slice(0, 500)}`) + const page = JSON.parse(body) + if (!Number.isInteger(page.id) || typeof page.slug !== "string" || typeof page.link !== "string") throw new Error(`Native WordPress page response was incomplete: ${body.slice(0, 500)}`) + return { id: page.id, slug: page.slug, link: page.link, title } +} + +async function openBricksBuilder(page) { + trace("browser.launch") + const browser = await chromium.launch({ headless: true }) + try { + const context = await browser.newContext() + await context.addCookies(cookies.map(({ name, value }) => ({ name, value, domain: "127.0.0.1", path: "/" }))) + const assetResponses = [] + const pageErrors = [] + context.on("response", (response) => { + const url = response.url() + if (url.includes("/wp-content/themes/bricks/assets/")) assetResponses.push({ url: new URL(url).pathname, status: response.status(), source: response.headers()["x-wp-codebox-static"] ?? null }) + }) + const builderTab = await context.newPage() + builderTab.on("pageerror", (error) => pageErrors.push(error.message)) + const builder = new URL(page.link) + builder.searchParams.set("bricks", "run") + trace("builder.goto") + const response = await builderTab.goto(builder.toString(), { waitUntil: "commit", timeout: 30000 }) + if (!response?.ok()) throw new Error(`Bricks builder request failed: ${response?.status() ?? "no response"}`) + await builderTab.waitForSelector("#bricks-preloader", { state: "attached", timeout: 60000 }) + trace("builder.preloader") + await builderTab.waitForTimeout(3000) + trace("builder.evaluate") + const evidence = await builderTab.evaluate(() => ({ + title: document.title, + builder: Boolean(document.querySelector(".brx-body.main")), + preloader: Boolean(document.querySelector("#bricks-preloader")), + preloaderVisible: (() => { + const node = document.querySelector("#bricks-preloader") + return node instanceof HTMLElement && getComputedStyle(node).display !== "none" && getComputedStyle(node).visibility !== "hidden" + })(), + appRoots: [...document.querySelectorAll("[data-v-app]")].length, + iframe: Boolean(document.querySelector("iframe")), + bodyClass: document.body.className, + })) + const requiredAssets = assetResponses.filter((entry) => /(?:builder\.min\.css|main\.min\.js|iframe\.min\.js)$/.test(entry.url)) + if (!evidence.builder || evidence.preloaderVisible || evidence.appRoots < 1 || !evidence.iframe) throw new Error(`Bricks builder did not finish opening: ${JSON.stringify(evidence)}`) + if (!requiredAssets.length || requiredAssets.some((entry) => entry.status !== 200 || entry.source !== "r2-wp-content")) { + throw new Error(`Bricks builder did not load its required R2 assets: ${JSON.stringify(requiredAssets)}`) + } + // The builder continues background autosave/heartbeat traffic. A viewport + // capture is enough for the boot proof and avoids waiting for its page + // height to settle indefinitely. + if (process.env.BRICKS_PROBE_SCREENSHOT) await builderTab.screenshot({ path: process.env.BRICKS_PROBE_SCREENSHOT, fullPage: false, timeout: 5000 }) + trace("builder.complete") + return { ...evidence, assetResponses: requiredAssets, pageErrors } + } finally { + trace("browser.close") + await browser.close() + } +} + +trace("login") +const dashboardHtml = await login() +trace("login.complete") +if (!process.env.BRICKS_PROBE_SKIP_INSTALL) await installTheme() +const themes = await request("/wp-admin/themes.php") +const html = await themes.text() +if (!themes.ok || !/Bricks/i.test(html)) throw new Error(`Active theme check failed: ${themes.status}`) +const bricksTheme = bricksThemeContext(html) +const active = /class=["'][^"']*theme\s+active[^"']*["']/.test(bricksTheme ?? "") +const activation = bricksTheme.match(/href=["']([^"']*themes\.php\?action=activate[^"']*stylesheet=bricks[^"']*)["']/i)?.[1] +if (!active && !activation) throw new Error(`Bricks is installed but WordPress did not expose an activation action: ${bricksTheme.slice(0, 1200)}`) +if (!active) { + const activated = await request(activation.replaceAll("&", "&").replaceAll("&", "&")) + if (![200, 301, 302].includes(activated.status)) throw new Error(`Bricks activation failed: status=${activated.status}; body=${(await activated.text()).slice(0, 500)}`) +} +const verified = await request("/wp-admin/themes.php") +const verifiedHtml = await verified.text() +const verifiedBricksTheme = bricksThemeContext(verifiedHtml) +if (!verified.ok || !/class=["'][^"']*theme\s+active[^"']*["']/.test(verifiedBricksTheme ?? "")) throw new Error(`Bricks did not remain the active WordPress theme: ${(verifiedBricksTheme ?? "missing").slice(0, 500)}`) +if (process.env.BRICKS_PROBE_SKIP_BUILDER) { + console.log(JSON.stringify({ status: "installed-and-active", origin, activeTheme: "Bricks" })) + process.exit(0) +} +const createdPage = await createBricksPage(dashboardHtml) +trace("page.created") +const builder = await openBricksBuilder(createdPage) +console.log(JSON.stringify({ status: "installed-active-builder-open", origin, activeTheme: "Bricks", createdPage, builder, cookies: cookies.map(({ name }) => name) })) diff --git a/packages/runtime-cloudflare/scripts/bricks-native-site-probe.mjs b/packages/runtime-cloudflare/scripts/bricks-native-site-probe.mjs new file mode 100644 index 00000000..6e807d5a --- /dev/null +++ b/packages/runtime-cloudflare/scripts/bricks-native-site-probe.mjs @@ -0,0 +1,490 @@ +import { readFile, writeFile } from "node:fs/promises" + +const origin = process.env.BRICKS_PROBE_ORIGIN ?? "http://127.0.0.1:8795" +const password = process.env.BRICKS_PROBE_PASSWORD ?? "bricks-cloudflare-test-password" +const logoPath = process.env.BRICKS_LOGO_PATH ?? "/Users/ty/Github/Stain-and-Seal-Pros/stain-and-seal-pros/images/logo_transparent.png" +const heroPath = process.env.BRICKS_HERO_PATH ?? "/Users/ty/Github/Stain-and-Seal-Pros/stain-and-seal-pros/images/deck-after-01.jpg" +const mcpAdapterPath = process.env.MCP_ADAPTER_ZIP ?? "packages/runtime-cloudflare/assets/mcp-adapter-0.6.1.zip" +const outputPath = process.env.BRICKS_PROBE_OUTPUT ?? "outputs/bricks-native-site-probe.json" +const idempotencyKey = process.env.BRICKS_FOUNDATION_KEY ?? "stain-seal-pros-cloudflare-native-v1" +const existingFoundation = process.env.BRICKS_EXISTING_FOUNDATION === "1" +const readOnlyReopen = process.env.BRICKS_READ_ONLY === "1" +const mediaSuffix = process.env.BRICKS_MEDIA_SUFFIX ? `-${process.env.BRICKS_MEDIA_SUFFIX}` : "" + +const cookies = [] + +function cookieHeader() { + return cookies.map(({ name, value }) => `${name}=${value}`).join("; ") +} + +function rememberCookies(response) { + for (const raw of response.headers.getSetCookie?.() ?? []) { + const [pair] = raw.split(";", 1) + const equals = pair.indexOf("=") + if (equals < 1) continue + const next = { name: pair.slice(0, equals), value: pair.slice(equals + 1) } + const index = cookies.findIndex((entry) => entry.name === next.name) + if (index >= 0) cookies[index] = next + else cookies.push(next) + } +} + +async function request(path, init = {}) { + const headers = new Headers(init.headers) + if (cookies.length) headers.set("cookie", cookieHeader()) + const response = await fetch(new URL(path, origin), { ...init, headers, redirect: "manual" }) + rememberCookies(response) + return response +} + +function extractRestNonce(html) { + const match = html.match(/"nonce":"([^"]+)"/) + if (!match) throw new Error("wp-admin did not expose a REST nonce") + return match[1] +} + +function extractBricksNonce(html) { + const marker = html.indexOf("var bricksData =") + const match = marker >= 0 ? html.slice(marker).match(/"nonce":"([^"]+)"/) : null + if (!match) throw new Error("Bricks AI settings did not expose its admin nonce") + return match[1] +} + +function checkedBricksAbilities(html) { + const abilities = [] + for (const tag of html.match(/]*>/gi) ?? []) { + if (!/name=["']bricksMcpEnabledAbilities\[\]["']/i.test(tag) || !/\bchecked\b/i.test(tag)) continue + const value = tag.match(/value=["']([^"']+)["']/i)?.[1] + if (value) abilities.push(value.replaceAll("&", "&")) + } + if (!abilities.includes("bricks/commit-site-foundation")) { + throw new Error(`Bricks did not offer commit-site-foundation among ${abilities.length} enabled abilities`) + } + return abilities +} + +async function login() { + const loginForm = await request("/wp-login.php") + const loginHtml = await loginForm.text() + if (!loginForm.ok || !/id=["']loginform["']/i.test(loginHtml)) throw new Error(`WordPress login form failed: ${loginForm.status}`) + const form = new URLSearchParams({ + log: "admin", + pwd: password, + redirect_to: `${origin}/wp-admin/`, + testcookie: "1", + "wp-submit": "Log In", + }) + const response = await request("/wp-login.php", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: form, + }) + if (![301, 302].includes(response.status)) throw new Error(`WordPress login failed: ${response.status}`) + const dashboard = await request("/wp-admin/") + const html = await dashboard.text() + if (!dashboard.ok || !/Dashboard/i.test(html)) throw new Error(`WordPress dashboard failed: ${dashboard.status}`) + return html +} + +async function enableAbilities() { + const page = await request("/wp-admin/admin.php?page=bricks-ai") + const html = await page.text() + if (!page.ok || !/name=["']abilitiesApi["']/i.test(html)) throw new Error(`Bricks AI settings failed: ${page.status}`) + const abilities = checkedBricksAbilities(html) + const settings = new URLSearchParams({ abilitiesApi: "on" }) + for (const ability of abilities) settings.append("bricksMcpEnabledAbilities[]", ability) + const payload = new URLSearchParams({ + action: "bricks_save_ai_settings", + formData: settings.toString(), + nonce: extractBricksNonce(html), + }) + const response = await request("/wp-admin/admin-ajax.php", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: payload, + }) + const body = await response.text() + if (!response.ok) throw new Error(`Bricks ability activation failed: ${response.status}; ${body.slice(0, 800)}`) + const parsed = JSON.parse(body) + if (!parsed.success) throw new Error(`Bricks rejected ability activation: ${body.slice(0, 800)}`) + return abilities +} + +async function installAndActivateMcpAdapter(dashboardHtml) { + let wpNonce = extractRestNonce(dashboardHtml) + let plugins = await rest("/wp-json/wp/v2/plugins?context=edit", wpNonce) + let adapter = Array.isArray(plugins.body) ? plugins.body.find((entry) => entry.plugin === "mcp-adapter/mcp-adapter") : null + if (!adapter) { + const upload = await request("/wp-admin/plugin-install.php?tab=upload") + const uploadHtml = await upload.text() + const uploadNonce = uploadHtml.match(/name=["']_wpnonce["'][^>]*value=["']([^"']+)["']/i)?.[1] + if (!upload.ok || !uploadNonce) throw new Error(`Plugin upload screen failed: ${upload.status}`) + const archive = await readFile(mcpAdapterPath) + const form = new FormData() + form.set("_wpnonce", uploadNonce) + form.set("_wp_http_referer", "/wp-admin/plugin-install.php?tab=upload") + form.set("pluginzip", new File([archive], "mcp-adapter-0.6.1.zip", { type: "application/zip" })) + form.set("install-plugin-submit", "Install Now") + const installed = await request("/wp-admin/update.php?action=upload-plugin", { method: "POST", body: form }) + const body = await installed.text() + await writeFile("/tmp/mcp-adapter-install-response.html", body) + if (!installed.ok || !/Plugin installed successfully/i.test(body)) throw new Error(`MCP Adapter install failed: ${installed.status}; ${body.slice(0, 1000)}`) + wpNonce = await freshRestNonce() + plugins = await rest("/wp-json/wp/v2/plugins?context=edit", wpNonce) + adapter = Array.isArray(plugins.body) ? plugins.body.find((entry) => entry.plugin === "mcp-adapter/mcp-adapter") : null + if (!adapter) throw new Error("MCP Adapter upload completed but WordPress did not register the plugin") + } + if (adapter.status !== "active") { + const activated = await rest("/wp-json/wp/v2/plugins/mcp-adapter/mcp-adapter", wpNonce, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ status: "active" }), + }) + adapter = activated.body + } + if (adapter.status !== "active") throw new Error(`MCP Adapter did not activate: ${JSON.stringify(adapter).slice(0, 1000)}`) + return { plugin: adapter.plugin, status: adapter.status, version: adapter.version ?? "0.6.1" } +} + +async function freshRestNonce() { + const dashboard = await request("/wp-admin/") + const html = await dashboard.text() + if (!dashboard.ok) throw new Error(`WordPress dashboard refresh failed: ${dashboard.status}`) + return extractRestNonce(html) +} + +async function rest(path, wpNonce, init = {}) { + const headers = new Headers(init.headers) + headers.set("x-wp-nonce", wpNonce) + const response = await request(path, { ...init, headers }) + const text = await response.text() + let body + try { + body = JSON.parse(text) + } catch { + body = text + } + if (!response.ok) throw new Error(`${init.method ?? "GET"} ${path} failed: ${response.status}; ${text.slice(0, 1800)}`) + return { response, body } +} + +async function uploadMedia(wpNonce, path, filename, mime, alt) { + const bytes = await readFile(path) + const slug = filename.replace(/\.[^.]+$/, "") + const existing = await rest(`/wp-json/wp/v2/media?slug=${encodeURIComponent(slug)}&per_page=1&context=edit`, wpNonce) + if (Array.isArray(existing.body) && existing.body[0]) { + const media = existing.body[0] + return { id: media.id, url: media.source_url, bytes: bytes.byteLength, mime, reused: true } + } + const { body } = await rest("/wp-json/wp/v2/media", wpNonce, { + method: "POST", + headers: { + "content-type": mime, + "content-disposition": `attachment; filename="${filename}"`, + }, + body: bytes, + }) + await rest(`/wp-json/wp/v2/media/${body.id}`, wpNonce, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ alt_text: alt }), + }) + return { id: body.id, url: body.source_url, bytes: bytes.byteLength, mime, reused: false } +} + +async function runAbility(name, input, wpNonce, destructive = true) { + const encodedInput = encodeURIComponent(JSON.stringify(input)) + const path = destructive + ? `/wp-json/wp-abilities/v1/abilities/${name}/run?input=${encodedInput}` + : `/wp-json/wp-abilities/v1/abilities/${name}/run` + const { body } = await rest(path, wpNonce, destructive + ? { method: "DELETE" } + : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ input }) }) + return body +} + +async function mcpRequest(wpNonce, sessionId, payload) { + const headers = { + "content-type": "application/json", + accept: "application/json, text/event-stream", + "x-wp-nonce": wpNonce, + } + if (sessionId) headers["mcp-session-id"] = sessionId + const response = await request("/wp-json/mcp/mcp-adapter-default-server", { + method: "POST", + headers, + body: JSON.stringify(payload), + }) + const text = await response.text() + let body = null + if (text.trim()) { + try { + body = JSON.parse(text) + } catch { + throw new Error(`MCP returned non-JSON: ${response.status}; ${text.slice(0, 1800)}`) + } + } + if (!response.ok || body?.error) throw new Error(`MCP request failed: ${response.status}; ${JSON.stringify(body).slice(0, 2400)}`) + return { response, body } +} + +async function openMcpSession(wpNonce) { + const initialized = await mcpRequest(wpNonce, null, { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "bricks-cloudflare-native-probe", version: "1.0.0" } }, + }) + const sessionId = initialized.response.headers.get("mcp-session-id") + if (!sessionId) throw new Error(`MCP initialize omitted Mcp-Session-Id: ${JSON.stringify(initialized.body).slice(0, 1200)}`) + await mcpRequest(wpNonce, sessionId, { jsonrpc: "2.0", method: "notifications/initialized" }) + return { sessionId, initialize: initialized.body?.result } +} + +async function mcpExecuteAbility(wpNonce, sessionId, abilityName, parameters, id = 2) { + const called = await mcpRequest(wpNonce, sessionId, { + jsonrpc: "2.0", + id, + method: "tools/call", + params: { + name: "mcp-adapter-execute-ability", + arguments: { ability_name: abilityName, parameters }, + }, + }) + const result = called.body?.result + if (result?.isError) throw new Error(`MCP tool call failed: ${JSON.stringify(result).slice(0, 2400)}`) + if (result?.structuredContent && typeof result.structuredContent === "object") return result.structuredContent + for (const item of result?.content ?? []) { + if (item?.type !== "text" || typeof item.text !== "string") continue + try { + return JSON.parse(item.text) + } catch { + // Keep looking for a structured content block. + } + } + throw new Error(`MCP execute-ability returned no structured result: ${JSON.stringify(result).slice(0, 2400)}`) +} + +async function closeMcpSession(wpNonce, sessionId) { + const response = await request("/wp-json/mcp/mcp-adapter-default-server", { + method: "DELETE", + headers: { "x-wp-nonce": wpNonce, "mcp-session-id": sessionId }, + }) + if (!response.ok) throw new Error(`MCP session close failed: ${response.status}; ${(await response.text()).slice(0, 1000)}`) +} + +function foundationInput(media) { + const logo = media.logo.url + const hero = media.hero.url + return { + idempotencyKey, + palette: { + name: "Stain & Seal Pros", + colors: [ + { name: "Brand Navy", value: "#0B3A66" }, + { name: "Brand Orange", value: "#FF6A00" }, + { name: "Ink", value: "#12202C" }, + { name: "Surface", value: "#FFFFFF" }, + { name: "Surface Warm", value: "#F4EFE8" }, + { name: "Muted", value: "#5B6873" }, + ], + }, + typography: { bodyFontFamily: "Inter", headingFontFamily: "Inter", rootFontSize: "62.5%" }, + header: { + title: "Site Header", + html: ``, + css: `.site-header{display:flex;align-items:center;justify-content:space-between;gap:var(--space-m);max-width:1200px;margin:0 auto;padding:1.6rem 2.4rem;background:var(--surface)}.brand img{display:block;width:17rem;height:auto}.main-nav{display:flex;align-items:center;gap:2.4rem}.main-nav a{color:var(--brand-navy);font-weight:700;text-decoration:none}.button{display:inline-flex;align-items:center;justify-content:center;padding:1.4rem 2.2rem;border-radius:.5rem;background:var(--brand-orange);color:#fff!important;font-weight:800;text-decoration:none}.button-small{padding:1rem 1.6rem}@media(max-width:767px){.site-header{padding:1.2rem 1.6rem}.brand img{width:13rem}.main-nav>a:not(.button){display:none}}`, + }, + footer: { + title: "Site Footer", + html: ``, + css: `.site-footer{display:grid;gap:1.6rem;justify-items:start;background:var(--brand-navy);color:#fff;padding:5rem max(2.4rem,calc((100vw - 1200px)/2))}.site-footer img{width:18rem;filter:brightness(0) invert(1)}.site-footer p{max-width:60ch}.site-footer a{color:#fff;font-weight:800}`, + }, + home: { + title: "Stain & Seal Pros | Wood Restoration & Protection", + slug: "home", + html: `

Springfield-area wood care specialists

Restore the wood you love. Protect it for years.

Professional cleaning, staining, sealing, and repair for outdoor wood surfaces across Southwest Missouri.

Request a free estimate
Freshly restored and stained residential deck
Craftsmanship you can see in every board.

Built for Missouri weather

Complete care for outdoor wood

Deck restoration

Cleaning, preparation, staining, and sealing that restores color and protects the surface.

Fence staining

Even coverage and durable protection for privacy fences, gates, and decorative woodwork.

Specialty wood care

Experienced care for log homes, pergolas, pre-stain projects, and wood repairs.

Real local work

Careful preparation. Clean results.

Every project starts with the condition of the wood and ends with a finish selected for the surface, exposure, and desired look.

Restored deck project completed by Stain & Seal Pros

Ready to protect your investment?

Tell us about your project.

Share the surface, approximate size, and where the project is located. We’ll help you choose the right next step.

Start your estimate
`, + css: `body{margin:0;color:var(--ink);background:var(--surface);font-family:Inter,system-ui,sans-serif}h1,h2,h3{color:var(--brand-navy);font-weight:800;line-height:1.05;margin:0}h1{font-size:clamp(4.2rem,7vw,8rem);max-width:12ch}h2{font-size:clamp(3.4rem,5vw,5.6rem)}h3{font-size:2.4rem}.hero{display:grid;grid-template-columns:minmax(0,1fr) minmax(36rem,.85fr);min-height:70rem;align-items:center;gap:5vw;padding:7rem max(2.4rem,calc((100vw - 1200px)/2));background:linear-gradient(115deg,var(--surface-warm),#fff)}.hero-copy{display:grid;gap:2.2rem;justify-items:start}.eyebrow{margin:0;color:var(--brand-orange);font-size:1.4rem;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.lede{max-width:58rem;color:var(--muted);font-size:2.1rem;line-height:1.6}.hero-media{margin:0}.hero-media img,.work img{width:100%;aspect-ratio:4/5;object-fit:cover;border-radius:1.2rem;box-shadow:0 2.5rem 6rem rgba(11,58,102,.18)}.hero-media figcaption{margin-top:1rem;color:var(--muted);font-size:1.4rem}.section,.work,.estimate{padding:9rem max(2.4rem,calc((100vw - 1200px)/2))}.section-heading{display:grid;gap:1.2rem;max-width:78rem}.service-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem;margin-top:4rem}.service-card{display:grid;gap:1.4rem;padding:3rem;border-top:.5rem solid var(--brand-orange);background:var(--surface-warm)}.service-card p,.work p,.estimate p{color:var(--muted);font-size:1.8rem;line-height:1.65}.work{display:grid;grid-template-columns:1fr 1fr;align-items:center;gap:7vw;background:var(--brand-navy)}.work h2,.work p:not(.eyebrow){color:#fff}.work img{aspect-ratio:3/2}.estimate{display:grid;gap:1.8rem;justify-items:start;max-width:1200px;margin:0 auto}.estimate p{max-width:64rem}@media(max-width:767px){.hero{grid-template-columns:1fr;min-height:auto;padding-top:5rem}.hero-media img{aspect-ratio:4/3}.service-grid{grid-template-columns:1fr}.work{grid-template-columns:1fr}.section,.work,.estimate{padding-top:6rem;padding-bottom:6rem}}`, + }, + } +} + +function existingFoundationTrees(media, classByName) { + const classes = (...names) => ({ _cssGlobalClasses: names.map((name) => { + const id = classByName.get(name) + if (!id) throw new Error(`Existing Bricks global class is missing: ${name}`) + return id + }) }) + const text = (value, classNames = []) => ({ name: "text-basic", settings: { text: value, ...classes(...classNames) } }) + const heading = (value, tag, classNames = []) => ({ name: "heading", settings: { text: value, tag, ...classes(...classNames) } }) + const link = (value, url, classNames = []) => ({ name: "text-link", settings: { text: value, link: { type: "external", url }, ...classes(...classNames) } }) + const image = (asset, _alt, classNames = []) => ({ name: "image", settings: { image: { id: asset.id, url: asset.url, size: "full" }, ...classes(...classNames) } }) + return { + header: [{ name: "div", settings: classes("site-header"), children: [ + { name: "div", settings: classes("brand"), children: [image(media.logo, "Stain & Seal Pros")] }, + { name: "div", settings: classes("main-nav"), children: [ + link("Services", "#services"), link("Our work", "#work"), link("Get an estimate", "#estimate", ["button", "button-small"]), + ] }, + ] }], + footer: [{ name: "div", settings: classes("site-footer"), children: [ + image(media.logo, "Stain & Seal Pros"), + text("Professional wood restoration and protection for decks, fences, log homes, pergolas, and more."), + link("Request an estimate", "#estimate"), + ] }], + home: [ + { name: "section", settings: classes("hero"), children: [ + { name: "div", settings: classes("hero-copy"), children: [ + text("Springfield-area wood care specialists", ["eyebrow"]), + heading("Restore the wood you love. Protect it for years.", "h1"), + text("Professional cleaning, staining, sealing, and repair for outdoor wood surfaces across Southwest Missouri.", ["lede"]), + link("Request a free estimate", "#estimate", ["button"]), + ] }, + { name: "div", settings: classes("hero-media"), children: [ + image(media.hero, "Freshly restored and stained residential deck"), + text("Craftsmanship you can see in every board."), + ] }, + ] }, + { name: "section", settings: { ...classes("section"), _attributes: [{ name: "id", value: "services" }] }, children: [ + { name: "div", settings: classes("section-heading"), children: [ + text("Built for Missouri weather", ["eyebrow"]), heading("Complete care for outdoor wood", "h2"), + ] }, + { name: "div", settings: classes("service-grid"), children: [ + { name: "div", settings: classes("service-card"), children: [heading("Deck restoration", "h3"), text("Cleaning, preparation, staining, and sealing that restores color and protects the surface.")] }, + { name: "div", settings: classes("service-card"), children: [heading("Fence staining", "h3"), text("Even coverage and durable protection for privacy fences, gates, and decorative woodwork.")] }, + { name: "div", settings: classes("service-card"), children: [heading("Specialty wood care", "h3"), text("Experienced care for log homes, pergolas, pre-stain projects, and wood repairs.")] }, + ] }, + ] }, + { name: "section", settings: { ...classes("work"), _attributes: [{ name: "id", value: "work" }] }, children: [ + { name: "div", children: [text("Real local work", ["eyebrow"]), heading("Careful preparation. Clean results.", "h2"), text("Every project starts with the condition of the wood and ends with a finish selected for the surface, exposure, and desired look.")] }, + image(media.hero, "Restored deck project completed by Stain & Seal Pros"), + ] }, + { name: "section", settings: { ...classes("estimate"), _attributes: [{ name: "id", value: "estimate" }] }, children: [ + text("Ready to protect your investment?", ["eyebrow"]), heading("Tell us about your project.", "h2"), + text("Share the surface, approximate size, and where the project is located. We’ll help you choose the right next step."), + link("Start your estimate", "#estimate-form", ["button"]), + ] }, + ], + } +} + +const dashboard = await login() +const mcpAdapter = await installAndActivateMcpAdapter(dashboard) +const enabled = await enableAbilities() +const wpNonce = await freshRestNonce() +const { body: abilities } = await rest("/wp-json/wp-abilities/v1/abilities?per_page=100", wpNonce) +const abilityNames = Array.isArray(abilities) ? abilities.map((entry) => entry.name) : [] +for (const required of ["bricks/commit-site-foundation", "bricks/get-page-elements", "bricks/create-template", "bricks/list-global-classes", "bricks/list-global-variables"]) { + if (!abilityNames.includes(required)) throw new Error(`Required Bricks ability is absent after activation: ${required}`) +} + +const media = { + logo: await uploadMedia(wpNonce, logoPath, `stain-seal-pros-logo${mediaSuffix}.png`, "image/png", "Stain & Seal Pros"), + hero: await uploadMedia(wpNonce, heroPath, `stain-seal-pros-deck-after${mediaSuffix}.jpg`, "image/jpeg", "Freshly restored and stained residential deck"), +} + +const mcp = await openMcpSession(wpNonce) +let foundation +if (readOnlyReopen) { + foundation = { + workflow: "read-only-reopen", + transactionState: "observed", + resources: { + headerTemplateId: Number(process.env.BRICKS_HEADER_TEMPLATE_ID ?? 9), + footerTemplateId: Number(process.env.BRICKS_FOOTER_TEMPLATE_ID ?? 11), + homePageId: Number(process.env.BRICKS_HOME_PAGE_ID ?? 13), + homeUrl: `${origin}/`, + }, + replayed: true, + } +} else if (existingFoundation) { + const resources = { + headerTemplateId: Number(process.env.BRICKS_HEADER_TEMPLATE_ID ?? 9), + footerTemplateId: Number(process.env.BRICKS_FOOTER_TEMPLATE_ID ?? 11), + homePageId: Number(process.env.BRICKS_HOME_PAGE_ID ?? 13), + homeUrl: `${origin}/`, + } + const context = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/get-design-context", { responseFormat: "summary", limit: 100 }, 19) + if (!context.success) throw new Error(`Bricks design context failed before focused repair: ${JSON.stringify(context).slice(0, 2400)}`) + const classByName = new Map((context.data?.globalClasses ?? []).map((item) => [item.name, item.id])) + const trees = existingFoundationTrees(media, classByName) + const imports = [] + const createTemplates = process.env.BRICKS_CREATE_TEMPLATES === "1" + if (createTemplates) { + for (const target of [ + { name: "header", type: "header", elements: trees.header }, + { name: "footer", type: "footer", elements: trees.footer }, + ]) { + const created = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/create-template", { + title: `Site ${target.name[0].toUpperCase()}${target.name.slice(1)} Cloudflare ${process.env.BRICKS_MEDIA_SUFFIX ?? "native"}`, + type: target.type, + status: "publish", + elements: target.elements, + settings: { templateConditions: [{ main: "any" }] }, + }, 18 + imports.length) + if (!created.success) throw new Error(`Bricks ${target.name} template creation failed: ${JSON.stringify(created).slice(0, 2400)}`) + resources[`${target.name}TemplateId`] = created.data.templateId + imports.push({ name: `create-${target.name}`, postId: created.data.templateId, result: created.data }) + } + } + const targets = createTemplates + ? [{ name: "home", postId: resources.homePageId, elements: trees.home }] + : [ + { name: "header", postId: resources.headerTemplateId, elements: trees.header }, + { name: "footer", postId: resources.footerTemplateId, elements: trees.footer }, + { name: "home", postId: resources.homePageId, elements: trees.home }, + ] + for (const target of targets) { + const committed = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/set-page-elements", { + postId: target.postId, + elements: target.elements, + }, 20 + imports.length * 2) + if (!committed.success) throw new Error(`Bricks focused ${target.name} native save failed: ${JSON.stringify(committed).slice(0, 2400)}`) + imports.push({ name: target.name, postId: target.postId, result: committed.data }) + } + for (const templateId of [resources.headerTemplateId, resources.footerTemplateId]) { + const conditions = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/set-template-conditions", { + templateId, + conditions: [{ main: "any" }], + }, 40 + imports.length) + if (!conditions.success) throw new Error(`Bricks template condition save failed for ${templateId}: ${JSON.stringify(conditions).slice(0, 2400)}`) + imports.push({ name: `conditions-${templateId}`, postId: templateId, result: conditions.data }) + } + foundation = { workflow: "focused-foundation-repair", transactionState: "committed", resources, imports, replayed: false } +} else { + const executed = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/commit-site-foundation", foundationInput(media)) + if (!executed.success) throw new Error(`Bricks foundation failed through MCP: ${JSON.stringify(executed).slice(0, 2400)}`) + foundation = executed.data +} +const resources = foundation?.resources ?? {} +const pageId = resources.homePageId +if (!Number.isInteger(pageId)) throw new Error(`Bricks foundation did not return a homepage ID: ${JSON.stringify(foundation).slice(0, 2400)}`) +const homeElements = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/get-page-elements", { postId: pageId, responseFormat: "summary" }, 3) +const headerElements = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/get-page-elements", { postId: resources.headerTemplateId, responseFormat: "summary" }, 4) +const footerElements = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/get-page-elements", { postId: resources.footerTemplateId, responseFormat: "summary" }, 5) +const designContext = await mcpExecuteAbility(wpNonce, mcp.sessionId, "bricks/get-design-context", { responseFormat: "summary", limit: 50 }, 6) +for (const [label, value] of Object.entries({ homeElements, headerElements, footerElements, designContext })) { + if (!value.success) throw new Error(`${label} failed through MCP: ${JSON.stringify(value).slice(0, 2400)}`) +} +await closeMcpSession(wpNonce, mcp.sessionId) +const result = { + status: "native-foundation-committed", + origin, + wordpressLogin: true, + enabledAbilityCount: enabled.length, + registeredBricksAbilityCount: abilityNames.filter((name) => name.startsWith("bricks/")).length, + mcpAdapter, + mcpProtocolVersion: mcp.initialize?.protocolVersion ?? null, + media, + foundation, + nativeEvidence: { + home: homeElements.data, + header: headerElements.data, + footer: footerElements.data, + designContext: designContext.data, + }, + pageId, + generatedAt: new Date().toISOString(), +} + +await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`) +console.log(JSON.stringify(result)) diff --git a/packages/runtime-cloudflare/scripts/bricks-render-proof.mjs b/packages/runtime-cloudflare/scripts/bricks-render-proof.mjs new file mode 100644 index 00000000..491456c6 --- /dev/null +++ b/packages/runtime-cloudflare/scripts/bricks-render-proof.mjs @@ -0,0 +1,85 @@ +import { mkdir, writeFile } from "node:fs/promises" +import { chromium } from "playwright" + +const origin = process.env.BRICKS_PROBE_ORIGIN ?? "http://127.0.0.1:8798" +const outputDirectory = process.env.BRICKS_RENDER_OUTPUT ?? "outputs/bricks-render" + +await mkdir(outputDirectory, { recursive: true }) +const browser = await chromium.launch({ headless: true }) +const results = [] + +try { + for (const target of [ + { name: "desktop", width: 1440, height: 1000 }, + { name: "mobile", width: 390, height: 844 }, + ]) { + const context = await browser.newContext({ viewport: { width: target.width, height: target.height }, deviceScaleFactor: 1 }) + const page = await context.newPage() + const response = await page.goto(origin, { waitUntil: "networkidle", timeout: 30000 }) + if (!response?.ok()) throw new Error(`${target.name} homepage failed: ${response?.status() ?? "no response"}`) + await page.locator("h1").waitFor({ state: "visible", timeout: 10000 }) + const scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight) + for (let y = 0; y < scrollHeight; y += Math.max(320, target.height - 120)) { + await page.evaluate((nextY) => scrollTo(0, nextY), y) + await page.waitForTimeout(80) + } + await page.evaluate(() => scrollTo(0, 0)) + await page.waitForTimeout(150) + const evidence = await page.evaluate(() => { + const rect = (selector) => { + const node = document.querySelector(selector) + if (!(node instanceof HTMLElement)) return null + const box = node.getBoundingClientRect() + return { x: box.x, y: box.y, width: box.width, height: box.height } + } + const rootStyle = getComputedStyle(document.documentElement) + const heroStyle = getComputedStyle(document.querySelector(".hero")) + const serviceStyle = getComputedStyle(document.querySelector(".service-grid")) + const images = [...document.images].map((image) => ({ + alt: image.alt, + complete: image.complete, + naturalWidth: image.naturalWidth, + naturalHeight: image.naturalHeight, + src: image.currentSrc, + })) + return { + title: document.title, + h1: document.querySelector("h1")?.textContent?.trim() ?? "", + nativeElementCount: document.querySelectorAll("[id^='brxe-']").length, + sections: document.querySelectorAll("main .brxe-section").length, + headerVisible: Boolean(document.querySelector("#brx-header")?.getClientRects().length), + footerVisible: Boolean(document.querySelector("#brx-footer")?.getClientRects().length), + viewportWidth: innerWidth, + scrollWidth: document.documentElement.scrollWidth, + horizontalOverflow: document.documentElement.scrollWidth > innerWidth + 1, + heroColumns: heroStyle.gridTemplateColumns, + serviceColumns: serviceStyle.gridTemplateColumns, + hero: rect(".hero"), + heroCopy: rect(".hero-copy"), + heroMedia: rect(".hero-media"), + serviceGrid: rect(".service-grid"), + cssVariables: { + brandNavy: rootStyle.getPropertyValue("--brand-navy").trim(), + brandOrange: rootStyle.getPropertyValue("--brand-orange").trim(), + spaceM: rootStyle.getPropertyValue("--space-m").trim(), + textM: rootStyle.getPropertyValue("--text-m").trim(), + }, + images, + } + }) + if (evidence.horizontalOverflow) throw new Error(`${target.name} has horizontal overflow: ${JSON.stringify(evidence)}`) + if (evidence.h1 !== "Restore the wood you love. Protect it for years.") throw new Error(`${target.name} did not render the expected Bricks H1`) + if (evidence.nativeElementCount < 40 || evidence.sections !== 4) throw new Error(`${target.name} native Bricks structure is incomplete: ${JSON.stringify(evidence)}`) + if (evidence.images.length < 4 || evidence.images.some((image) => !image.complete || image.naturalWidth < 1)) throw new Error(`${target.name} media did not render: ${JSON.stringify(evidence.images)}`) + const screenshot = `${outputDirectory}/${target.name}.png` + await page.screenshot({ path: screenshot, fullPage: true }) + results.push({ ...target, screenshot, responseHeaders: await response.allHeaders(), evidence }) + await context.close() + } +} finally { + await browser.close() +} + +const report = { status: "rendered-desktop-and-mobile", origin, results, generatedAt: new Date().toISOString() } +await writeFile(`${outputDirectory}/report.json`, `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify(report)) diff --git a/packages/runtime-cloudflare/scripts/create-bricks-runtime-pack.py b/packages/runtime-cloudflare/scripts/create-bricks-runtime-pack.py new file mode 100755 index 00000000..5482f20b --- /dev/null +++ b/packages/runtime-cloudflare/scripts/create-bricks-runtime-pack.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Build the one-read PHP hydration pack from a retained Miniflare R2 revision.""" + +import argparse +import datetime +import hashlib +import json +import sqlite3 +import uuid +import zipfile +from pathlib import Path + + +def one(paths: list[Path], label: str) -> Path: + if len(paths) != 1: + raise RuntimeError(f"Expected one {label}; found {len(paths)}.") + return paths[0] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("state", type=Path) + parser.add_argument("source_revision") + parser.add_argument("output", type=Path) + args = parser.parse_args() + + r2 = args.state / "v3/r2" + db = one([path for path in (r2 / "miniflare-R2BucketObject").glob("*.sqlite") if path.name != "metadata.sqlite"], "Miniflare R2 database") + bucket = one([path for path in r2.iterdir() if path.is_dir() and path.name not in {"miniflare-R2BucketObject"}], "Miniflare R2 bucket") + blobs = bucket / "blobs" + connection = sqlite3.connect(db) + + def body(key: str) -> bytes: + row = connection.execute("SELECT blob_id,size FROM _mf_objects WHERE key=?", (key,)).fetchone() + if not row: + raise RuntimeError(f"Missing local R2 object: {key}") + data = (blobs / row[0]).read_bytes() + if len(data) != row[1]: + raise RuntimeError(f"Size mismatch: {key}") + return data + + source_key = f"sites/default/markdown/revisions/{args.source_revision}.json" + manifest = json.loads(body(source_key)) + markdown = manifest["files"] + wp_content = [ + file for file in manifest.get("wpContent", []) + if not file["path"].startswith("themes/bricks/assets/") + and not file["path"].startswith("themes/bricks/languages/") + ] + entries = [(f"markdown/{file['path']}", file) for file in markdown] + entries += [(f"wp-content/{file['path']}", file) for file in wp_content] + entries.sort(key=lambda item: item[0]) + + args.output.mkdir(parents=True, exist_ok=False) + pack_path = args.output / "runtime-restore-pack.zip" + decoded = 0 + with zipfile.ZipFile(pack_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive: + for name, file in entries: + data = body(file["objectKey"]) + if hashlib.sha256(data).hexdigest() != file["sha256"] or len(data) != file["size"]: + raise RuntimeError(f"Integrity mismatch: {file['objectKey']}") + info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0)) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + archive.writestr(info, data, compress_type=zipfile.ZIP_DEFLATED, compresslevel=6) + decoded += len(data) + tombstones = json.dumps({"wpContentDeleted": manifest.get("wpContentDeleted", [])}, separators=(",", ":")).encode() + info = zipfile.ZipInfo("metadata/wp-content-deleted.json", date_time=(1980, 1, 1, 0, 0, 0)) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + archive.writestr(info, tombstones, compress_type=zipfile.ZIP_DEFLATED, compresslevel=6) + + pack = pack_path.read_bytes() + pack_sha = hashlib.sha256(pack).hexdigest() + revision = str(uuid.uuid4()) + persisted_at = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + manifest.update({ + "revision": revision, + "manifestKey": f"sites/default/markdown/revisions/{revision}.json", + "persistedAt": persisted_at, + "restorePack": { + "schema": "wp-codebox/cloudflare-canonical-restore-pack/v1", + "objectKey": f"sites/default/restore-packs/{pack_sha}.zip", + "sha256": pack_sha, + "size": len(pack), + "fileCount": len(entries), + "decodedBytes": decoded, + }, + }) + manifest_path = args.output / f"{revision}.json" + manifest_path.write_text(json.dumps(manifest, separators=(",", ":"))) + pointer = {"revision": revision, "manifestKey": manifest["manifestKey"], "persistedAt": persisted_at} + (args.output / "pointer.json").write_text(json.dumps(pointer, separators=(",", ":"))) + print(json.dumps({**pointer, "restorePack": manifest["restorePack"], "sourceRevision": args.source_revision}, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/packages/runtime-cloudflare/scripts/local-gate.mjs b/packages/runtime-cloudflare/scripts/local-gate.mjs index 73478c5b..331abf33 100644 --- a/packages/runtime-cloudflare/scripts/local-gate.mjs +++ b/packages/runtime-cloudflare/scripts/local-gate.mjs @@ -37,7 +37,12 @@ function siteCredential(rootCredential, siteId, purpose) { } try { - await run("npm", ["run", "generate:wordpress-runtime-corpus"]) + // The Bricks feasibility harness reuses a freshly generated, checked + // runtime corpus when no local PHP CLI is available for the optional seed + // generator. Normal upstream gates retain their full generation step. + if (!process.env.WP_CODEBOX_REUSE_RUNTIME_CORPUS) { + await run("npm", ["run", "generate:wordpress-runtime-corpus"]) + } await run("npm", ["run", "provision:wordpress-runtime-corpus", "--", "--local", "--persist-to", stateDirectory]) const staticArtifactImport = await provisionStaticArtifact() await startWorker(!publicProvisioning && coordinator === "durable-object", publicProvisioning ? controlWranglerConfig : executionWranglerConfig) diff --git a/packages/runtime-cloudflare/scripts/native-bricks-build-interop.mts b/packages/runtime-cloudflare/scripts/native-bricks-build-interop.mts new file mode 100644 index 00000000..0e3391ec --- /dev/null +++ b/packages/runtime-cloudflare/scripts/native-bricks-build-interop.mts @@ -0,0 +1,72 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { execFileSync } from 'node:child_process'; +const [preparedPath, buildPath] = process.argv.slice(2); +if (!preparedPath || !buildPath) throw new Error('Usage: tsx scripts/native-bricks-build-interop.mts PREPARED_DIR BUILD_SOURCE_DIR'); +const directory = resolve(preparedPath), build = resolve(buildPath); +const load = (file: string) => import(pathToFileURL(resolve(build,file)).href); +const { WordPressBricksCloudflareWriter } = await load('src/product/runtime/wordpress-bricks-cloudflare-writer.ts'); +const { createWordPressBricksSiteArtifact } = await load('src/product/engine/wordpress-bricks-site-artifact.ts'); +const { nativeFixtureArtifact, emptyContentPointer } = await load('tests/unit/product-engine-wordpress-bricks-fixtures.ts'); +const prepared = JSON.parse(await readFile(`${directory}/prepared.json`,'utf8')); +const credentials = JSON.parse(await readFile(`${directory}/credentials.json`,'utf8')); +const siteId=prepared.siteId ?? 'default'; +const origin = process.env.NATIVE_API_ORIGIN ?? prepared.origin ?? 'http://127.0.0.1:8810'; +const buildCommit = prepared.build_commit ?? execFileSync('git',['-C',build,'rev-parse','HEAD'],{encoding:'utf8'}).trim(); +if (!prepared.build_commit) await writeFile(`${directory}/prepared.json`,JSON.stringify({...prepared,build_commit:buildCommit},null,2)); +const fixture = JSON.parse(new TextDecoder().decode((await nativeFixtureArtifact()).bytes)); +const strip = ({sha256, ...value}: any) => value; +const candidate = strip(fixture); delete candidate.schema; +candidate.site.site_id=siteId; candidate.runtime=prepared.runtime; +candidate.evidence_inputs.creative_provenance.canonicalCommit=buildCommit; +candidate.documents.pages=candidate.documents.pages.map((value:any)=>({...strip(value),status:'publish'})); +candidate.documents.templates=candidate.documents.templates.map((value:any)=>({...strip(value),status:'publish'})); +candidate.design_system.global_classes=candidate.design_system.global_classes.map(strip); +candidate.design_system.global_variables=candidate.design_system.global_variables.map(strip); +candidate.design_system.theme_style=strip(candidate.design_system.theme_style); +candidate.assets=candidate.assets.map(({sha256,bytes,...value}:any)=>value); +const first = await createWordPressBricksSiteArtifact(candidate); +candidate.documents.pages[0].title='Native API revised heading'; +candidate.documents.pages[0].elements[0].children[0].children[0].settings.text='Native API revised heading'; +const second = await createWordPressBricksSiteArtifact(candidate); +const writer = new WordPressBricksCloudflareWriter({baseUrl:'https://native-operator-rehearsal.invalid',bearerToken:credentials.apiToken,fetcher:async(request:Request)=>{ + const url=new URL(request.url); const response=await fetch(new Request(origin+url.pathname+url.search,request)); + if(!response.ok) await writeFile(`${directory}/last-http-error.json`,JSON.stringify({status:response.status,path:url.pathname,body:(await response.clone().text()).slice(0,12000)},null,2)); + return response; +}}); +const results:any[]=[]; +function request(action:string, artifact:any, base:any=null, restore:any=null,suffix=action){ + const doc=JSON.parse(new TextDecoder().decode(artifact.bytes));const op=`${process.env.NATIVE_API_RUN_ID ?? "build_native_v3"}_${suffix}`; + return {schema_version:1,target_runtime:'wordpress_bricks_cloudflare_v1',operation_id:op,idempotency_key:op,site_id:siteId,customer_id:doc.site.customer_id,bricks_artifact:{version:doc.schema,sha256:artifact.sha256},runtime_artifact:prepared.runtime,dossier:doc.evidence_inputs.dossier,creative_provenance:doc.evidence_inputs.creative_provenance,authorized_assets:doc.evidence_inputs.authorized_asset_hashes.map((sha256:string)=>({sha256,usage_status:'approved'})),starter_id:doc.site.starter_id,editing:doc.editing,license_secret_ref:'BRICKS_LICENSE_KEY',revision_receipt:base??emptyContentPointer(),rollback_receipt:{required:true,...(restore??base??emptyContentPointer())},protected_preview:{state:'requested',access:'agency_managed',url:null}}; +} +async function run(action:string,artifact:any,base:any=null,target:any=null){ + const input=request(action,artifact,base,target); + const result=action==='restore'?await writer.restore(input):await writer[action](input,artifact); + const replay=action==='restore'?await writer.restore(input):await writer[action](input,artifact); + if(JSON.stringify(result)!==JSON.stringify(replay))throw new Error('Replay changed terminal operation'); + const polled=await writer.readOperation(action,input); + if(JSON.stringify(result)!==JSON.stringify(polled))throw new Error('Poll changed terminal operation'); + const receipt=result.operation.receipt; + const previewUrl=receipt.protected_preview.url ?? (siteId==='default'?origin+'/':`${origin}/v1/bricks/sites/${siteId}/previews/${receipt.revision_receipt.receipt_id}/`); + const previewPath=new URL(previewUrl); + const rendered=await fetch(origin+previewPath.pathname+previewPath.search,{headers:{authorization:`Bearer ${credentials.apiToken}`}}); + const html=await rendered.text(); + await writeFile(`${directory}/last-render.json`,JSON.stringify({action,url:previewUrl,status:rendered.status,headers:Object.fromEntries(rendered.headers),body:html.slice(0,12000)},null,2)); + if(!rendered.ok)throw new Error(`Actual protected native render returned ${rendered.status}`); + const heading=action==='revise'?'Native API revised heading':'A brighter home'; + if(!html.includes(heading))throw new Error('Actual public native render omitted expected heading'); + results.push({action,request:input,operation:result.operation,public_heading_observed:heading,preview_url:previewUrl,idempotent_replay:true,poll_verified:true}); + await writeFile(`${directory}/build-interop-progress.json`,JSON.stringify({buildCommit,origin,results},null,2)); + console.log(JSON.stringify({action,state:result.operation.state,version:result.operation.receipt.revision_receipt.canonical_state_version})); + return result.operation.receipt; +} +await writeFile(`${directory}/build-artifact.json`,first.bytes); +const provisioned=await run('provision',first); +const revised=await run('revise',second,provisioned.revision_receipt); +let staleRejected=false; +try {await writer.revise(request('revise',first,provisioned.revision_receipt,null,'stale'),first);}catch(error:any){if(error.status!==409)throw error;staleRejected=true;} +if(!staleRejected)throw new Error('Stale base was accepted'); +const restored=await run('restore',first,revised.revision_receipt,provisioned.revision_receipt); +await writeFile(`${directory}/build-interop.json`,JSON.stringify({status:'real-native-provision-revise-restore-passed',buildCommit,origin,staleRejected,results},null,2)+'\n'); +console.log('Build producer and writer verified real PHP native provision, revision, stale rejection, restore, replay and public observation.'); diff --git a/packages/runtime-cloudflare/scripts/native-bricks-cold-reopen.mjs b/packages/runtime-cloudflare/scripts/native-bricks-cold-reopen.mjs new file mode 100644 index 00000000..98d3eb7e --- /dev/null +++ b/packages/runtime-cloudflare/scripts/native-bricks-cold-reopen.mjs @@ -0,0 +1,32 @@ +import {readFile,writeFile,mkdir} from 'node:fs/promises'; +import {resolve} from 'node:path'; +import {createHash} from 'node:crypto'; +import {chromium} from 'playwright'; +const directory=resolve(process.argv[2]); +const evidence=resolve(process.argv[3]); +const {apiToken}=JSON.parse(await readFile(`${directory}/credentials.json`,'utf8')); +const proof=JSON.parse(await readFile(`${directory}/build-interop.json`,'utf8')); +const restored=proof.results.find(result=>result.action==='restore').operation; +const response=await fetch(`${proof.origin}/v1/sites/${restored.siteId}/operations/${restored.id}`,{headers:{authorization:`Bearer ${apiToken}`}}); +const current=await response.json(); +if(response.status!==200||JSON.stringify(current.operation)!==JSON.stringify(restored))throw new Error('Cold operation receipt changed'); +await mkdir(evidence,{recursive:true}); +const browser=await chromium.launch({headless:true}); +const observations=[]; +for(const [name,width,height] of [['desktop',1440,1000],['mobile',390,844]]){ + const page=await browser.newPage({viewport:{width,height},deviceScaleFactor:1}); + const preview=restored.receipt.protected_preview.url ?? proof.origin+'/'; + await page.route("**/*", async route => { + if(new URL(route.request().url()).origin!==new URL(proof.origin).origin)return route.abort(); + await route.continue({headers:{...route.request().headers(),authorization:`Bearer ${apiToken}`}}); + }); + const response=await page.goto(preview,{waitUntil:'networkidle',timeout:60000}); + const observed=await page.evaluate(()=>({heading:document.querySelector('h1')?.textContent,nativeElements:document.querySelectorAll('[id^="brxe-"]').length,images:[...document.images].map(image=>({src:image.currentSrc,loaded:image.complete&&image.naturalWidth>0,width:image.naturalWidth})),overflow:document.documentElement.scrollWidth>innerWidth})); + if(response.status()!==200||observed.heading!=='A brighter home'||!observed.nativeElements||!observed.images.length||observed.images.some(image=>!image.loaded)||observed.overflow)throw new Error('Cold native render failed '+JSON.stringify(observed)); + await page.screenshot({path:`${evidence}/${name}.png`,fullPage:true}); + observations.push({viewport:name,status:response.status(),...observed,screenshot_sha256:createHash('sha256').update(await readFile(`${evidence}/${name}.png`)).digest('hex')}); + await page.close(); +} +await browser.close(); +await writeFile(`${evidence}/cold-reopen.json`,JSON.stringify({status:'cold-native-receipt-and-public-render-passed',origin:proof.origin,restored_revision:restored.receipt.revision_receipt,observations,qualification:'Programmatic native API proof only; licensed visual editor not run.'},null,2)+'\n'); +console.log(JSON.stringify({status:'cold-reopen-passed',version:restored.receipt.revision_receipt.canonical_state_version,viewports:observations.map(value=>({viewport:value.viewport,nativeElements:value.nativeElements,images:value.images.length,overflow:value.overflow}))})); diff --git a/packages/runtime-cloudflare/scripts/prepare-bricks-2.4-rc-probe.py b/packages/runtime-cloudflare/scripts/prepare-bricks-2.4-rc-probe.py new file mode 100644 index 00000000..93a3b21c --- /dev/null +++ b/packages/runtime-cloudflare/scripts/prepare-bricks-2.4-rc-probe.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Build the exact Bricks 2.4-rc compatibility artifact used by this probe.""" + +from __future__ import annotations + +import argparse +import hashlib +import tempfile +import zipfile +from pathlib import Path + +SOURCE_SHA256 = "a97171e889ff82a92949dff994f10ceeb38cc70d46d7df3d97d3522562c2e887" +OUTPUT_SHA256 = "fb36858225fbf4511d22684046bcbec275f84689fdd2f0e80f312da4fd72527a" + +REPLACEMENTS = { + "bricks/includes/abilities/site-foundation.php": [ + ( + "\t\t\t\t\t\t\t'buttons' => [ 'background' => [ 'color' => [ 'raw' => \"var(--{$primary})\" ] ] ],", + "\t\t\t\t\t\t\t'button' => [ 'background' => [ 'color' => [ 'raw' => \"var(--{$primary})\" ] ] ],", + ) + ], + "bricks/includes/html-to-bricks/element-mapper.php": [ + ( + """\t\t\t$bricks_element['settings']['_importImage'] = [ +\t\t\t\t'url' => $src, +\t\t\t\t'alt' => $alt ? $alt : '', +\t\t\t]; + +\t\t\t$bricks_element['settings']['image'] = [ +\t\t\t\t'url' => $src, +\t\t\t\t'isPlaceholder' => true, +\t\t\t];""", + """\t\t\t$bricks_element['settings']['image'] = [ +\t\t\t\t'url' => $src, +\t\t\t\t'external' => true, +\t\t\t];""", + ) + ], + "bricks/includes/abilities/workspace.php": [ + ( + """\t\t$previous_global_data = \\Bricks\\Database::$global_data; +\t\t$previous_asset_state = ( new \\ReflectionClass( \\Bricks\\Assets::class ) )->getStaticProperties(); +\t\t$previous_theme_state = ( new \\ReflectionClass( \\Bricks\\Theme_Styles::class ) )->getStaticProperties();""", + """\t\t$previous_global_data = \\Bricks\\Database::$global_data; +\t\t$asset_reflection = new \\ReflectionClass( \\Bricks\\Assets::class ); +\t\t$theme_reflection = new \\ReflectionClass( \\Bricks\\Theme_Styles::class ); +\t\t$previous_asset_state = $asset_reflection->getStaticProperties(); +\t\t$previous_theme_state = $theme_reflection->getStaticProperties();""", + ), + ( + "\t\t\t\t\\Bricks\\Assets::${$property} = $value;", + "\t\t\t\t$asset_reflection->getProperty( $property )->setValue( null, $value );", + ), + ( + "\t\t\t\t\\Bricks\\Theme_Styles::${$property} = $value;", + "\t\t\t\t$theme_reflection->getProperty( $property )->setValue( null, $value );", + ), + ], +} + + +def digest(path: Path) -> str: + sha = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + sha.update(chunk) + return sha.hexdigest() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("source", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + source = args.source.expanduser().resolve() + output = args.output.expanduser().resolve() + if digest(source) != SOURCE_SHA256: + raise SystemExit(f"unexpected source SHA-256: {digest(source)}") + + with tempfile.TemporaryDirectory(prefix="bricks-2.4-rc-probe-") as temporary: + root = Path(temporary) + with zipfile.ZipFile(source) as archive: + archive.extractall(root) + for relative, replacements in REPLACEMENTS.items(): + path = root / relative + text = path.read_text() + for old, new in replacements: + if text.count(old) != 1: + raise SystemExit(f"expected exactly one patch target in {relative}") + text = text.replace(old, new) + path.write_text(text) + + output.parent.mkdir(parents=True, exist_ok=True) + # Keep the source artifact's entry order and metadata so the output hash + # is reproducible and directly comparable with the probe artifact. + with zipfile.ZipFile(source) as original, zipfile.ZipFile(output, "w") as patched: + for info in original.infolist(): + path = root / info.filename + data = b"" if info.is_dir() else path.read_bytes() + patched.writestr(info, data) + + actual = digest(output) + if actual != OUTPUT_SHA256: + raise SystemExit(f"unexpected output SHA-256: {actual}") + print(f"{actual} {output}") + + +if __name__ == "__main__": + main() diff --git a/packages/runtime-cloudflare/scripts/prepare-native-bricks-allocation.py b/packages/runtime-cloudflare/scripts/prepare-native-bricks-allocation.py new file mode 100644 index 00000000..2c360ed4 --- /dev/null +++ b/packages/runtime-cloudflare/scripts/prepare-native-bricks-allocation.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Prepare an empty, isolated local D1/R2 Bricks allocation; never writes its source. + +The source is a retained Miniflare fixture containing the agency-authorized runtime +package. WordPress baseline data comes from the checked-in canonical seed. Customer +pages, media, design resources, histories, and credentials are not copied. +""" +import argparse, base64, datetime, hashlib, io, json, re, secrets, sqlite3, uuid, zipfile +from urllib.parse import urlparse +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--source-state', type=Path, required=True) + parser.add_argument('--destination-state', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--runtime-zip', type=Path, required=True) + parser.add_argument('--runtime-version', default='2.4-rc') + parser.add_argument('--port', type=int, default=8809) + parser.add_argument('--site-id', default='default') + parser.add_argument('--origin') + args = parser.parse_args() + if not re.fullmatch('[a-z0-9]+(?:-[a-z0-9]+)*',args.site_id) or len(args.site_id)>63: raise RuntimeError('Invalid site ID.') + site=args.site_id; origin=args.origin or 'http://127.0.0.1:'+str(args.port) + parsed_origin=urlparse(origin) + if parsed_origin.path or parsed_origin.query or parsed_origin.fragment or parsed_origin.username or not parsed_origin.hostname: raise RuntimeError('Origin must be canonical.') + package = Path(__file__).resolve().parents[1] + source, dest, output = args.source_state.resolve(), args.destination_state.resolve(), args.output.resolve() + if dest.exists() or output.exists() or dest == source or source in dest.parents: + raise RuntimeError('Destination state and output must be new directories outside the source.') + dest.mkdir(parents=True); output.mkdir(parents=True) + source_d1 = next(path for path in (source/'v3/d1/miniflare-D1DatabaseObject').glob('*.sqlite') if path.name != 'metadata.sqlite') + source_r2 = next(path for path in (source/'v3/r2/miniflare-R2BucketObject').glob('*.sqlite') if path.name != 'metadata.sqlite') + source_blobs = next(path/'blobs' for path in (source/'v3/r2').iterdir() if path.is_dir() and path.name != 'miniflare-R2BucketObject') + source_database, source_objects = sqlite3.connect(source_d1), sqlite3.connect(source_r2) + current = source_database.execute("SELECT revision,manifest_key,persisted_at FROM wp_codebox_state WHERE site_id='default'").fetchone() + if not current or not current[0]: raise RuntimeError('Source fixture has no canonical pointer.') + def body(key): + row = source_objects.execute('SELECT blob_id,size FROM _mf_objects WHERE key=?',(key,)).fetchone() + if not row: raise RuntimeError('Source object unavailable: '+key) + data = (source_blobs/row[0]).read_bytes() + if len(data) != row[1]: raise RuntimeError('Source object size mismatch.') + return data + manifest = json.loads(body(current[1])) + if not any(file['path']=='themes/bricks/style.css' for file in manifest['wpContent']): raise RuntimeError('Source fixture has no Bricks runtime.') + object_db = dest/source_r2.relative_to(source); object_db.parent.mkdir(parents=True) + objects = sqlite3.connect(object_db); source_objects.backup(objects) + objects.execute('DELETE FROM _mf_objects') + blobs = dest/source_blobs.relative_to(source); blobs.mkdir(parents=True) + def put(key,data,content_type='application/octet-stream'): + blob = uuid.uuid4().hex + (blobs/blob).write_bytes(data) + objects.execute('INSERT OR REPLACE INTO _mf_objects(key,blob_id,version,size,etag,uploaded,checksums,http_metadata,custom_metadata) VALUES(?,?,?,?,?,?,?,?,?)',(key,blob,uuid.uuid4().hex,len(data),hashlib.md5(data).hexdigest(),int(datetime.datetime.now().timestamp()*1000),'{}',json.dumps({'contentType':content_type}),'{}')) + # Runtime archives/manifests are global; omit every previous site's state. + for (key,) in source_objects.execute("SELECT key FROM _mf_objects WHERE key NOT LIKE 'sites/%'"): + put(key,body(key)) + # Vendor files come directly from the pinned ZIP, never from a patched proof fixture. + wp_content = [file for file in manifest['wpContent'] if not file['path'].startswith('themes/bricks/')] + for file in wp_content: + data = body(file['objectKey']) + if hashlib.sha256(data).hexdigest() != file['sha256']: raise RuntimeError('Runtime file digest mismatch.') + file['objectKey']='sites/'+site+'/wp-content/objects/'+file['sha256'] + put(file['objectKey'],data) + with zipfile.ZipFile(args.runtime_zip) as vendor_zip: + for entry in vendor_zip.infolist(): + if entry.is_dir(): continue + if not entry.filename.startswith('bricks/') or '..' in Path(entry.filename).parts: raise RuntimeError('Unexpected Bricks ZIP path.') + data=vendor_zip.read(entry); digest=hashlib.sha256(data).hexdigest() + record={'path':'themes/'+entry.filename,'objectKey':'sites/'+site+'/wp-content/objects/'+digest,'sha256':digest,'size':len(data)} + put(record['objectKey'],data); wp_content.append(record) + wp_content.sort(key=lambda file:file['path']) + with zipfile.ZipFile(package/'assets/markdown-database-integration-canonical-seed.zip') as seed: + files = {name:seed.read(name) for name in seed.namelist() if not name.endswith('.md')} + for file in manifest['files']: + if file['path'] in {'_options/template.json','_options/stylesheet.json','_options/bricks_mcp_settings.json','_options/bricks_global_settings.json'}: + files[file['path']] = body(file['objectKey']) + def option(name,value): + path = '_options/'+name+'.json'; row=json.loads(files[path]); row['option_value']=value + files[path]=json.dumps(row,indent=4).encode() + option('home',origin); option('siteurl',origin) + option('blogname','Native Bricks API rehearsal'); option('blogdescription','Isolated native runtime API proof') + option('page_on_front','0'); option('show_on_front','posts'); option('blog_public','0') + password,token,claim = secrets.token_urlsafe(32),secrets.token_urlsafe(32),secrets.token_urlsafe(32) + users=json.loads(files['_tables/users.json']); users[0]['user_pass']=hashlib.md5(password.encode()).hexdigest(); users[0]['user_email']='operator@native-rehearsal.invalid'; users[0]['user_url']='' + files['_tables/users.json']=json.dumps(users,indent=4).encode() + canonical=[] + for path,data in sorted(files.items()): + sha=hashlib.sha256(data).hexdigest(); key='sites/'+site+'/markdown/objects/'+sha + put(key,data); canonical.append({'path':path,'objectKey':key,'sha256':sha,'size':len(data)}) + revision=str(uuid.uuid4()); stamp=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='milliseconds').replace('+00:00','Z') + pointer={'revision':revision,'manifestKey':'sites/'+site+'/markdown/revisions/'+revision+'.json','persistedAt':stamp} + clean={**pointer,'files':canonical,'uploads':[],'wpContent':wp_content,'wpContentDeleted':[]} + # Prepare the one-read PHP reconstruction pack outside Worker request limits. + runtime_files=[file for file in wp_content if not file['path'].startswith(('themes/bricks/assets/','themes/bricks/languages/'))] + packed=[('markdown/'+file['path'],file) for file in canonical]+[('wp-content/'+file['path'],file) for file in runtime_files] + buffer=io.BytesIO() + with zipfile.ZipFile(buffer,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=6) as pack: + for name,file in sorted(packed): + blob=objects.execute('SELECT blob_id FROM _mf_objects WHERE key=?',(file['objectKey'],)).fetchone()[0] + pack.writestr(name,(blobs/blob).read_bytes()) + pack.writestr('metadata/wp-content-deleted.json','{"wpContentDeleted":[]}') + packed_bytes=buffer.getvalue(); digest=hashlib.sha256(packed_bytes).hexdigest() + metadata={'schema':'wp-codebox/cloudflare-canonical-restore-pack/v1','objectKey':'sites/'+site+'/restore-packs/'+digest+'.zip','sha256':digest,'size':len(packed_bytes),'fileCount':len(packed),'decodedBytes':sum(file['size'] for _,file in packed)} + put(metadata['objectKey'],packed_bytes,'application/zip'); clean['restorePack']=metadata + put(pointer['manifestKey'],json.dumps(clean,separators=(',',':')).encode(),'application/json'); objects.commit() + target_d1=dest/source_d1.relative_to(source); target_d1.parent.mkdir(parents=True) + database=sqlite3.connect(target_d1); source_database.backup(database); database.execute('PRAGMA foreign_keys=OFF') + for (name,) in database.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_cf_%' AND name NOT LIKE 'sqlite_%'").fetchall(): + database.execute('DELETE FROM "'+name.replace('"','""')+'"') + database.execute('INSERT INTO wp_codebox_state(site_id,revision,manifest_key,persisted_at,version) VALUES(?,?,?,?,1)',(site,revision,pointer['manifestKey'],stamp)) + database.execute('INSERT INTO wp_codebox_commits(site_id,version,revision,manifest_key,persisted_at) VALUES(?,1,?,?,?)',(site,revision,pointer['manifestKey'],stamp)); database.commit() + runtime={'version':args.runtime_version,'sha256':hashlib.sha256(args.runtime_zip.read_bytes()).hexdigest()} + config={'name':'native-bricks-api-local-rehearsal','main':str(package/'src/worker-d1.ts'),'compatibility_date':'2026-07-18','compatibility_flags':['nodejs_compat'],'limits':{'cpu_ms':300000},'r2_buckets':[{'binding':'WORDPRESS_STATE_BUCKET','bucket_name':source_blobs.parent.name}],'d1_databases':[{'binding':'WORDPRESS_STATE_DATABASE','database_name':'wp-codebox-runtime-state','database_id':'00000000-0000-0000-0000-000000000000'}],'rules':[{'type':'CompiledWasm','globs':['**/*.wasm'],'fallthrough':False},{'type':'Data','globs':['**/*.sqlite','**/*-runtime.zip','**/*-canonical-seed.zip'],'fallthrough':False}],'vars':{'WORDPRESS_SITE_CONTEXTS':json.dumps([{'id':site,'hostname':parsed_origin.hostname,'origin':origin}]),'WORDPRESS_BRICKS_PREPARED_ALLOCATIONS':json.dumps({site:{**pointer,'runtime':runtime}}),'WORDPRESS_NATIVE_DEPLOYMENT_VERSION':'local-native-api-rehearsal'}} + (output/'wrangler.jsonc').write_text(json.dumps(config,indent=2)+'\n') + expires=(datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(days=1)).isoformat(timespec='milliseconds').replace('+00:00','Z') + credentials=[{'id':'native-rehearsal','principal':'native-rehearsal','digest':hashlib.sha256(token.encode()).hexdigest(),'scopes':['sites:create','sites:read','sites:import'],'expiresAt':expires,'maxSites':1,'sites':[site]}] + secret_values={'WORDPRESS_API_TOKENS':json.dumps(credentials),'WORDPRESS_ADMIN_PASSWORD':password,'WORDPRESS_ADMIN_CLAIM_SECRET':claim,'WORDPRESS_AUTH_SECRET':secrets.token_urlsafe(48)} + (output/'secrets.json').write_text(json.dumps(secret_values)); (output/'secrets.json').chmod(0o600) + (output/'.dev.vars').write_text('\n'.join(k+"='"+v+"'" for k,v in secret_values.items())+'\n'); (output/'.dev.vars').chmod(0o600) + (output/'credentials.json').write_text(json.dumps({'apiToken':token,'adminPassword':password})); (output/'credentials.json').chmod(0o600) + receipt={'schema':'wp-codebox/native-bricks-prepared-allocation/v1','environment':'local-only','siteId':site,'origin':origin,'sourceRevision':current[0],'pointer':pointer,'runtime':runtime,'emptyNativeDocuments':True,'uploadCount':0,'canonicalFileCount':len(canonical),'runtimeFileCount':len(wp_content),'stateDirectory':str(dest),'config':str(output/'wrangler.jsonc')} + (output/'prepared.json').write_text(json.dumps(receipt,indent=2)+'\n'); print(json.dumps(receipt)) + +if __name__=='__main__':main() diff --git a/packages/runtime-cloudflare/scripts/provision-wordpress-runtime-corpus.mjs b/packages/runtime-cloudflare/scripts/provision-wordpress-runtime-corpus.mjs index cf122aaf..37bacbca 100644 --- a/packages/runtime-cloudflare/scripts/provision-wordpress-runtime-corpus.mjs +++ b/packages/runtime-cloudflare/scripts/provision-wordpress-runtime-corpus.mjs @@ -10,7 +10,10 @@ const local = args.includes("--local") const remote = args.includes("--remote") const persistIndex = args.indexOf("--persist-to") const persistTo = persistIndex === -1 ? undefined : args[persistIndex + 1] +const bucketIndex = args.indexOf("--bucket") +const bucket = bucketIndex === -1 ? "wp-codebox-runtime-chubes" : args[bucketIndex + 1] if (local === remote || (local && !persistTo)) throw new Error("Use exactly one of --local --persist-to or --remote.") +if (!bucket || !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(bucket)) throw new Error("--bucket must be a valid R2 bucket name.") const manifest = JSON.parse(await readFile(resolve(packageRoot, "assets/wordpress-runtime-artifact.json"), "utf8")) const archive = await readFile(resolve(packageRoot, "artifacts/cloudflare-wordpress-runtime-corpus.zip")) @@ -35,7 +38,7 @@ for (const artifact of [ { key: sqliteManifest.key, size: sqliteManifest.archive.size, file: resolve(packageRoot, "artifacts/cloudflare-sqlite-database-integration.zip") }, { key: websiteImporterManifest.key, size: websiteImporterManifest.archive.size, file: resolve(packageRoot, "artifacts/cloudflare-website-importer.zip") }, ]) { - const command = ["r2", "object", "put", `wp-codebox-runtime-chubes/${artifact.key}`, "--file", artifact.file, local ? "--local" : "--remote"] + const command = ["r2", "object", "put", `${bucket}/${artifact.key}`, "--file", artifact.file, local ? "--local" : "--remote"] if (persistTo) command.push("--persist-to", persistTo) await new Promise((resolve, reject) => { const child = spawn("wrangler", command, { cwd: packageRoot, env: childEnvironment(), stdio: "inherit" }) diff --git a/packages/runtime-cloudflare/scripts/upload-native-bricks-canary-allocation.py b/packages/runtime-cloudflare/scripts/upload-native-bricks-canary-allocation.py new file mode 100644 index 00000000..0844efc2 --- /dev/null +++ b/packages/runtime-cloudflare/scripts/upload-native-bricks-canary-allocation.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Upload only a prepared native rehearsal namespace to the existing disposable canary.""" +import concurrent.futures, hashlib, json, sqlite3, sys, time, tomllib +from pathlib import Path +from urllib.request import Request, urlopen +from urllib.error import HTTPError +ACCOUNT='c9a6d8fc0e01d908d3d399d12043b2fb' +BUCKET='bricks24-native-canary-20260910' +D1='fe4d6486-6cd6-4a2e-9542-4f2dd8af7d8a' +output=Path(sys.argv[1]).resolve() +prepared=json.loads((output/'prepared.json').read_text()) +site=prepared.get('siteId') +if site not in {'native-api','native-engine','native-sales-rehearsal'}: raise RuntimeError('This bounded uploader accepts only explicitly named isolated native rehearsal namespaces.') +state=Path(prepared['stateDirectory'])/'v3' +oauth=tomllib.loads((Path.home()/'Library/Preferences/.wrangler/config/default.toml').read_text())['oauth_token'] +base='https://api.cloudflare.com/client/v4/accounts/'+ACCOUNT + +def api(path,method='GET',data=None,headers=None): + for attempt in range(8): + try: + with urlopen(Request(base+path,data=data,method=method,headers={'Authorization':'Bearer '+oauth,**(headers or {})}),timeout=90) as response: + return response.read() + except HTTPError as error: + if error.code in [429,500,502,503,504] and attempt<7: + time.sleep(min(60,int(error.headers.get('retry-after','0')) or 2**attempt));continue + raise RuntimeError('Cloudflare operation failed: HTTP '+str(error.code)+' '+error.read().decode()[:300]) from None + +def query(sql,params=[]): + response=json.loads(api('/d1/database/'+D1+'/query','POST',json.dumps({'sql':sql,'params':params}).encode(),{'Content-Type':'application/json'})) + if not response.get('success'): raise RuntimeError('D1 query failed: '+str(response.get('errors'))) + return response['result'] + +prior=query("SELECT site_id,revision,manifest_key,persisted_at,version FROM wp_codebox_state WHERE site_id IN ('default',?)",[site])[0]['results'] +if any(row['site_id']==site for row in prior): raise RuntimeError('native-api already has a canonical pointer; no upload or reset performed.') +(output/'remote-before.json').write_text(json.dumps(prior,indent=2)) +db=sqlite3.connect(next(file for file in (state/'r2/miniflare-R2BucketObject').glob('*.sqlite') if file.name!='metadata.sqlite')) +objects=db.execute("SELECT key,blob_id,size,http_metadata FROM _mf_objects WHERE key LIKE ?",('sites/'+site+'/%',)).fetchall() +blobs=state/'r2/wp-codebox-runtime-chubes/blobs' +progress_file=output/'uploaded-objects.jsonl' +completed={json.loads(line)['key'] for line in progress_file.read_text().splitlines()} if progress_file.exists() else set() + +def upload(row): + key,blob,size,metadata=row + content=(blobs/blob).read_bytes() + if len(content)!=size: raise RuntimeError('Prepared object size mismatch') + digest=hashlib.sha256(content).hexdigest() + if '/objects/' in key and key.rsplit('/',1)[1]!=digest: raise RuntimeError('Prepared immutable object digest mismatch') + if key not in completed: + api('/r2/buckets/'+BUCKET+'/objects/'+key,'PUT',content,{'Content-Type':json.loads(metadata).get('contentType','application/octet-stream')}) + return {'key':key,'sha256':digest,'bytes':size} + +with progress_file.open('a') as log, concurrent.futures.ThreadPoolExecutor(max_workers=6) as pool: + for index,receipt in enumerate(pool.map(upload,objects),1): + if receipt['key'] not in completed:log.write(json.dumps(receipt)+'\n');log.flush() + if index%100==0 or index==len(objects):print(json.dumps({'uploaded':index,'total':len(objects)}),flush=True) +pointer=prepared['pointer'] +query('INSERT INTO wp_codebox_state(site_id,revision,manifest_key,persisted_at,version) VALUES(?,?,?,?,1)', [site,pointer['revision'],pointer['manifestKey'],pointer['persistedAt']]) +query('INSERT INTO wp_codebox_commits(site_id,version,revision,manifest_key,persisted_at) VALUES(?,1,?,?,?)',[site,pointer['revision'],pointer['manifestKey'],pointer['persistedAt']]) +after=query("SELECT site_id,revision,manifest_key,persisted_at,version FROM wp_codebox_state WHERE site_id IN ('default',?)",[site])[0]['results'] +if next(row for row in prior if row['site_id']=='default')!=next(row for row in after if row['site_id']=='default'):raise RuntimeError('Default canonical pointer changed during preparation') +receipt={'status':'isolated-remote-allocation-prepared','account':ACCOUNT,'bucket':BUCKET,'database':D1,'objects':len(objects),'bytes':sum(row[2] for row in objects),'before':prior,'after':after} +(output/'remote-prepared.json').write_text(json.dumps(receipt,indent=2)+'\n');print(json.dumps(receipt),flush=True) diff --git a/packages/runtime-cloudflare/src/bricks-clock-compatibility.ts b/packages/runtime-cloudflare/src/bricks-clock-compatibility.ts new file mode 100644 index 00000000..eeeca202 --- /dev/null +++ b/packages/runtime-cloudflare/src/bricks-clock-compatibility.ts @@ -0,0 +1,25 @@ +/** + * PHP uniqid() polls gettimeofday until the microsecond changes. Cloudflare's + * clock is frozen until I/O, so repeated IDs can stall in that loop. Preserve + * Bricks' six-character ID generation while supplying random entropy directly. + * Existing document IDs and the echo/return contract are untouched. + */ +export function patchBricksRandomIds(source: string): string { + const before = "self::generate_hash( md5( uniqid( rand(), true ) ) )"; + const after = "self::generate_hash( bin2hex( random_bytes( 16 ) ) )"; + if (source.includes(after) && !source.includes(before)) return source; + if (source.split(before).length !== 2) throw new Error("Unsupported Bricks random ID implementation."); + return source.replace(before, after); +} + +/** Undo the earlier public-request pruning: element controls need Settings_Base. */ +export function restoreBricksSettings(source: string): string { + const conditional = "\t\t\t$this->settings = new Settings();"; + const templates = "\t\t$this->templates = new Templates();"; + const restored = "\t\t$this->settings = new Settings();"; + if (!source.includes(conditional) && source.includes(restored)) return source; + if (source.split(conditional).length !== 2 || source.split(templates).length !== 2) { + throw new Error("Unsupported Bricks conditional Settings implementation."); + } + return source.replace(conditional, "").replace(templates, `${templates}\n${restored}`); +} diff --git a/packages/runtime-cloudflare/src/bricks-preview.ts b/packages/runtime-cloudflare/src/bricks-preview.ts new file mode 100644 index 00000000..910f779b --- /dev/null +++ b/packages/runtime-cloudflare/src/bricks-preview.ts @@ -0,0 +1,109 @@ +import { authenticateProvisioningRequest, provisioningTokenAllowsSite } from './provisioning-api.js' +import { parseSiteContexts, type SiteContext } from './site-context.js' +import type { NativeEnv, NativePointer } from './native-bricks-api.js' +import type { MarkdownPointer, RevisionState } from './revision-coordinator.js' + +export interface NativePreviewBackend { + state(site: SiteContext): Promise + render(request: Request, site: SiteContext, pointer: MarkdownPointer): Promise +} +const prefix = /^\/v1\/bricks\/sites\/([a-z0-9-]{1,63})\/previews\/([A-Za-z0-9_-]{1,160})(\/.*)?$/ +const browserAsset = /\.(?:css|js|mjs|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|mp4|webm|pdf)$/i +const privateHeaders = { 'cache-control': 'private, no-store', 'x-robots-tag': 'noindex, nofollow, noarchive', 'referrer-policy': 'no-referrer' } +function failure(status: number, code: string) { return Response.json({ error: { code } }, { status, headers: privateHeaders }) } + +/** Native allocations are protected from their first preparation, even before + * a successful mutation receipt exists. Persisted ownership retains protection + * if an operator later removes the preparation entry. The public default POC + * is deliberately outside this customer-allocation policy. */ +export async function isProtectedNativeSite(env: NativeEnv, site: SiteContext): Promise { + if (site.id === 'default') return false + if (Object.hasOwn(JSON.parse(env.WORDPRESS_BRICKS_PREPARED_ALLOCATIONS ?? '{}'), site.id)) return true + const exists = await env.WORDPRESS_STATE_DATABASE.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='wp_codebox_native_operations'").first() + return !!exists && !!await env.WORDPRESS_STATE_DATABASE.prepare('SELECT site_id FROM wp_codebox_native_operations WHERE site_id=? LIMIT 1').bind(site.id).first() +} + +export function nativePreviewPathAllowed(path: string, search: URLSearchParams): boolean { + let decoded: string + try { decoded = decodeURIComponent(path) } catch { return false } + if (decoded !== path || !path.startsWith('/') || path.includes('//') || /[\\\x00-\x20]/.test(path) || path.split('/').some(part => part === '.' || part === '..')) return false + if (path.startsWith('/__') || /^\/(?:wp-admin|wp-json|v1|r2)(?:\/|$)/i.test(path) || /\.php(?:\/|$)/i.test(path)) return false + const asset = path.startsWith('/wp-content/') || path.startsWith('/wp-includes/') + if (asset && !browserAsset.test(path)) return false + if (!asset && path !== '/' && !/^\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]*\/?$/.test(path)) return false + for (const [key,value] of search) { + if (asset ? key !== 'ver' || !/^[A-Za-z0-9_.-]{1,100}$/.test(value) : !['p','page_id','paged'].includes(key) || !/^[1-9][0-9]{0,9}$/.test(value)) return false + } + return true +} + +export function rewriteNativePreview(text: string, siteOrigin: string, mount: string): string { + // WordPress absolute URLs, including inline JSON, plus root-relative HTML/CSS + // URLs. Relative CSS paths already resolve inside the same protected mount. + text = text.replace(/((?:href|src|action|poster|srcset)\s*=\s*["']|url\(\s*["']?)(\/)(?!\/)/gi, (_all, before) => `${before}${mount}/`) + .replace(/(,\s*)(\/wp-(?:content|includes)\/)/g, `$1${mount}$2`) + return text.split(siteOrigin.replaceAll('/', '\\/')).join(mount.replaceAll('/', '\\/')) + .split(siteOrigin).join(mount).split(siteOrigin.replace(/^https?:/, '')).join(mount) +} + +export async function routeNativeBricksPreview(request: Request, env: NativeEnv, backend: NativePreviewBackend): Promise { + const url = new URL(request.url) + const match = prefix.exec(url.pathname) + if (!match) return url.pathname.includes('/previews/') && url.pathname.startsWith('/v1/bricks/') ? failure(404,'not_found') : null + if (request.method !== 'GET' && request.method !== 'HEAD') return failure(405,'read_only_preview') + const token = await authenticateProvisioningRequest(request, env, 'sites:read') + if (token instanceof Response) return new Response(token.body, { status: token.status, headers: {...Object.fromEntries(token.headers), ...privateHeaders} }) + const [,siteId,receiptId] = match + if (!provisioningTokenAllowsSite(token,siteId)) return failure(404,'not_found') + const site = parseSiteContexts(env.WORDPRESS_SITE_CONTEXTS).find(site => site.id === siteId) + if (!site) return failure(404,'not_found') + // Never advertise protection while the same customer namespace is public. + if (!await isProtectedNativeSite(env,site)) return failure(409,'native_allocation_not_protected') + const exists = await env.WORDPRESS_STATE_DATABASE.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='wp_codebox_native_operations'").first() + if (!exists) return failure(404,'not_found') + const row = await env.WORDPRESS_STATE_DATABASE.prepare(`SELECT receipt_json,generation FROM wp_codebox_native_operations WHERE site_id=? AND principal=? AND state='succeeded' + AND json_extract(receipt_json,'$.revision_receipt.receipt_id')=? LIMIT 1`).bind(siteId,token.principal,receiptId).first<{receipt_json:string;generation:number}>() + if (!row) return failure(404,'not_found') + const lifecycle = await env.WORDPRESS_STATE_DATABASE.prepare(`SELECT s.generation,s.state,l.state AS lifecycle_state,l.expires_at FROM wp_codebox_sites s + LEFT JOIN wp_codebox_site_lifecycles l ON l.site_id=s.site_id AND l.generation=s.generation WHERE s.site_id=?`).bind(siteId).first<{generation:number;state:string;lifecycle_state:string|null;expires_at:number|null}>() + if (!lifecycle || lifecycle.generation !== row.generation || lifecycle.state !== 'active' || (lifecycle.lifecycle_state !== null && (lifecycle.lifecycle_state !== 'active' || (lifecycle.expires_at ?? 0) <= Date.now()))) return failure(410,'allocation_unavailable') + const receipt = JSON.parse(row.receipt_json) + const pointer: NativePointer = receipt.revision_receipt + if (receipt.site_id !== site.id || pointer.receipt_id !== receiptId) return failure(409,'receipt_mismatch') + const current = (state: RevisionState) => state.version === pointer.canonical_state_version && state.pointer?.revision === pointer.canonical_state_revision && state.pointer.manifestKey === pointer.canonical_manifest_key && state.pointer.persistedAt === pointer.canonical_persisted_at + const state = await backend.state(site) + if (!current(state)) return failure(409,'stale_revision') + const path = match[3] ?? '/' + if (!nativePreviewPathAllowed(path,url.searchParams)) return failure(403,'read_only_preview') + // The engine supplies its existing protected customer mount, not a browser + // supplied host header. Require a plain root-relative path without a query. + const mount = request.headers.get('x-wp-codebox-preview-base') ?? `/v1/bricks/sites/${siteId}/previews/${receiptId}` + if (!/^\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]+$/.test(mount)) return failure(400,'invalid_preview_base') + const safeRequest = new Request(`${site.origin}${path}${url.search}`, { method: request.method, headers: { accept: request.headers.get('accept') ?? '*/*' } }) + const response = await backend.render(safeRequest,site,state.pointer!) + if (!current(await backend.state(site))) { await response.body?.cancel(); return failure(409,'stale_revision') } + if (response.status >= 300 && response.status < 400) { + await response.body?.cancel() + const destination = new URL(response.headers.get('location') ?? '/',site.origin) + if (destination.origin !== site.origin || !nativePreviewPathAllowed(destination.pathname,destination.searchParams)) return failure(409,'preview_redirect_denied') + return new Response(null,{status:response.status,headers:{...privateHeaders,location:`${mount}${destination.pathname}${destination.search}`}}) + } + const headers = new Headers(response.headers) + for (const name of ['set-cookie','content-length','content-encoding','etag','last-modified','link','refresh','access-control-allow-origin']) headers.delete(name) + for (const [key,value] of Object.entries(privateHeaders)) headers.set(key,value) + headers.set('x-wp-codebox-native-receipt',receiptId) + headers.set('x-wp-codebox-canonical-version',String(state.version)) + headers.set('x-wp-codebox-canonical-revision',state.pointer!.revision) + headers.set('x-content-type-options','nosniff') + headers.set('content-security-policy',"default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'none'; form-action 'none'; base-uri 'none'") + const type = headers.get('content-type') ?? '' + const body = request.method === 'HEAD' ? null : /text\/html|text\/css|javascript/.test(type) ? rewriteNativePreview(await response.text(),site.origin,mount) : response.body + return new Response(body,{status:response.status,headers}) +} + +export function nativePreviewReceipt(site: SiteContext, receiptId: string, apiOrigin: string) { + if (site.id === 'default') return { state: 'requested' as const, url: null } + const origin = new URL(apiOrigin) + if (origin.origin !== apiOrigin || (origin.protocol !== 'https:' && !(origin.protocol === 'http:' && ['localhost','127.0.0.1','[::1]'].includes(origin.hostname))) || !/^[A-Za-z0-9_-]{1,160}$/.test(receiptId)) throw new Error('Native preview receipt identity is invalid.') + return { state: 'ready' as const, url: `${apiOrigin}/v1/bricks/sites/${site.id}/previews/${receiptId}/` } +} diff --git a/packages/runtime-cloudflare/src/native-bricks-api.ts b/packages/runtime-cloudflare/src/native-bricks-api.ts new file mode 100644 index 00000000..403f64d0 --- /dev/null +++ b/packages/runtime-cloudflare/src/native-bricks-api.ts @@ -0,0 +1,179 @@ +import { authenticateProvisioningRequest, provisioningTokenAllowsSite, provisioningIdempotencyKey, type ProvisioningEnv } from "./provisioning-api.js" +import { parseSiteContexts, type SiteContext } from "./site-context.js" +import { type MarkdownPointer, type RevisionCoordinator, type RevisionLease, RevisionConflict } from "./revision-coordinator.js" +import { validateNativeBricksArtifact, stableJson, sha256Hex, MAX_NATIVE_BRICKS_ARTIFACT_BYTES, type NativeBricksArtifact } from "./native-bricks-artifact.js" +import { readBoundedRequestBytes } from "./static-artifact-import.js" + +export interface NativePointer { + receipt_id: string; canonical_state_version: number; canonical_state_revision: string + canonical_manifest_key: string; canonical_persisted_at: string; artifact_sha256: string + native_document_hashes: string[]; design_system_version: string +} +export interface NativeMutation { + schema: "wp-codebox/native-bricks-mutation-request/v1"; action: "provision" | "revise" | "restore" + idempotencyKey: string; operationId: string; siteId: string; customerId: string + artifact: { sha256: string; size: number; r2Key: string } | null + authority: { expectedBase: NativePointer | null; restoreTarget: NativePointer | null } + targetRequest: Record +} +export interface NativeSeed extends MarkdownPointer { runtime: { version: string; sha256: string } } +export interface NativeEnv extends ProvisioningEnv { WORDPRESS_BRICKS_PREPARED_ALLOCATIONS?: string } +export interface NativeExecution { pointer: MarkdownPointer; receipt: Record } +export interface NativeBackend { + coordinator(site: SiteContext): RevisionCoordinator + execute(site: SiteContext, input: NativeMutation, artifact: NativeBricksArtifact | null, lease: RevisionLease, prepare: (result: NativeExecution) => Promise): Promise +} +interface OperationRow { site_id: string; operation_id: string; principal: string; fingerprint: string; input_json: string; state: string; prepared_json: string | null; receipt_json: string | null; error_json: string | null; generation: number } +const ready = new WeakMap>() +const schema = "wp-codebox/provisioning-api/v1" +const id = /^[A-Za-z0-9_-]{1,160}$/ +const digest = /^[a-f0-9]{64}$/ + +export async function routeNativeBricksApi(request: Request, env: NativeEnv, backend: NativeBackend): Promise { + const parts = new URL(request.url).pathname.split("/").filter(Boolean) + const poll = parts.length === 5 && parts[0] === "v1" && parts[1] === "sites" && parts[3] === "operations" + if (!(parts[0] === "v1" && parts[1] === "bricks") && !poll) return null + if (poll) { + await initialize(env) + const row = await byOperation(env, parts[2], parts[4]) + if (!row) return null + if (request.method !== "GET") return error(405, "method_not_allowed", "GET is required.") + const token = await authenticateProvisioningRequest(request, env, "sites:read") + if (token instanceof Response) return token + if (row.principal !== token.principal || !provisioningTokenAllowsSite(token, row.site_id)) return error(404, "not_found", "Operation unavailable.") + return resource(row) + } + if (parts.length === 4 && parts[2] === "artifacts" && digest.test(parts[3])) return stage(request, env, parts[3]) + const action = parts.length === 3 && parts[2] === "sites" ? "provision" : parts.length === 5 && parts[2] === "sites" ? parts[4] === "revisions" ? "revise" : parts[4] === "restores" ? "restore" : null : null + if (!action) return error(404, "not_found", "Native route unavailable.") + if (request.method !== "POST") return error(405, "method_not_allowed", "POST is required.") + const token = await authenticateProvisioningRequest(request, env, action === "provision" ? "sites:create" : "sites:import") + if (token instanceof Response) return token + const key = provisioningIdempotencyKey(request); if (key instanceof Response) return key + let input: NativeMutation + let seed: NativeSeed + let site: SiteContext + let artifact: NativeBricksArtifact | null = null + try { + input = JSON.parse(new TextDecoder().decode(await readBoundedRequestBytes(request, 128 * 1024))) + validateNativeMutation(input, action, key) + if (parts.length === 5 && parts[3] !== input.siteId) throw new Error("Path and requested site disagree.") + if (!provisioningTokenAllowsSite(token, input.siteId)) return error(404, "not_found", "Allocation unavailable.") + const contexts = parseSiteContexts(env.WORDPRESS_SITE_CONTEXTS) + const configured = contexts.find(value => value.id === input.siteId) + const seeds = JSON.parse(env.WORDPRESS_BRICKS_PREPARED_ALLOCATIONS ?? "{}") + if (!configured || !seeds[input.siteId]) return error(409, "native_allocation_unprepared", "Native provisioning requires an operator-prepared isolated Bricks allocation.") + site = configured; seed = seeds[input.siteId] + if (!seed.revision || !seed.manifestKey || !seed.persistedAt || !digest.test(seed.runtime?.sha256 ?? "")) throw new Error("Prepared allocation pin is invalid.") + if (input.targetRequest.runtime_artifact?.sha256 !== seed.runtime.sha256 || input.targetRequest.runtime_artifact?.version !== seed.runtime.version) throw new Error("Bricks runtime package does not match the prepared allocation.") + const artifactHash = input.artifact?.sha256 ?? input.authority.restoreTarget!.artifact_sha256 + { + const object = await env.WORDPRESS_STATE_BUCKET.get(artifactKey(artifactHash)) + if (!object || (input.artifact && object.size !== input.artifact.size)) throw new Error("Staged native artifact unavailable.") + const bytes = new Uint8Array(await object.arrayBuffer()) + if (await sha256Hex(bytes) !== artifactHash) throw new Error("Staged native artifact digest mismatch.") + artifact = await validateNativeBricksArtifact(bytes) + if (artifactHash !== input.targetRequest.bricks_artifact?.sha256) throw new Error("Native payload digest differs from the target request.") + if (stableJson(artifact.editing) !== stableJson(input.targetRequest.editing) || artifact.site.starter_id !== input.targetRequest.starter_id || stableJson(artifact.runtime) !== stableJson(input.targetRequest.runtime_artifact)) throw new Error("Native editing or runtime authority mismatch.") + if (artifact.site.site_id !== input.siteId || artifact.site.customer_id !== input.customerId || artifact.runtime.sha256 !== seed.runtime.sha256) throw new Error("Native artifact identity or runtime mismatch.") + if (stableJson(artifact.evidence_inputs.dossier) !== stableJson(input.targetRequest.dossier) || stableJson(artifact.evidence_inputs.creative_provenance) !== stableJson(input.targetRequest.creative_provenance) || stableJson(artifact.evidence_inputs.authorized_asset_hashes) !== stableJson(input.targetRequest.authorized_assets.map((asset: any) => asset.sha256))) throw new Error("Native artifact source authority mismatch.") + } + } catch (cause) { return error(400, "invalid_native_mutation", message(cause)) } + await initialize(env) + const fingerprint = await sha256Hex(new TextEncoder().encode(stableJson(input))) + let row = await env.WORDPRESS_STATE_DATABASE.prepare("SELECT * FROM wp_codebox_native_operations WHERE site_id = ? AND idempotency_key = ?").bind(site.id, key).first() + if (row && (row.fingerprint !== fingerprint || row.principal !== token.principal || row.operation_id !== input.operationId)) return error(409, "idempotency_conflict", "Idempotency key belongs to a different native mutation.") + if (!row && await byOperation(env, site.id, input.operationId)) return error(409, "idempotency_conflict", "Operation ID belongs to a different idempotency key.") + if (row?.state === "succeeded") return resource(row) + const coordinator = backend.coordinator(site) + let lease: RevisionLease | undefined + let committed = false + try { + const now = Date.now() + await env.WORDPRESS_STATE_DATABASE.prepare("INSERT OR IGNORE INTO wp_codebox_sites(site_id,hostname,origin,state,created_at,activated_at,updated_at) VALUES(?,?,?,'active',?,?,?)").bind(site.id,site.hostname,site.origin,now,now,now).run() + lease = await coordinator.acquire(600_000) + const lifecycle = await env.WORDPRESS_STATE_DATABASE.prepare("SELECT s.generation, s.state, l.state AS lifecycle_state, l.expires_at FROM wp_codebox_sites s LEFT JOIN wp_codebox_site_lifecycles l ON l.site_id=s.site_id AND l.generation=s.generation WHERE s.site_id=?").bind(site.id).first<{generation:number;state:string;lifecycle_state:string|null;expires_at:number|null}>() + if (!lifecycle || lifecycle.state !== "active" || (lifecycle.lifecycle_state !== null && (lifecycle.lifecycle_state !== "active" || (lifecycle.expires_at ?? 0) <= Date.now()))) throw new RevisionConflict("Prepared allocation is inactive.") + if (row && row.generation !== lifecycle.generation) throw new RevisionConflict("Allocation generation changed.") + if (row?.prepared_json) { + const prepared: NativeExecution = JSON.parse(row.prepared_json) + const receipt = await coordinator.committed(prepared.receipt.revision_receipt.canonical_state_version) + if (sameManifest(receipt, prepared.pointer)) { await finish(env, row, prepared.receipt); await coordinator.release(lease); committed = true; return resource((await byOperation(env, site.id, input.operationId))!) } + } + const prior = await env.WORDPRESS_STATE_DATABASE.prepare("SELECT receipt_json FROM wp_codebox_native_operations WHERE site_id=? AND state='succeeded' ORDER BY committed_version DESC LIMIT 1").bind(site.id).first<{receipt_json:string}>() + const priorReceipt = prior ? JSON.parse(prior.receipt_json) : null + if (input.action === "provision") { + if (priorReceipt || !sameManifest(lease.pointer, seed)) throw new RevisionConflict("Provision requires the exact unused prepared allocation.") + } else if (!priorReceipt || stableJson(priorReceipt.revision_receipt) !== stableJson(input.authority.expectedBase) || !pointerMatchesLease(input.authority.expectedBase!, lease)) throw new RevisionConflict("Exact native revision is stale.") + if (input.action === "restore") { + const target = input.authority.restoreTarget! + const saved = await env.WORDPRESS_STATE_DATABASE.prepare("SELECT receipt_json FROM wp_codebox_native_operations WHERE site_id=? AND state='succeeded' AND committed_version=?").bind(site.id, target.canonical_state_version).first<{receipt_json:string}>() + if (!saved || stableJson(JSON.parse(saved.receipt_json).revision_receipt) !== stableJson(target)) throw new RevisionConflict("Restore target is not an exact committed native revision.") + } + if (!row) { + const owner = await env.WORDPRESS_STATE_DATABASE.prepare("SELECT principal FROM wp_codebox_native_operations WHERE site_id=? LIMIT 1").bind(site.id).first<{principal:string}>() + if (owner && owner.principal !== token.principal) throw new RevisionConflict("Allocation belongs to another principal.") + await env.WORDPRESS_STATE_DATABASE.prepare("INSERT INTO wp_codebox_native_operations(site_id,operation_id,idempotency_key,principal,fingerprint,input_json,state,generation,created_at) VALUES(?,?,?,?,?,?,'running',?,?)").bind(site.id,input.operationId,key,token.principal,fingerprint,JSON.stringify(input),lifecycle.generation,Date.now()).run() + row = (await byOperation(env, site.id, input.operationId))! + } + const operation = row + const result = await backend.execute(site,input,artifact,lease,async result => { + await env.WORDPRESS_STATE_DATABASE.prepare("UPDATE wp_codebox_native_operations SET prepared_json=? WHERE site_id=? AND operation_id=?").bind(JSON.stringify(result),site.id,input.operationId).run() + }) + const terminal = await coordinator.commit(lease,result.pointer) + committed = true + if (terminal.version !== result.receipt.revision_receipt.canonical_state_version) throw new Error("Native commit version mismatch.") + await finish(env,operation,result.receipt) + return resource((await byOperation(env,site.id,input.operationId))!) + } catch (cause) { + if (row && !committed) await env.WORDPRESS_STATE_DATABASE.prepare("UPDATE wp_codebox_native_operations SET state='retryable',error_json=? WHERE site_id=? AND operation_id=?").bind(JSON.stringify({code:cause instanceof RevisionConflict?"stale_revision":"native_runtime_error",message:message(cause)}),site.id,input.operationId).run() + return error(cause instanceof RevisionConflict?409:422,cause instanceof RevisionConflict?"stale_revision":"native_runtime_error",message(cause)) + } finally { if (lease && !committed) await coordinator.abort(lease) } +} + +async function stage(request:Request,env:NativeEnv,hash:string):Promise{ + if(request.method!=="PUT")return error(405,"method_not_allowed","PUT is required.") + const token=await authenticateProvisioningRequest(request,env,"sites:create");if(token instanceof Response)return token + const key=provisioningIdempotencyKey(request);if(key instanceof Response)return key + try{ + const bytes=await readBoundedRequestBytes(request,MAX_NATIVE_BRICKS_ARTIFACT_BYTES) + if(await sha256Hex(bytes)!==hash)return error(409,"artifact_digest_mismatch","Native artifact SHA does not match its bytes.") + const artifact=await validateNativeBricksArtifact(bytes) + if(!provisioningTokenAllowsSite(token,artifact.site.site_id))return error(404,"not_found","Allocation unavailable.") + const r2Key=artifactKey(hash) + await env.WORDPRESS_STATE_BUCKET.put(r2Key,bytes,{onlyIf:{etagDoesNotMatch:"*"},httpMetadata:{contentType:"application/json"}}) + const object=await env.WORDPRESS_STATE_BUCKET.get(r2Key) + if(!object||object.size!==bytes.byteLength||await sha256Hex(new Uint8Array(await object.arrayBuffer()))!==hash)return error(409,"artifact_conflict","Immutable native artifact conflicts.") + return Response.json({schema:"wp-codebox/provisioning-artifact/v1",artifact:{sha256:hash,size:bytes.byteLength,r2Key}}) + }catch(cause){return error(400,"invalid_native_artifact",message(cause))} +} +export function validateNativeMutation(input:NativeMutation,action:string,key:string):void{ + if(!input||input.schema!=="wp-codebox/native-bricks-mutation-request/v1"||input.action!==action||input.idempotencyKey!==key||!id.test(input.operationId)||!id.test(input.customerId)||!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(input.siteId)||input.siteId.length>63||!input.authority||!input.targetRequest)throw new Error("Native request identity is invalid.") + const t=input.targetRequest + if(t.operation_id!==input.operationId||t.idempotency_key!==key||t.site_id!==input.siteId||t.customer_id!==input.customerId||t.target_runtime!=="wordpress_bricks_cloudflare_v1")throw new Error("Target request identity mismatch.") + const empty = {receipt_id:null,canonical_state_version:null,canonical_state_revision:null,canonical_manifest_key:null,canonical_persisted_at:null,artifact_sha256:null,native_document_hashes:null,design_system_version:null} + const {required,...rollback}=t.rollback_receipt??{} + if(t.schema_version!==1||t.bricks_artifact?.version!=="wp-codebox/bricks-site-artifact/v1"||t.license_secret_ref!=="BRICKS_LICENSE_KEY"||required!==true||stableJson(t.revision_receipt)!==stableJson(input.authority.expectedBase??empty)||stableJson(rollback)!==stableJson(input.authority.restoreTarget??input.authority.expectedBase??empty))throw new Error("Target request exact authority mismatch.") + if(!Array.isArray(t.authorized_assets)||t.authorized_assets.some((asset:any)=>asset.usage_status!=="approved"||!digest.test(asset.sha256)))throw new Error("Native assets must be agency-approved.") + if(action==="provision"&&(input.authority.expectedBase!==null||input.authority.restoreTarget!==null))throw new Error("Provision authority must be empty.") + if(action!=="provision")validatePointer(input.authority.expectedBase) + if(action==="restore"){validatePointer(input.authority.restoreTarget);if(input.artifact!==null)throw new Error("Restore uses the retained revision, not a new artifact.")} + else{if(input.authority.restoreTarget!==null||!input.artifact||!digest.test(input.artifact.sha256)||input.artifact.r2Key!==artifactKey(input.artifact.sha256)||!Number.isSafeInteger(input.artifact.size)||input.artifact.size<1||input.artifact.size>MAX_NATIVE_BRICKS_ARTIFACT_BYTES)throw new Error("Native staged artifact reference is invalid.")} +} +function validatePointer(value:NativePointer|null):void{if(!value||!id.test(value.receipt_id)||!Number.isSafeInteger(value.canonical_state_version)||value.canonical_state_version<1||!value.canonical_state_revision||!value.canonical_manifest_key||!Number.isFinite(Date.parse(value.canonical_persisted_at))||!digest.test(value.artifact_sha256)||!Array.isArray(value.native_document_hashes)||!value.native_document_hashes.length||value.native_document_hashes.some(hash=>!digest.test(hash))||!value.design_system_version)throw new Error("Exact content-bound pointer is required.")} +function artifactKey(hash:string):string{return `sites/provisioning/bricks-artifacts/${hash}.json`} +function sameManifest(a:MarkdownPointer|null,b:MarkdownPointer):boolean{return !!a&&a.revision===b.revision&&a.manifestKey===b.manifestKey&&a.persistedAt===b.persistedAt} +function pointerMatchesLease(pointer:NativePointer,lease:RevisionLease):boolean{return pointer.canonical_state_version===lease.version&&sameManifest(lease.pointer,{revision:pointer.canonical_state_revision,manifestKey:pointer.canonical_manifest_key,persistedAt:pointer.canonical_persisted_at})} +function error(status:number,code:string,message:string):Response{return Response.json({schema,error:{code,message}},{status})} +function message(cause:unknown):string{ + if(!(cause instanceof Error))return "Native operation failed." + if(cause.message.startsWith("PHP.run()")){ + const vendor=cause.message.match(/Uncaught Exception: ([^\n]+)/)?.[1] + return vendor ? vendor.replace(/<[^>]*>/g,"").split(" | data:")[0].slice(0,400) : "Native PHP execution failed." + } + return cause.message.slice(0,400) +} +function resource(row:OperationRow):Response{return Response.json({schema,operation:{id:row.operation_id,siteId:row.site_id,state:row.state,stage:row.state==="succeeded"?"prepared-allocation-native-documents-committed":"native-mutation",progress:row.state==="succeeded"?100:0,retryAt:null,error:row.error_json?JSON.parse(row.error_json):null,receipt:row.receipt_json?JSON.parse(row.receipt_json):null}})} +async function byOperation(env:NativeEnv,site:string,op:string):Promise{return env.WORDPRESS_STATE_DATABASE.prepare("SELECT * FROM wp_codebox_native_operations WHERE site_id=? AND operation_id=?").bind(site,op).first()} +async function finish(env:NativeEnv,row:OperationRow,receipt:Record):Promise{await env.WORDPRESS_STATE_DATABASE.prepare("UPDATE wp_codebox_native_operations SET state='succeeded',receipt_json=?,committed_version=?,error_json=NULL WHERE site_id=? AND operation_id=?").bind(JSON.stringify(receipt),receipt.revision_receipt.canonical_state_version,row.site_id,row.operation_id).run()} +function initialize(env:NativeEnv):Promise{let promise=ready.get(env.WORDPRESS_STATE_DATABASE);if(!promise){promise=env.WORDPRESS_STATE_DATABASE.exec("CREATE TABLE IF NOT EXISTS wp_codebox_native_operations(site_id TEXT NOT NULL,operation_id TEXT NOT NULL,idempotency_key TEXT NOT NULL,principal TEXT NOT NULL,fingerprint TEXT NOT NULL,input_json TEXT NOT NULL,state TEXT NOT NULL,generation INTEGER NOT NULL,created_at INTEGER NOT NULL,prepared_json TEXT,receipt_json TEXT,error_json TEXT,committed_version INTEGER,PRIMARY KEY(site_id,operation_id),UNIQUE(site_id,idempotency_key));").then(()=>{});ready.set(env.WORDPRESS_STATE_DATABASE,promise)}return promise} diff --git a/packages/runtime-cloudflare/src/native-bricks-artifact.ts b/packages/runtime-cloudflare/src/native-bricks-artifact.ts new file mode 100644 index 00000000..74d783b8 --- /dev/null +++ b/packages/runtime-cloudflare/src/native-bricks-artifact.ts @@ -0,0 +1,163 @@ +/** Immutable native document envelope shared with Build's bricks-site-artifact/v1 producer. */ +export const MAX_NATIVE_BRICKS_ARTIFACT_BYTES = 12 * 1024 * 1024 +const MAX_MEDIA_BYTES = 8 * 1024 * 1024 +const ids = /^[A-Za-z0-9_-]{1,160}$/u +const hashes = /^[a-f0-9]{64}$/u +type JsonObject = Record +export interface NativeBricksElement { id: string; name: string; settings: JsonObject; children: NativeBricksElement[] } +interface NativeDocument { logical_id: string; title: string; status: "draft" | "publish"; elements: NativeBricksElement[]; sha256: string } +export interface NativeBricksArtifact { + schema: "wp-codebox/bricks-site-artifact/v1" + site: { site_id: string; customer_id: string; title: string; starter_id: string } + runtime: { version: string; sha256: string } + evidence_inputs: { + dossier: { revision: number; sha256: string } + creative_provenance: { version: string; canonicalCommit: string; guideSha256: string; catalogSha256: string; references: { id: string; sha256: string }[] } + authorized_asset_hashes: string[] + } + design_system: { + version: string; palette: { id: string; name: string; value: string }[] + typography: { body_font_family: string; heading_font_family: string; root_font_size: string } + global_classes: { id: string; name: string; settings: JsonObject; sha256: string }[] + global_variables: { id: string; name: string; type: "color" | "number" | "string"; value: string; sha256: string }[] + theme_style: { settings: JsonObject; sha256: string } + } + documents: { pages: (NativeDocument & { slug: string })[]; templates: (NativeDocument & { type: "header" | "footer" | "section" | "single" | "archive"; conditions: JsonObject[] })[] } + assets: { logical_id: string; filename: string; mime_type: "image/png" | "image/jpeg" | "image/webp"; alt_text: string; sha256: string; bytes: number; content_base64: string }[] + editing: { granularity: "native_bricks_elements"; target_role: { role_slug: string; required_capabilities: string[] }; acceptance_sequence: ["edit", "publish", "public_observation", "restore", "reopen"] } +} + +export function stableJson(value: unknown): string { + const normalize = (item: unknown): unknown => Array.isArray(item) ? item.map(normalize) + : item !== null && typeof item === "object" ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entry]) => [key, normalize(entry)])) : item + return JSON.stringify(normalize(value)) +} +export async function sha256Hex(bytes: Uint8Array | string): Promise { + const input = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes.buffer instanceof ArrayBuffer ? bytes as Uint8Array : new Uint8Array(bytes) + return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", input)), byte => byte.toString(16).padStart(2, "0")).join("") +} +function requireValue(condition: unknown, detail: string): asserts condition { if (!condition) throw new Error(`Invalid native Bricks artifact: ${detail}`) } +function object(value: unknown, keys?: string[]): JsonObject { + requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required") + const result = value as JsonObject + if (keys) requireValue(Object.keys(result).length === keys.length && keys.every(key => Object.hasOwn(result, key)), `expected fields ${keys.join(",")}`) + return result +} +function list(value: unknown, max: number, min = 0): unknown[] { requireValue(Array.isArray(value) && value.length >= min && value.length <= max, "collection limit"); return value } +function text(value: unknown, max: number, min = 1): string { requireValue(typeof value === "string" && value.length >= min && value.length <= max && (min === 0 || value.trim() === value), "text limit"); return value } +function identifier(value: unknown): string { const result = text(value, 160); requireValue(ids.test(result), "identifier"); return result } +function digest(value: unknown): string { const result = text(value, 64); requireValue(hashes.test(result), "SHA-256"); return result } +function oneOf(value: unknown, choices: string[]): void { requireValue(typeof value === "string" && choices.includes(value), "enum value") } +function unique(values: unknown[], field: string): void { requireValue(new Set(values).size === values.length, `duplicate ${field}`) } +async function component(value: JsonObject): Promise { + const { sha256, ...payload } = value + requireValue(await sha256Hex(stableJson(payload)) === digest(sha256), "component hash mismatch") +} + +export async function validateNativeBricksArtifact(bytes: Uint8Array): Promise { + requireValue(bytes.byteLength > 0 && bytes.byteLength <= MAX_NATIVE_BRICKS_ARTIFACT_BYTES, "artifact byte limit") + const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes) + const parsed: unknown = JSON.parse(source) + // Bound arbitrary settings before any recursive canonicalization or native-tree traversal. + const pending: { value: unknown; depth: number }[] = [{ value: parsed, depth: 0 }] + let nodes = 0 + while (pending.length) { + const { value, depth } = pending.pop()! + requireValue(++nodes <= 200_000 && depth <= 80, "JSON complexity limit") + if (typeof value === "number") requireValue(Number.isFinite(value), "finite number required") + if (value && typeof value === "object") { + for (const [key, entry] of Object.entries(value)) { + requireValue(!["__proto__", "prototype", "constructor"].includes(key), "unsupported object key") + pending.push({ value: entry, depth: depth + 1 }) + } + } + } + const root = object(parsed, ["schema", "site", "runtime", "evidence_inputs", "design_system", "documents", "assets", "editing"]) + requireValue(root.schema === "wp-codebox/bricks-site-artifact/v1", "schema") + requireValue(source === stableJson(root), "noncanonical JSON") + const site = object(root.site, ["site_id", "customer_id", "title", "starter_id"]) + requireValue(/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(text(site.site_id, 63)), "site DNS label") + identifier(site.customer_id); identifier(site.starter_id); text(site.title, 200) + const runtime = object(root.runtime, ["version", "sha256"]); text(runtime.version, 120); digest(runtime.sha256) + const evidence = object(root.evidence_inputs, ["dossier", "creative_provenance", "authorized_asset_hashes"]) + const dossier = object(evidence.dossier, ["revision", "sha256"]) + requireValue(Number.isSafeInteger(dossier.revision) && Number(dossier.revision) > 0, "dossier revision"); digest(dossier.sha256) + const provenance = object(evidence.creative_provenance, ["version", "canonicalCommit", "guideSha256", "catalogSha256", "references"]) + text(provenance.version, 120); requireValue(/^[a-f0-9]{40}$/u.test(text(provenance.canonicalCommit, 40)), "canonical source commit") + digest(provenance.guideSha256); digest(provenance.catalogSha256) + const references = list(provenance.references, 32).map(item => { const row = object(item, ["id", "sha256"]); identifier(row.id); digest(row.sha256); return row }) + unique(references.map(row => row.id), "reference ID"); unique(references.map(row => row.sha256), "reference hash") + const authorized = list(evidence.authorized_asset_hashes, 100).map(digest) + const design = object(root.design_system, ["version", "palette", "typography", "global_classes", "global_variables", "theme_style"]) + text(design.version, 200) + const palette = list(design.palette, 100, 1).map(item => { const row = object(item, ["id", "name", "value"]); identifier(row.id); text(row.name, 120); text(row.value, 120); return row }) + unique(palette.map(row => row.id), "palette ID") + const typography = object(design.typography, ["body_font_family", "heading_font_family", "root_font_size"]) + text(typography.body_font_family, 200); text(typography.heading_font_family, 200); text(typography.root_font_size, 40) + const classes = list(design.global_classes, 500).map(item => { const row = object(item, ["id", "name", "settings", "sha256"]); identifier(row.id); text(row.name, 120); object(row.settings); return row }) + unique(classes.map(row => row.id), "global class ID") + for (const row of classes) await component(row) + const variables = list(design.global_variables, 500).map(item => { const row = object(item, ["id", "name", "type", "value", "sha256"]); identifier(row.id); text(row.name, 120); oneOf(row.type, ["color", "number", "string"]); text(row.value, 2000, 0); return row }) + unique(variables.map(row => row.id), "global variable ID") + for (const row of variables) await component(row) + const theme = object(design.theme_style, ["settings", "sha256"]); object(theme.settings); await component(theme) + const assets = list(root.assets, 100).map(item => object(item, ["logical_id", "filename", "mime_type", "alt_text", "sha256", "bytes", "content_base64"])) + unique(assets.map(row => identifier(row.logical_id)), "asset ID") + let mediaBytes = 0 + for (const asset of assets) { + const filename = text(asset.filename, 255) + requireValue(!/[/\\\u0000-\u001f]/u.test(filename) && filename !== "." && filename !== "..", "media filename") + oneOf(asset.mime_type, ["image/png", "image/jpeg", "image/webp"]); text(asset.alt_text, 500, 0) + const encoded = text(asset.content_base64, MAX_NATIVE_BRICKS_ARTIFACT_BYTES) + requireValue(encoded.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/u.test(encoded), "base64 encoding") + // Avoid Uint8Array.from(string): its iterator can allocate an intermediate + // array far larger than the media itself inside a Worker isolate. + const decoded = atob(encoded) + const raw = new Uint8Array(decoded.length) + for (let index = 0; index < decoded.length; index++) raw[index] = decoded.charCodeAt(index) + requireValue(raw.byteLength > 0 && raw.byteLength === asset.bytes && (mediaBytes += raw.byteLength) <= MAX_MEDIA_BYTES, "media byte limit") + requireValue(await sha256Hex(raw) === digest(asset.sha256), "media hash mismatch") + const signature = asset.mime_type === "image/png" ? [137,80,78,71,13,10,26,10].every((v,i) => raw[i] === v) + : asset.mime_type === "image/jpeg" ? raw[0] === 255 && raw[1] === 216 && raw[2] === 255 + : new TextDecoder().decode(raw.slice(0,4)) === "RIFF" && new TextDecoder().decode(raw.slice(8,12)) === "WEBP" + requireValue(signature, "media MIME signature mismatch") + } + requireValue(stableJson(assets.map(asset => asset.sha256)) === stableJson(authorized), "asset authority mismatch") + const assetIds = new Set(assets.map(asset => asset.logical_id)) + const usedAssets = new Set() + const documents = object(root.documents, ["pages", "templates"]) + const elementIds = new Set() + const supported = ["container", "section", "block", "div", "heading", "text-basic", "text", "text-link", "button", "icon", "image", "form", "svg"] + const visit = (entries: unknown, depth = 0): void => { + requireValue(depth <= 32, "native tree depth") + for (const item of list(entries, 2000)) { + const element = object(item, ["id", "name", "settings", "children"]) + const eid = identifier(element.id); requireValue(!elementIds.has(eid), "duplicate element ID"); elementIds.add(eid) + requireValue(elementIds.size <= 5000, "native element count") + oneOf(element.name, supported); const settings = object(element.settings) + const stack: unknown[] = [settings] + while (stack.length) { const entry = stack.pop(); if (entry && typeof entry === "object") for (const [key, value] of Object.entries(entry)) { if (key === "asset_ref") { requireValue(assetIds.has(identifier(value)), "unknown asset_ref"); usedAssets.add(value as string) } else if (value && typeof value === "object") stack.push(value) } } + if (element.name === "image") { const image = object(settings.image, ["asset_ref", "size"]); requireValue(assetIds.has(identifier(image.asset_ref)), "image requires asset_ref"); requireValue(image.size === "full", "image size must be full") } + visit(element.children, depth + 1) + } + } + for (const kind of ["pages", "templates"] as const) { + const rows = list(documents[kind], 500, 1) + const logicalIds: string[] = []; const slugs: string[] = [] + for (const item of rows) { + const row = object(item, ["logical_id", "title", "status", "elements", "sha256", ...(kind === "pages" ? ["slug"] : ["type", "conditions"])]) + logicalIds.push(identifier(row.logical_id)); text(row.title, 200); oneOf(row.status, ["draft", "publish"]) + if (kind === "pages") { const slug = text(row.slug, 64); requireValue(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(slug), "page slug"); slugs.push(slug) } + else { oneOf(row.type, ["header", "footer", "section", "single", "archive"]); for (const condition of list(row.conditions, 100)) object(condition) } + list(row.elements, 2000, 1); visit(row.elements); await component(row) + } + unique(logicalIds, `${kind} logical ID`); unique(slugs, "page slug") + } + requireValue(assetIds.size === usedAssets.size, "unused staged asset") + const editing = object(root.editing, ["granularity", "target_role", "acceptance_sequence"]) + requireValue(editing.granularity === "native_bricks_elements", "native editing required") + const role = object(editing.target_role, ["role_slug", "required_capabilities"]); identifier(role.role_slug) + unique(list(role.required_capabilities, 32, 1).map(identifier), "role capability") + requireValue(stableJson(editing.acceptance_sequence) === stableJson(["edit", "publish", "public_observation", "restore", "reopen"]), "editing sequence") + return root as unknown as NativeBricksArtifact +} diff --git a/packages/runtime-cloudflare/src/native-bricks-php.ts b/packages/runtime-cloudflare/src/native-bricks-php.ts new file mode 100644 index 00000000..c285f877 --- /dev/null +++ b/packages/runtime-cloudflare/src/native-bricks-php.ts @@ -0,0 +1,155 @@ +/** Executes vendor native save/ownership APIs inside an already isolated PHP runtime. */ +export const NATIVE_BRICKS_APPLY_PHP = String.raw` 'administrator', 'number' => 1]); +if (!$admins) throw new Exception('Prepared native allocation has no administrator.'); +// The operator-created administrator owns the native authoring context. +$admins[0]->add_cap(Bricks\Capabilities::FULL_ACCESS); +wp_set_current_user($admins[0]->ID); +if (!class_exists('Bricks\\Abilities\\Elements') || !class_exists('Bricks\\Abilities\\Design')) throw new Exception('Prepared allocation does not contain Bricks native abilities.'); +function native_result($value) { + if (is_wp_error($value)) throw new Exception($value->get_error_code() . ': ' . $value->get_error_message()); + return $value; +} +function native_design($method, $input) { + return native_result(call_user_func(['Bricks\\Abilities\\Design', $method], $input)); +} +function native_id($namespace, $logical) { + static $seen = []; + $id = preg_match('/^[a-z0-9]{6}$/', $logical) ? $logical : substr(hash('sha256', $namespace . ':' . $logical), 0, 6); + if (isset($seen[$namespace][$id]) && $seen[$namespace][$id] !== $logical) throw new Exception('Native ID normalization collision.'); + $seen[$namespace][$id] = $logical; + return $id; +} +function native_elements($elements, $media) { + foreach ($elements as &$element) { + $element['id'] = native_id('element', $element['id']); + if (isset($element['settings']['_cssGlobalClasses'])) $element['settings']['_cssGlobalClasses'] = array_map(fn($id) => native_id('class', $id), $element['settings']['_cssGlobalClasses']); + if ($element['name'] === 'image') { + $ref = $element['settings']['image']['asset_ref'] ?? null; + if (!$ref || !isset($media[$ref])) throw new Exception('Native image asset reference is unavailable.'); + $element['settings']['image'] = ['id' => $media[$ref]['id'], 'url' => wp_get_attachment_url($media[$ref]['id']), 'size' => $element['settings']['image']['size'] ?? 'full']; + } + $element['children'] = native_elements($element['children'] ?? [], $media); + } + unset($element); + return $elements; +} +function native_snapshot($map) { + $hashes = []; $pages = []; $templates = []; $media = []; + foreach ($map['documents'] as $logical => $record) { + $read = native_result(Bricks\Abilities\Elements::get_page_elements(['postId' => $record['id'], 'responseFormat' => 'summary'])); + if (empty($read['documentDigest'])) throw new Exception('Native document digest is unavailable.'); + $hashes[] = $read['documentDigest']; + if ($record['kind'] === 'page') $pages[] = $record['id']; else $templates[] = $record['id']; + } + foreach ($map['media'] as $record) $media[] = $record['id']; + $design = []; + foreach ([BRICKS_DB_GLOBAL_CLASSES, BRICKS_DB_GLOBAL_VARIABLES, BRICKS_DB_THEME_STYLES, BRICKS_DB_COLOR_PALETTE] as $option) $design[$option] = get_option($option, []); + $hashes[] = hash('sha256', wp_json_encode($design)); + return ['native_records' => ['page_ids' => $pages, 'template_ids' => $templates, 'media_ids' => $media], 'native_document_hashes' => $hashes, 'design_system_version' => $map['design_system_version'], 'peak_php_bytes' => memory_get_peak_usage(true)]; +} +$input = json_decode(file_get_contents('/tmp/native-bricks-input.json'), true, 512, JSON_THROW_ON_ERROR); +$map = get_option('wp_codebox_native_bricks_map', ['documents' => [], 'media' => [], 'design_system_version' => 'uninitialized']); +if ($input['action'] === 'restore-read') { + echo json_encode(native_snapshot($map), JSON_THROW_ON_ERROR); + return; +} +$artifact = $input['artifact']; +require_once ABSPATH . 'wp-admin/includes/file.php'; +require_once ABSPATH . 'wp-admin/includes/media.php'; +require_once ABSPATH . 'wp-admin/includes/image.php'; +$next_media = []; +foreach ($artifact['assets'] as $asset) { + $logical = $asset['logical_id']; + if (isset($map['media'][$logical]) && $map['media'][$logical]['sha256'] === $asset['sha256']) { + $next_media[$logical] = $map['media'][$logical]; + update_post_meta($next_media[$logical]['id'], '_wp_attachment_image_alt', $asset['alt_text']); + continue; + } + $bytes = base64_decode($asset['content_base64'], true); + if ($bytes === false || strlen($bytes) !== $asset['bytes'] || hash('sha256', $bytes) !== $asset['sha256']) throw new Exception('Native media integrity mismatch.'); + $upload = wp_upload_bits($asset['filename'], null, $bytes); + if (!empty($upload['error'])) throw new Exception('Native media upload failed.'); + $attachment = native_result(wp_insert_attachment(['post_mime_type' => $asset['mime_type'], 'post_title' => pathinfo($asset['filename'], PATHINFO_FILENAME), 'post_status' => 'inherit'], $upload['file'], 0, true)); + $metadata = wp_generate_attachment_metadata($attachment, $upload['file']); + if (!$metadata) throw new Exception('Native media metadata generation failed.'); + wp_update_attachment_metadata($attachment, $metadata); + update_post_meta($attachment, '_wp_attachment_image_alt', $asset['alt_text']); + $next_media[$logical] = ['id' => $attachment, 'sha256' => $asset['sha256']]; +} +// A complete artifact replaces the managed design store using vendor ownership APIs. +$snapshot = native_design('list_global_classes', ['limit' => 500]); +foreach ($snapshot['items'] as $class) { + $fresh = native_design('list_global_classes', ['limit' => 500]); + foreach ($fresh['items'] as $item) if ($item['id'] === $class['id']) native_design('delete_global_class', ['classId' => $item['id'], 'expectedOwnership' => $item['itemOwnership'], 'lockOwnership' => $fresh['lockOwnership'], 'allowOrphans' => true]); +} +$classes = []; +foreach ($artifact['design_system']['global_classes'] as $class) $classes[] = ['id' => native_id('class', $class['id']), 'name' => sanitize_title($class['name']), 'settings' => $class['settings']]; +if ($classes) { $fresh = native_design('list_global_classes', ['limit' => 500]); native_design('batch_create_global_classes', ['classes' => $classes, 'expectedOwnership' => $fresh['ownership']]); } +$variables = native_design('list_global_variables', ['limit' => 500]); +foreach ($variables['items'] as $variable) { + $fresh = native_design('list_global_variables', ['limit' => 500]); + foreach ($fresh['items'] as $item) if ($item['id'] === $variable['id']) native_design('delete_global_variable', ['variableId' => $item['id'], 'expectedOwnership' => $item['itemOwnership'], 'allowOrphans' => true]); +} +$variables = []; +foreach ($artifact['design_system']['global_variables'] as $variable) $variables[] = ['id' => native_id('variable', $variable['id']), 'name' => sanitize_title($variable['name']), 'value' => $variable['value']]; +if ($variables) { $fresh = native_design('list_global_variables', ['limit' => 500]); native_design('set_global_variables', ['variables' => $variables, 'expectedVariableOwnership' => $fresh['variableOwnership'], 'expectedCategoryOwnership' => $fresh['categoryOwnership']]); } +$palettes = native_design('list_color_palettes', ['limit' => 500]); +// Keep a temporary empty palette while replacing the complete managed palette. +// Bricks requires one effective palette and rejects duplicate named variables. +$temporary = native_design('create_color_palette', ['name' => 'Native transaction ' . wp_generate_uuid4(), 'colors' => [], 'expectedOwnership' => $palettes['ownership']]); +foreach ($palettes['items'] as $palette) { + $fresh = native_design('list_color_palettes', ['limit' => 500]); + foreach ($fresh['items'] as $item) if ($item['id'] === $palette['id']) native_design('delete_color_palette', ['paletteId' => $item['id'], 'expectedOwnership' => $item['itemOwnership'], 'allowOrphans' => true]); +} +$colors = []; +foreach ($artifact['design_system']['palette'] as $color) $colors[] = ['id' => native_id('color', $color['id']), 'name' => $color['name'], 'raw' => 'var(--native-' . sanitize_title($color['id']) . ')', 'light' => $color['value']]; +$fresh = native_design('list_color_palettes', ['limit' => 500]); +native_design('create_color_palette', ['name' => 'Managed native palette', 'colors' => $colors, 'expectedOwnership' => $fresh['ownership']]); +$fresh = native_design('list_color_palettes', ['limit' => 500]); +foreach ($fresh['items'] as $item) if ($item['id'] === $temporary['palette']['id']) native_design('delete_color_palette', ['paletteId' => $item['id'], 'expectedOwnership' => $item['itemOwnership'], 'allowOrphans' => true]); +$styles = native_design('list_theme_styles', ['limit' => 500]); +foreach ($styles['items'] as $style) { + $fresh = native_design('list_theme_styles', ['limit' => 500]); + foreach ($fresh['items'] as $item) if ($item['id'] === $style['id']) native_design('delete_theme_style', ['id' => $item['id'], 'expectedOwnership' => $item['itemOwnership'], 'acknowledgeStyleRemoval' => true]); +} +$theme_settings = $artifact['design_system']['theme_style']['settings']; +$typography = $artifact['design_system']['typography']; +$theme_settings['typography']['typographyHtml'] = $typography['root_font_size']; +$theme_settings['typography']['typographyBody']['font-family'] = $typography['body_font_family']; +$theme_settings['typography']['typographyHeadings']['font-family'] = $typography['heading_font_family']; +native_design('create_theme_style', ['label' => 'Managed native style', 'conditions' => [['main' => 'any']], 'settings' => $theme_settings]); +$next_documents = []; +foreach (['pages' => 'page', 'templates' => 'template'] as $collection => $kind) { + foreach ($artifact['documents'][$collection] as $document) { + $logical = $document['logical_id']; + $existing = $map['documents'][$logical] ?? null; + $elements = native_elements($document['elements'], $next_media); + if ($existing && $existing['kind'] !== $kind) throw new Exception('Native logical document kind changed.'); + if ($existing && $kind === 'template' && ($existing['type'] ?? $document['type']) !== $document['type']) throw new Exception('Changing template type requires a new logical document ID.'); + if ($kind === 'page') { + $post = ['post_type' => 'page', 'post_title' => $document['title'], 'post_name' => $document['slug'], 'post_status' => $document['status']]; + if ($existing) $post['ID'] = $existing['id']; + $post_id = native_result(wp_insert_post($post, true)); + } elseif (!$existing) { + $created = native_result(Bricks\Abilities\Templates::create_template(['title' => $document['title'], 'type' => $document['type'] === 'single' ? 'content' : $document['type'], 'status' => $document['status'], 'elements' => $elements, 'settings' => ['templateConditions' => $document['conditions']]])); + $post_id = $created['templateId']; + } else { + $post_id = native_result(wp_update_post(['ID' => $existing['id'], 'post_title' => $document['title'], 'post_status' => $document['status']], true)); + } + native_result(Bricks\Abilities\Elements::set_page_elements(['postId' => $post_id, 'elements' => $elements])); + if ($kind === 'template') native_result(Bricks\Abilities\Templates::set_template_conditions(['templateId' => $post_id, 'conditions' => $document['conditions']])); + $next_documents[$logical] = ['id' => $post_id, 'kind' => $kind, 'type' => $document['type'] ?? 'page']; + } +} +foreach ($map['documents'] as $logical => $record) if (!isset($next_documents[$logical])) wp_delete_post($record['id'], true); +foreach ($map['media'] as $logical => $record) if (!isset($next_media[$logical]) || $next_media[$logical]['id'] !== $record['id']) wp_delete_attachment($record['id'], true); +$first_page = $artifact['documents']['pages'][0]['logical_id']; +update_option('show_on_front', 'page'); +update_option('page_on_front', $next_documents[$first_page]['id']); +update_option('blogname', $artifact['site']['title']); +$map = ['documents' => $next_documents, 'media' => $next_media, 'design_system_version' => $artifact['design_system']['version']]; +update_option('wp_codebox_native_bricks_map', $map); +echo json_encode(native_snapshot($map), JSON_THROW_ON_ERROR); +`; diff --git a/packages/runtime-cloudflare/src/phase-trace.ts b/packages/runtime-cloudflare/src/phase-trace.ts index 5c21141e..a2f9d20a 100644 --- a/packages/runtime-cloudflare/src/phase-trace.ts +++ b/packages/runtime-cloudflare/src/phase-trace.ts @@ -21,6 +21,8 @@ export interface PhaseTraceSummary { phases: PhaseTraceEntry[] } +export type PhaseProgressObserver = (entry: Readonly) => void + const MAX_DURATION_MS = 600_000 const MAX_EVIDENCE_VALUE = Number.MAX_SAFE_INTEGER const MAX_PHASES = 64 @@ -56,7 +58,7 @@ export class CloudflarePhaseTrace { private reservedPhases = 0 private compositeActive = false - constructor(private readonly clock: () => number = defaultNow) { + constructor(private readonly clock: () => number = defaultNow, private readonly onPhase?: PhaseProgressObserver) { this.startedAt = clock() } @@ -70,7 +72,9 @@ export class CloudflarePhaseTrace { if (!this.active) throw new Error("Cloudflare phase completion has no active leaf.") const active = this.active this.active = undefined - this.phases.push({ name: active.name, durationMs: boundedMs(this.clock() - active.startedAt), evidence: boundedEvidence({ ...active.evidence, ...evidence }) }) + const entry = { name: active.name, durationMs: boundedMs(this.clock() - active.startedAt), evidence: boundedEvidence({ ...active.evidence, ...evidence }) } + this.phases.push(entry) + try { this.onPhase?.({ ...entry, evidence: entry.evidence && { ...entry.evidence } }) } catch { /* Observability cannot fail the request. */ } } async measure(name: string, work: () => Promise, evidence?: Record): Promise { diff --git a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts index bbc7d465..09d4bdb9 100644 --- a/packages/runtime-cloudflare/src/php-wasm-universal.d.ts +++ b/packages/runtime-cloudflare/src/php-wasm-universal.d.ts @@ -15,6 +15,7 @@ declare module "@php-wasm/universal" { } export class PHP { + unlink(path: string): void constructor(runtimeId: number) run(request: { code: string }): Promise<{ bytes: Uint8Array; text: string }> isDir(path: string): boolean diff --git a/packages/runtime-cloudflare/src/provisioning-api.ts b/packages/runtime-cloudflare/src/provisioning-api.ts index 6f7d3504..5fed1db0 100644 --- a/packages/runtime-cloudflare/src/provisioning-api.ts +++ b/packages/runtime-cloudflare/src/provisioning-api.ts @@ -422,3 +422,6 @@ async function claimCapability(root: string, allocation: ProvisioningAllocation) const digest = await crypto.subtle.sign("HMAC", key, encoder.encode(identity)) return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("") } + +// Native runtime admission shares the same principal credentials and scopes. +export { authenticate as authenticateProvisioningRequest, allowed as provisioningTokenAllowsSite, idempotencyKey as provisioningIdempotencyKey } diff --git a/packages/runtime-cloudflare/src/sqlite-delete-alias-compatibility.ts b/packages/runtime-cloudflare/src/sqlite-delete-alias-compatibility.ts new file mode 100644 index 00000000..cec9763e --- /dev/null +++ b/packages/runtime-cloudflare/src/sqlite-delete-alias-compatibility.ts @@ -0,0 +1,13 @@ +/** + * SQLite integration 2.2.23 translates a complete `tableAlias` node to + * `AS alias`, then incorrectly uses that text as the DELETE target map key. + * The driver's other alias maps correctly translate its identifier child. + * Preserve the joined SELECT, value/version predicates and actual deletion. + */ +export function patchSqliteDeleteAlias(source: string): string { + const before = "$alias = $this->unquote_sqlite_identifier( $this->translate( $alias_node ) );" + const after = "$alias = $this->unquote_sqlite_identifier( $this->translate( $alias_node->get_first_child_node( 'identifier' ) ) );" + if (!source.includes(before) && source.includes(after)) return source + if (source.split(before).length !== 2) throw new Error("Unsupported SQLite multi-table DELETE alias implementation.") + return source.replace(before, after) +} diff --git a/packages/runtime-cloudflare/src/worker.ts b/packages/runtime-cloudflare/src/worker.ts index 598f1dd3..eeadb32e 100644 --- a/packages/runtime-cloudflare/src/worker.ts +++ b/packages/runtime-cloudflare/src/worker.ts @@ -1,4 +1,10 @@ +import { routeNativeBricksPreview, isProtectedNativeSite, nativePreviewReceipt } from "./bricks-preview.js" +import { patchSqliteDeleteAlias } from "./sqlite-delete-alias-compatibility.js" +import { routeNativeBricksApi, type NativeMutation, type NativeExecution, type NativePointer } from "./native-bricks-api.js" +import { NATIVE_BRICKS_APPLY_PHP } from "./native-bricks-php.js" +import { stableJson, type NativeBricksArtifact } from "./native-bricks-artifact.js" import { loadPHPRuntime, PHP, type PHPRequestHandler, type PHPResponseData } from "@php-wasm/universal" +import { patchBricksRandomIds, restoreBricksSettings } from "./bricks-clock-compatibility.js" 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. @@ -22,13 +28,13 @@ import { allocationIdentity, CloudflareAllocationLifecycle } from "./allocation- import { toFetchResponse, toPHPRequest } from "./request-translation.js" import { DEFAULT_SITE_CONTEXT, parseSiteContexts, previewDomain, resolvePreviewSiteContextFromRequest, resolveSiteContextFromRequest, siteStorageKeys, type SiteContext } from "./site-context.js" import { validateUploadManifestFiles, validateUploadMetadata } from "./upload-persistence.js" -import { deriveSiteCredential, deriveWordPressAuthConstants, type WordPressAuthConstant } from "./wordpress-auth.js" +import { deriveSiteCredential, deriveWordPressAuthConstants } from "./wordpress-auth.js" import { isWordPressRuntimeFile, wordpressStaticArchivePath, wordpressStaticContentType } from "./wordpress-runtime-corpus.js" import { materializeWordPressRuntimeArtifact, type WordPressRuntimeArtifactManifest } from "./wordpress-runtime-artifact.js" import { validateWordPressStaticArtifactManifest, type WordPressStaticArtifactManifest } from "./wordpress-static-artifact.js" import { readRuntimeArchiveArtifact, validateRuntimeArchiveArtifactManifest, type RuntimeArchiveArtifactManifest } from "./runtime-archive-artifact.js" import { runtimeArchiveComponentOwnedWpContentPaths, type RuntimeArchiveComponent } from "@automattic/wp-codebox-core/runtime-archive-component" -import { isCanonicalWpContentPath, MAX_WP_CONTENT_FILES, MAX_WP_CONTENT_FILE_BYTES, MAX_WP_CONTENT_TOTAL_BYTES, runtimeOwnedWpContentPaths, validateWpContentDeletedPaths, validateWpContentManifestFiles, validateWpContentMetadata } from "./wp-content-persistence.js" +import { isCanonicalWpContentPath, isWorkerMaterializedWpContentPath, MAX_WP_CONTENT_FILES, MAX_WP_CONTENT_FILE_BYTES, MAX_WP_CONTENT_TOTAL_BYTES, runtimeOwnedWpContentPaths, validateWpContentDeletedPaths, validateWpContentManifestFiles, validateWpContentMetadata } from "./wp-content-persistence.js" import markdownDatabaseIntegrationRuntime from "../assets/markdown-database-integration-runtime.zip" import canonicalMarkdownSeed from "../assets/markdown-database-integration-canonical-seed.zip" import canonicalMarkdownSeedManifest from "../assets/markdown-database-integration-canonical-seed.json" with { type: "json" } @@ -74,7 +80,8 @@ const MARKDOWN_INDEX_PATH = "/tmp/markdown-index.sqlite" const MARKDOWN_RESOLVED_INDEX_PATH = "/tmp/markdown-index-8133b4cf3c66.sqlite" const MARKDOWN_CHANGES_PATH = "/tmp/wp-codebox-canonical-changes.json" const PUBLICATION_CHANGES_PATH = "/tmp/wp-codebox-publication-changes.json" -const WORDPRESS_PAGE_CACHE_SCHEMA = "v3" +// Bump for changes to PHP materialization/rendering without a content revision. +const WORDPRESS_PAGE_CACHE_SCHEMA = "v4" const PUBLIC_WP_CONTENT_EXTENSION = /\.(?:css|js|mjs|json|txt|xml|woff2?|ttf|otf|eot|svg|png|jpe?g|gif|webp|avif|ico)$/i const MAX_CRON_EVENTS_PER_INVOCATION = 5 const MAX_CRON_INVOCATION_MS = 25_000 @@ -226,6 +233,9 @@ update_option('wp_codebox_static_artifact_imports', $records, false); $GLOBALS['wpdb']->flush_canonical_writes(); echo wp_json_encode(array_merge(array('status' => 'imported'), $record), JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);` export interface RuntimeEnv { + WORDPRESS_BRICKS_PREPARED_ALLOCATIONS?: string + WORDPRESS_NATIVE_DEPLOYMENT_VERSION?: string + WORDPRESS_VERSION_METADATA?: { id: string } WORDPRESS_STATE_BUCKET: R2Bucket WORDPRESS_SITE_CONTEXTS?: string WORDPRESS_PREVIEW_DOMAIN?: string @@ -234,11 +244,21 @@ export interface RuntimeEnv { WORDPRESS_ADMIN_CLAIM_SECRET?: string WORDPRESS_ADMIN_PASSWORD?: string WORDPRESS_AUTH_SECRET?: string + // Secret binding. When set, Bricks reads its official PHP constant rather + // than persisting a license credential in WordPress state. + BRICKS_LICENSE_KEY?: string WORDPRESS_OPERATOR_TOKEN?: string WORDPRESS_API_TOKENS?: string WORDPRESS_STATE_DATABASE?: D1Database WORDPRESS_RUNTIME_QUEUE?: RuntimeQueue WORDPRESS_QUEUE_POLICY?: string + WORDPRESS_PHASE_PROGRESS_LOGS?: string +} + +function requestPhaseTrace(env: RuntimeEnv): CloudflarePhaseTrace { + return new CloudflarePhaseTrace(undefined, env.WORDPRESS_PHASE_PROGRESS_LOGS === "1" ? (phase) => { + console.log(JSON.stringify({ schema: "wp-codebox/cloudflare-runtime-phase-progress/v1", ...phase })) + } : undefined) } export function createCloudflareRuntime( @@ -295,6 +315,17 @@ export function createCloudflareRuntime( if (new URL(request.url).pathname === "/v1" || new URL(request.url).pathname.startsWith("/v1/")) { const operations = resolveOperations?.(env) if (!operations || !("WORDPRESS_STATE_DATABASE" in env)) return Response.json({ schema: "wp-codebox/provisioning-api/v1", error: { code: "not_found", message: "The API resource is unavailable." } }, { status: 404 }) + await operations.initialize() + const nativePreview = await routeNativeBricksPreview(request, env as Env & { WORDPRESS_STATE_DATABASE: D1Database }, { + state: site => resolveCoordinator(env, site).state(), + render: (request, site, pointer) => renderNativeBricksPreview(request, env, site, pointer), + }) + if (nativePreview) return nativePreview + const native = await routeNativeBricksApi(request, env as Env & { WORDPRESS_STATE_DATABASE: D1Database }, { + coordinator: site => resolveCoordinator(env, site), + execute: (site, input, artifact, lease, prepare) => runNativeBricksMutation(env, site, input, artifact, lease, prepare), + }) + if (native) return native return routeProvisioningApi(request, env as Env & { WORDPRESS_STATE_DATABASE: D1Database }, operations) } let site: SiteContext @@ -310,6 +341,9 @@ export function createCloudflareRuntime( if (error instanceof Error && error.message === "Unknown site hostname.") return new Response(error.message, { status: 421 }) throw error } + if (env.WORDPRESS_STATE_DATABASE && await isProtectedNativeSite(env as Env & { WORDPRESS_STATE_DATABASE: D1Database }, site)) { + return new Response("Native allocation requires its receipt-bound preview API.", { status: 404, headers: { "cache-control": "private, no-store", "x-robots-tag": "noindex, nofollow" } }) + } if (new URL(request.url).pathname === "/wp-cron.php") return new Response("WordPress cron is managed by the Cloudflare scheduled handler.", { status: 404 }) const publishedResponse = await servePublishedWordPressPage(request, env.WORDPRESS_STATE_BUCKET, site) if (publishedResponse) return publishedResponse @@ -352,9 +386,16 @@ export function createCloudflareRuntime( } if (route.kind === "operator-publish") return publishCanonicalWordPressPages(request, env, coordinator, site) if (route.kind === "probe") { - const trace = new CloudflarePhaseTrace() + let canonicalProbe: { pointer: MarkdownPointer; site: SiteContext; authConstants: Record } | undefined + if (route.phase.startsWith("canonical-")) { + if (!await isAuthorizedOperator(request, env, site)) return new Response("Canonical boot probe authorization failed.", { status: 401 }) + const state = await coordinator.state() + if (!state.pointer) return new Response("Canonical boot probe requires an existing pointer.", { status: 409 }) + canonicalProbe = { pointer: state.pointer, site, authConstants: await canonicalWordPressAuthConstants(env, site) } + } + const trace = requestPhaseTrace(env) try { - const response = await trace.measure("boot-probe.opaque", () => runBootProbe(route.phase, env.WORDPRESS_STATE_BUCKET)) + const response = await trace.measure("boot-probe.opaque", () => runBootProbe(route.phase, env.WORDPRESS_STATE_BUCKET, canonicalProbe)) const summary = trace.complete("diagnostic", "cold", "not-applicable") logPhaseTrace(summary) return attachServerTiming(response, summary) @@ -430,6 +471,12 @@ interface RuntimeFileMetadata { sha256: string } +interface CanonicalHydrationPlan { + markdown: MarkdownManifestFile[] + uploads: MarkdownManifestFile[] + wpContent: MarkdownManifestFile[] +} + interface WordPressPageSnapshot { schema: typeof PUBLISHED_PAGE_SCHEMA canonicalRevision: string @@ -1221,7 +1268,7 @@ function validateWordPressPageSnapshot(snapshot: WordPressPageSnapshot, canonica } async function runCoordinatedWordPressRequest(request: Request, env: RuntimeEnv, coordinator: RevisionCoordinator, site: SiteContext, route: "wordpress" | "health" | "r2-mutate" | "static-artifact-import", staticArtifactImport?: StaticArtifactImport, onCommitted?: (committed: { pointer: MarkdownPointer; version: number }) => Promise, onPreparedCommit?: (prepared: { pointer: MarkdownPointer; version: number; ssiResult: unknown; publicationJobKey: string | null }) => Promise, heartbeat?: (stage: string, progress: number) => Promise): Promise { - const trace = new CloudflarePhaseTrace() + const trace = requestPhaseTrace(env) const mutatesCanonicalState = isMutation(request, route) const operation: TraceOperation = route === "health" ? "diagnostic" : mutatesCanonicalState ? "mutation" : "read" let runtimeDisposition: RuntimeDisposition = "not-used" @@ -1489,7 +1536,10 @@ function wordPressPageCacheKey(request: Request, pointer: MarkdownPointer, site: } async function wordPressPageSnapshotKey(request: Request, pointer: MarkdownPointer, site: SiteContext): Promise { - return publishedPageObjectKey(pointer.revision, canonicalPublicRoute(request), site) + const key = await publishedPageObjectKey(pointer.revision, canonicalPublicRoute(request), site) + // Runtime caches must not reuse stale rendered bytes after a runtime upgrade. + // Explicit publication artifacts keep their independent immutable keys. + return `${key.slice(0, -5)}.${WORDPRESS_PAGE_CACHE_SCHEMA}.json` } function pageCacheResponse(response: Response, head: boolean, status: "hit" | "miss", source: "edge" | "r2" | "render"): Response { @@ -1597,9 +1647,24 @@ async function disposeRequestHandler(requestHandler: PHPRequestHandler): Promise await dispose.call(requestHandler) } -async function bootRuntime(bucket: R2Bucket, pointer: MarkdownPointer, origin: string, authConstants: Record, includeWebsiteImporter = false, site: SiteContext = DEFAULT_SITE_CONTEXT, trace?: CloudflarePhaseTrace): Promise { +async function bootRuntime(bucket: R2Bucket, pointer: MarkdownPointer, origin: string, authConstants: Record, includeWebsiteImporter = false, site: SiteContext = DEFAULT_SITE_CONTEXT, trace?: CloudflarePhaseTrace, checkpoint?: (stage: string) => Promise, nativeRuntime = false): Promise { + // Reading the bounded, server-required revision before constructing PHP + // measured about 8 MiB lower at Bricks init than fetching each object from + // inside the live PHP hook. bootWordPressRuntime severs these arrays after + // copying them into MEMFS, so the request handler does not retain a duplicate. const revision = await readCanonicalRevision(bucket, pointer, site, trace) - const booted = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, revision.markdown, new Uint8Array(markdownPrimaryBootstrapIndex), origin, authConstants, bucket, true, revision.uploads, revision.wpContent, revision.wpContentDeleted, includeWebsiteImporter, trace) + await checkpoint?.("canonical-hydrated") + // Bricks media-health checks and WordPress image operations need real files. + // Hydrate the integrity-checked canonical uploads even though browser assets + // are served from R2. Persistence inventories MEMFS, so omitting these files + // would also turn an unrelated save into deletion of every existing upload. + const booted = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, revision.markdown, new Uint8Array(markdownPrimaryBootstrapIndex), origin, authConstants, bucket, true, revision.uploads, revision.wpContent, revision.wpContentDeleted, includeWebsiteImporter, trace, revision.wpContentR2OnlyPaths, undefined, nativeRuntime) + revision.markdown = [] + revision.uploads = [] + revision.wpContent = [] + revision.wpContentDeleted = [] + revision.wpContentR2OnlyPaths = [] + await checkpoint?.("php-ready") return { ...booted, pointer } } @@ -1655,11 +1720,17 @@ require '/wordpress/wp-load.php'; $GLOBALS['wpdb']->flush_canonical_writes(); echo 'flushed';` } -async function canonicalWordPressAuthConstants(env: RuntimeEnv, site: SiteContext): Promise> { - return deriveWordPressAuthConstants(env.WORDPRESS_AUTH_SECRET ?? "", site.id) +async function canonicalWordPressAuthConstants(env: RuntimeEnv, site: SiteContext): Promise> { + const authConstants = await deriveWordPressAuthConstants(env.WORDPRESS_AUTH_SECRET ?? "", site.id) + return { + ...authConstants, + WP_HOME: site.origin, + WP_SITEURL: site.origin, + ...(env.BRICKS_LICENSE_KEY ? { BRICKS_LICENSE_KEY: env.BRICKS_LICENSE_KEY } : {}), + } } -async function persistRuntime(bucket: R2Bucket, runtime: Runtime, changes: MarkdownChanges, site: SiteContext, diagnosticsStartedAt = Date.now(), retained = new MutationRetainedBytes(), trace?: CloudflarePhaseTrace): Promise { +async function persistRuntime(bucket: R2Bucket, runtime: Runtime, changes: MarkdownChanges, site: SiteContext, diagnosticsStartedAt = Date.now(), retained = new MutationRetainedBytes(), trace?: CloudflarePhaseTrace, native?: { releaseBeforePack: () => Promise; checkpoint: (stage: string) => Promise }): Promise { validateMarkdownChanges(changes) const currentManifest = trace ? await trace.measure("canonical.manifest.current.read", () => readMarkdownManifest(bucket, runtime.pointer, site)) : await readMarkdownManifest(bucket, runtime.pointer, site) if (!currentManifest) throw new Error(`R2 Markdown manifest is missing: ${runtime.pointer.manifestKey}`) @@ -1668,12 +1739,31 @@ async function persistRuntime(bucket: R2Bucket, runtime: Runtime, changes: Markd validateWpContentDeletedPaths(currentManifest.wpContentDeleted ?? [], WEBSITE_IMPORTER_OWNED_WP_CONTENT_PATHS) const changedPaths = [...changes.created, ...changes.changed].sort((left, right) => left.localeCompare(right)) const uploads = trace ? await trace.measure("persistence.upload.inventory", () => collectUploadFiles(runtime.php)) : await collectUploadFiles(runtime.php) + await native?.checkpoint("upload-inventoried") logMutationPhase(diagnosticsStartedAt, "upload-inventory", retained, { files: uploads.length, bytes: sumMetadataBytes(uploads) }) const uploadManifestFiles = trace ? await trace.measure("persistence.upload.write", () => persistRuntimeObjects(bucket, runtime.php, UPLOADS_ROOT, uploads, currentManifest.uploads ?? [], siteStorageKeys(site).uploadObjectPrefix, retained)) : await persistRuntimeObjects(bucket, runtime.php, UPLOADS_ROOT, uploads, currentManifest.uploads ?? [], siteStorageKeys(site).uploadObjectPrefix, retained) logMutationPhase(diagnosticsStartedAt, "upload-persist", retained, { files: uploadManifestFiles.length }) - const wpContent = trace ? await trace.measure("persistence.wp-content.inventory", () => collectWpContentFiles(runtime.php)) : await collectWpContentFiles(runtime.php) + await native?.checkpoint("uploads-persisted") + // Native document authoring cannot mutate its pinned theme/plugin package. + // Keep that immutable inventory; generated media/CSS lives under uploads. + const wpContentInventory = native ? { files: currentManifest.wpContent ?? [], deleted: currentManifest.wpContentDeleted ?? [] } : trace ? await trace.measure("persistence.wp-content.inventory", () => collectWpContentFiles(runtime.php)) : await collectWpContentFiles(runtime.php) + // A reconstructed PHP-WASM runtime intentionally does not contain Bricks' + // R2-only public assets/translations. Retain their immutable manifest rows + // across content-only edits; a warm theme upload supplies the authoritative + // full inventory when those files are first introduced or changed. + // Warm runtimes have the real Bricks static files after an upload. Cold + // runtimes intentionally contain only zero-byte PHP metadata stubs. Retain + // the manifest rows only when the current inventory does not already carry + // the file; otherwise adding both copies creates duplicate manifest paths. + const wpContentInventoryPaths = new Set(wpContentInventory.files.map((file) => file.path)) + const retainedR2OnlyWpContent = (currentManifest.wpContent ?? []).filter((file) => !isWorkerMaterializedWpContentPath(file.path) && !wpContentInventoryPaths.has(file.path) && !wpContentInventory.deleted.includes(file.path)) + const wpContent = { + files: [...wpContentInventory.files, ...retainedR2OnlyWpContent].sort((left, right) => left.path.localeCompare(right.path)), + deleted: wpContentInventory.deleted, + } logMutationPhase(diagnosticsStartedAt, "wp-content-inventory", retained, { files: wpContent.files.length, bytes: sumMetadataBytes(wpContent.files), deleted: wpContent.deleted.length }) const wpContentManifestFiles = trace ? await trace.measure("persistence.wp-content.write", () => persistRuntimeObjects(bucket, runtime.php, "/wordpress/wp-content", wpContent.files, currentManifest.wpContent ?? [], siteStorageKeys(site).wpContentObjectPrefix, retained)) : await persistRuntimeObjects(bucket, runtime.php, "/wordpress/wp-content", wpContent.files, currentManifest.wpContent ?? [], siteStorageKeys(site).wpContentObjectPrefix, retained) + await native?.checkpoint("wp-content-preserved") logMutationPhase(diagnosticsStartedAt, "wp-content-persist", retained, { files: wpContentManifestFiles.length }) const manifestFiles = new Map(currentManifest.files.map((file) => [file.path, file])) @@ -1702,6 +1792,7 @@ async function persistRuntime(bucket: R2Bucket, runtime: Runtime, changes: Markd if (trace) trace.end({ bytes: markdownBytes, failed: true }) throw error } + await native?.checkpoint("canonical-objects-persisted") logMutationPhase(diagnosticsStartedAt, "markdown-persist", retained, { files: changedPaths.length }) const files = [...manifestFiles.values()].sort((left, right) => left.path.localeCompare(right.path)) @@ -1710,7 +1801,7 @@ async function persistRuntime(bucket: R2Bucket, runtime: Runtime, changes: Markd && JSON.stringify(currentManifest.wpContent ?? []) === JSON.stringify(wpContentManifestFiles) && JSON.stringify(currentManifest.wpContentDeleted ?? []) === JSON.stringify(wpContent.deleted) if (unchanged) return runtime.pointer - return trace ? trace.measure("persistence.manifest.write", () => persistMarkdownManifest(bucket, files, site, uploadManifestFiles, wpContentManifestFiles, wpContent.deleted), { files: files.length }) : persistMarkdownManifest(bucket, files, site, uploadManifestFiles, wpContentManifestFiles, wpContent.deleted) + return trace ? trace.measure("persistence.manifest.write", () => persistMarkdownManifest(bucket, files, site, uploadManifestFiles, wpContentManifestFiles, wpContent.deleted, { php: runtime.php, releaseBeforePack: native?.releaseBeforePack, checkpoint: native?.checkpoint }), { files: files.length }) : persistMarkdownManifest(bucket, files, site, uploadManifestFiles, wpContentManifestFiles, wpContent.deleted, { php: runtime.php, releaseBeforePack: native?.releaseBeforePack, checkpoint: native?.checkpoint }) } function readCanonicalChanges(php: PHP): MarkdownChanges { @@ -1842,7 +1933,7 @@ function isCanonicalRelativePath(path: string): boolean { return path.length > 0 && !path.startsWith("/") && !path.includes("\\") && !path.split("/").includes("..") } -async function readCanonicalRevision(bucket: R2Bucket, pointer: MarkdownPointer, site: SiteContext, trace?: CloudflarePhaseTrace): Promise<{ markdown: RuntimeFile[]; uploads: RuntimeFile[]; wpContent: RuntimeFile[]; wpContentDeleted: string[] }> { +async function readCanonicalRevision(bucket: R2Bucket, pointer: MarkdownPointer, site: SiteContext, trace?: CloudflarePhaseTrace): Promise<{ markdown: RuntimeFile[]; uploads: RuntimeFile[]; wpContent: RuntimeFile[]; wpContentR2OnlyPaths: string[]; wpContentDeleted: string[] }> { if (!isCanonicalRestorePointer(pointer, site)) throw new Error("Canonical pointer belongs to a different site namespace.") const readManifest = async () => { const manifestObject = await bucket.get(pointer.manifestKey) @@ -1856,10 +1947,13 @@ async function readCanonicalRevision(bucket: R2Bucket, pointer: MarkdownPointer, return manifest } const manifest = trace ? await trace.measure("canonical.manifest.read", readManifest) : await readManifest() - const objectEvidence = { markdownFiles: manifest.files.length, markdownBytes: sumManifestFileBytes(manifest.files), uploadFiles: manifest.uploads?.length ?? 0, uploadBytes: sumManifestFileBytes(manifest.uploads ?? []), wpContentFiles: manifest.wpContent?.length ?? 0, wpContentBytes: sumManifestFileBytes(manifest.wpContent ?? []), wpContentDeleted: manifest.wpContentDeleted?.length ?? 0 } + const materializedWpContent = (manifest.wpContent ?? []).filter((file) => isWorkerMaterializedWpContentPath(file.path)) + const wpContentR2OnlyPaths = (manifest.wpContent ?? []).filter((file) => !isWorkerMaterializedWpContentPath(file.path)).map((file) => file.path) + const objectEvidence = { markdownFiles: manifest.files.length, markdownBytes: sumManifestFileBytes(manifest.files), uploadFiles: manifest.uploads?.length ?? 0, uploadBytes: sumManifestFileBytes(manifest.uploads ?? []), wpContentFiles: manifest.wpContent?.length ?? 0, wpContentBytes: sumManifestFileBytes(manifest.wpContent ?? []), wpContentMaterializedFiles: materializedWpContent.length, wpContentMaterializedBytes: sumManifestFileBytes(materializedWpContent), wpContentDeleted: manifest.wpContentDeleted?.length ?? 0 } if (manifest.restorePack !== undefined) { const restorePack = manifest.restorePack - validateCanonicalRestorePackMetadata(restorePack, siteStorageKeys(site).root, { markdown: manifest.files, uploads: manifest.uploads ?? [], wpContent: manifest.wpContent ?? [] }) + const runtimePackFiles = { markdown: manifest.files, uploads: [], wpContent: materializedWpContent, wpContentDeleted: manifest.wpContentDeleted ?? [] } + validateCanonicalRestorePackMetadata(restorePack, siteStorageKeys(site).root, runtimePackFiles) const fetchPack = async () => { const object = await bucket.get(restorePack.objectKey) if (!object) throw new Error("Canonical restore pack is missing.") @@ -1867,29 +1961,39 @@ async function readCanonicalRevision(bucket: R2Bucket, pointer: MarkdownPointer, return new Uint8Array(await object.arrayBuffer()) } const pack = trace ? await trace.measure("canonical.restore-pack.fetch", fetchPack, { requests: 1, bytes: restorePack.size }) : await fetchPack() - const restore = () => decodeCanonicalRestorePack(restorePack, siteStorageKeys(site).root, { markdown: manifest.files, uploads: manifest.uploads ?? [], wpContent: manifest.wpContent ?? [], wpContentDeleted: manifest.wpContentDeleted ?? [] }, pack) - return trace ? trace.measure("canonical.restore-pack.verify-decode", restore, { requests: 0, bytes: restorePack.decodedBytes, files: restorePack.fileCount }) : restore() + const restore = () => decodeCanonicalRestorePack(restorePack, siteStorageKeys(site).root, runtimePackFiles, pack) + const restored = trace ? await trace.measure("canonical.restore-pack.verify-decode", restore, { requests: 0, bytes: restorePack.decodedBytes, files: restorePack.fileCount }) : await restore() + // Restore packs deliberately exclude media. The authoritative upload + // manifest still needs hydration, with the same hash checks as non-pack boot. + const uploads = await readManifestFiles(bucket, manifest.uploads ?? [], "upload") + return { markdown: restored.markdown, uploads, wpContent: restored.wpContent, wpContentR2OnlyPaths, wpContentDeleted: restored.wpContentDeleted } } const hydrate = () => Promise.all([ readManifestFiles(bucket, manifest.files, "Markdown"), readManifestFiles(bucket, manifest.uploads ?? [], "upload"), - readManifestFiles(bucket, manifest.wpContent ?? [], "wp-content"), + readManifestFiles(bucket, materializedWpContent, "wp-content"), ]) - const [markdown, uploads, wpContent] = trace ? await trace.measure("canonical.objects.hydrate", hydrate, { ...objectEvidence, requests: manifest.files.length + (manifest.uploads?.length ?? 0) + (manifest.wpContent?.length ?? 0) }) : await hydrate() - return { markdown, uploads, wpContent, wpContentDeleted: manifest.wpContentDeleted ?? [] } + const [markdown, uploads, wpContent] = trace ? await trace.measure("canonical.objects.hydrate", hydrate, { ...objectEvidence, requests: manifest.files.length + (manifest.uploads?.length ?? 0) + materializedWpContent.length }) : await hydrate() + return { markdown, uploads, wpContent, wpContentR2OnlyPaths, wpContentDeleted: manifest.wpContentDeleted ?? [] } } function sumManifestFileBytes(files: MarkdownManifestFile[]): number { return files.reduce((total, file) => total + file.size, 0) } async function readManifestFiles(bucket: R2Bucket, files: MarkdownManifestFile[], label: string): Promise { - return Promise.all(files.map(async (file): Promise => { - const object = await bucket.get(file.objectKey) - if (!object) throw new Error(`R2 ${label} object is missing: ${file.objectKey}`) - if (object.size !== file.size) throw new Error(`R2 ${label} object failed size validation: ${file.objectKey}`) - const bytes = new Uint8Array(await object.arrayBuffer()) - if (bytes.byteLength !== file.size || await sha256Hex(bytes) !== file.sha256) throw new Error(`R2 ${label} object failed integrity validation: ${file.objectKey}`) - return { path: file.path, bytes } - })) + const result: RuntimeFile[] = [] + // A first native allocation may have hundreds of PHP files before a restore + // pack exists. Bound concurrent R2 requests and their retained response data. + for (let offset = 0; offset < files.length; offset += 24) { + result.push(...await Promise.all(files.slice(offset, offset + 24).map(async (file): Promise => { + const object = await bucket.get(file.objectKey) + if (!object) throw new Error(`R2 ${label} object is missing: ${file.objectKey}`) + if (object.size !== file.size) throw new Error(`R2 ${label} object failed size validation: ${file.objectKey}`) + const bytes = new Uint8Array(await object.arrayBuffer()) + if (bytes.byteLength !== file.size || await sha256Hex(bytes) !== file.sha256) throw new Error(`R2 ${label} object failed integrity validation: ${file.objectKey}`) + return { path: file.path, bytes } + }))) + } + return result } async function persistMarkdownRevision(bucket: R2Bucket, files: RuntimeFile[], site: SiteContext = DEFAULT_SITE_CONTEXT, current?: MarkdownPointer, changes?: MarkdownChanges, uploads: RuntimeFile[] = [], wpContent: RuntimeFile[] = [], wpContentDeleted: string[] = []): Promise { @@ -1976,7 +2080,7 @@ function validateUploadFiles(files: RuntimeFile[]): void { validateUploadMetadata(files.map((file) => ({ path: file.path, size: file.bytes.byteLength }))) } -async function persistMarkdownManifest(bucket: R2Bucket, files: MarkdownManifestFile[], site: SiteContext, uploads: MarkdownManifestFile[] = [], wpContent: MarkdownManifestFile[] = [], wpContentDeleted: string[] = []): Promise { +async function persistMarkdownManifest(bucket: R2Bucket, files: MarkdownManifestFile[], site: SiteContext, uploads: MarkdownManifestFile[] = [], wpContent: MarkdownManifestFile[] = [], wpContentDeleted: string[] = [], packedSource?: { php?: PHP; metadata?: CanonicalRestorePackMetadata; releaseBeforePack?: () => Promise; checkpoint?: (stage: string) => Promise }): Promise { const revision = crypto.randomUUID() const manifestKey = `${siteStorageKeys(site).markdownRevisionPrefix}/${revision}.json` const persistedAt = new Date().toISOString() @@ -1985,15 +2089,43 @@ async function persistMarkdownManifest(bucket: R2Bucket, files: MarkdownManifest validateUploadManifestFiles(uploads, site) validateWpContentManifestFiles(wpContent, site, WEBSITE_IMPORTER_OWNED_WP_CONTENT_PATHS) validateWpContentDeletedPaths(wpContentDeleted, WEBSITE_IMPORTER_OWNED_WP_CONTENT_PATHS) - // Canonical objects remain authoritative; independently re-read them before deriving the pack. - const [markdown, restoredUploads, restoredWpContent] = await Promise.all([ - readManifestFiles(bucket, files, "Markdown"), - readManifestFiles(bucket, uploads, "upload"), - readManifestFiles(bucket, wpContent, "wp-content"), - ]) - const pack = await createCanonicalRestorePack(siteStorageKeys(site).root, { markdown, uploads: restoredUploads, wpContent: restoredWpContent, wpContentDeleted }) - await putImmutableBytes(bucket, pack.metadata.objectKey, pack.bytes, "application/zip") - const manifest: MarkdownManifest = { ...pointer, files, uploads, wpContent, wpContentDeleted, restorePack: pack.metadata } + // Canonical objects remain authoritative. The one-read runtime pack contains + // only data PHP needs at boot. Existing upload bodies and browser-only Bricks + // assets remain addressable as immutable R2 objects through the full manifest. + const materializedWpContent = wpContent.filter((file) => isWorkerMaterializedWpContentPath(file.path)) + let restorePack = packedSource?.metadata + if (restorePack) { + validateCanonicalRestorePackMetadata(restorePack, siteStorageKeys(site).root, { markdown: files, uploads: [], wpContent: materializedWpContent }) + } else { + const fromPhp = async (root: string, sourceFiles: MarkdownManifestFile[]): Promise => { + const result: RuntimeFile[] = [] + for (const file of sourceFiles) { + let bytes = packedSource!.php!.readFileAsBuffer(`${root}/${file.path}`) + if (bytes.byteLength !== file.size || await sha256Hex(bytes) !== file.sha256) { + // These two files receive checked-in Cloudflare materialization + // patches. Keep the pinned vendor bytes in canonical native packs; + // the same runtime patches are applied again at every reconstruction. + if (!packedSource?.releaseBeforePack || !["themes/bricks/includes/helpers.php", "themes/bricks/includes/init.php"].includes(file.path)) throw new Error(`Materialized restore-pack source differs from canonical objects: ${file.path}`) + bytes = (await readManifestFiles(bucket, [file], "pinned runtime"))[0].bytes + } + result.push({ path: file.path, bytes }) + if (packedSource?.releaseBeforePack) packedSource.php!.unlink(`${root}/${file.path}`) + } + return result + } + // The mutation already has verified PHP files. Refetching every unchanged + // object here consumed over 1,000 subrequests for a native Bricks document. + const [markdown, restoredWpContent] = packedSource?.php + ? await Promise.all([fromPhp(MARKDOWN_ROOT, files), fromPhp("/wordpress/wp-content", materializedWpContent)]) + : await Promise.all([readManifestFiles(bucket, files, "Markdown"), readManifestFiles(bucket, materializedWpContent, "wp-content")]) + await packedSource?.checkpoint?.("pack-source-copied") + await packedSource?.releaseBeforePack?.() + await packedSource?.checkpoint?.("php-released") + const pack = await createCanonicalRestorePack(siteStorageKeys(site).root, { markdown, uploads: [], wpContent: restoredWpContent, wpContentDeleted }) + await putImmutableBytes(bucket, pack.metadata.objectKey, pack.bytes, "application/zip") + restorePack = pack.metadata + } + const manifest: MarkdownManifest = { ...pointer, files, uploads, wpContent, wpContentDeleted, restorePack } await bucket.put(manifestKey, JSON.stringify(manifest), { httpMetadata: { contentType: "application/json" }, }) @@ -2010,7 +2142,11 @@ async function readMarkdownManifest(bucket: R2Bucket, pointer: MarkdownPointer, validateUploadManifestFiles(manifest.uploads ?? [], site) validateWpContentManifestFiles(manifest.wpContent ?? [], site, WEBSITE_IMPORTER_OWNED_WP_CONTENT_PATHS) validateWpContentDeletedPaths(manifest.wpContentDeleted ?? [], WEBSITE_IMPORTER_OWNED_WP_CONTENT_PATHS) - if (manifest.restorePack !== undefined) validateCanonicalRestorePackMetadata(manifest.restorePack, siteStorageKeys(site).root, { markdown: manifest.files, uploads: manifest.uploads ?? [], wpContent: manifest.wpContent ?? [] }) + if (manifest.restorePack !== undefined) validateCanonicalRestorePackMetadata(manifest.restorePack, siteStorageKeys(site).root, { + markdown: manifest.files, + uploads: [], + wpContent: (manifest.wpContent ?? []).filter((file) => isWorkerMaterializedWpContentPath(file.path)), + }) return manifest } @@ -2064,7 +2200,94 @@ async function packagedCanonicalMarkdownSeed(): Promise { return files.sort((left, right) => left.path.localeCompare(right.path)) } -async function runBootProbe(phase: string, bucket: R2Bucket): Promise { +async function runBootProbe( + phase: string, + bucket: R2Bucket, + canonicalProbe?: { pointer: MarkdownPointer; site: SiteContext; authConstants: Record }, +): Promise { + if (phase.startsWith("canonical-")) { + if (!canonicalProbe) return new Response("Canonical boot probe context is unavailable.", { status: 409 }) + const streamedWordPressPhase = phase.match(/^canonical-streamed-(includes|embed|textdomain|ai-client|plugin-constants|muplugins|plugins|globals|theme|site-health-class|site-health|current-user|init|wp-loaded)$/)?.[1] + const initPrefix = phase.match(/^canonical-streamed-init-prefix-(\d{1,2})$/)?.[1] + if (phase === "canonical-streamed-wordpress" || phase === "canonical-streamed-init-list" || phase === "canonical-streamed-wp-loaded-list" || initPrefix || streamedWordPressPhase) { + const runtime = await bootRuntime(bucket, canonicalProbe.pointer, canonicalProbe.site.origin, canonicalProbe.authConstants, false, canonicalProbe.site) + try { + const code = phase === "canonical-streamed-init-list" + ? wordpressInitCallbacksProbeCode() + : phase === "canonical-streamed-wp-loaded-list" + ? wordpressHookCallbacksProbeCode("wp_loaded") + : initPrefix + ? wordpressInitPrefixProbeCode(Number(initPrefix)) + : streamedWordPressPhase + ? wordpressProbeCode(`mdi-${streamedWordPressPhase}`) + : " site_url(), 'wordpressVersion' => get_bloginfo('version'), 'theme' => wp_get_theme()->get('Name'), 'memoryBytes' => memory_get_usage(true), 'peakMemoryBytes' => memory_get_peak_usage(true)]);" + const loaded = (await runtime.php.run({ code })).text.trim() + return probeResponse(phase, JSON.parse(loaded) as Record) + } finally { + await discardRuntime(runtime, canonicalProbe.site) + } + } + const revision = await readCanonicalRevision(bucket, canonicalProbe.pointer, canonicalProbe.site) + const evidence = { + markdownFiles: revision.markdown.length, + markdownBytes: sumRuntimeFileBytes(revision.markdown), + uploadFiles: revision.uploads.length, + uploadBytes: sumRuntimeFileBytes(revision.uploads), + wpContentFiles: revision.wpContent.length, + wpContentBytes: sumRuntimeFileBytes(revision.wpContent), + wpContentR2OnlyFiles: revision.wpContentR2OnlyPaths.length, + } + if (phase === "canonical-hydrate") return probeResponse(phase, evidence) + const wordpressPhase = phase.match(/^canonical-(includes|embed|textdomain|ai-client|plugin-constants|muplugins|plugins|globals|theme|site-health-class|site-health|current-user|init|wp-loaded)$/)?.[1] + const directWpLoadedList = phase === "canonical-wp-loaded-list" + const includeUploads = phase === "canonical-markdown-uploads" || phase === "canonical-all-files" || phase === "canonical-wordpress" || directWpLoadedList || Boolean(wordpressPhase) + const includeWpContent = phase === "canonical-markdown-wpcontent" || phase === "canonical-all-files" || phase === "canonical-wordpress" || directWpLoadedList || Boolean(wordpressPhase) + if (!["canonical-markdown", "canonical-markdown-uploads", "canonical-markdown-wpcontent", "canonical-all-files", "canonical-wordpress"].includes(phase) && !directWpLoadedList && !wordpressPhase) { + return new Response(`Unknown canonical boot probe phase: ${phase}`, { status: 400 }) + } + const runtime = await bootWordPressRuntime( + "do-not-attempt-installing", + true, + true, + undefined, + revision.markdown, + new Uint8Array(markdownPrimaryBootstrapIndex), + canonicalProbe.site.origin, + canonicalProbe.authConstants, + bucket, + true, + undefined, + includeWpContent ? revision.wpContent : undefined, + revision.wpContentDeleted, + false, + undefined, + includeWpContent ? revision.wpContentR2OnlyPaths : [], + ) + // The runtime has copied these bytes into PHP MEMFS. Do not retain a + // second JS-side copy while WordPress and Bricks initialize. + revision.markdown = [] + revision.uploads = [] + revision.wpContent = [] + revision.wpContentR2OnlyPaths = [] + revision.wpContentDeleted = [] + try { + if (directWpLoadedList) { + const listed = (await runtime.php.run({ code: wordpressHookCallbacksProbeCode("wp_loaded") })).text.trim() + return probeResponse(phase, { ...evidence, ...JSON.parse(listed) as Record }) + } + if (wordpressPhase) { + const stopped = (await runtime.php.run({ code: wordpressProbeCode(`mdi-${wordpressPhase}`) })).text.trim() + return probeResponse(phase, { ...evidence, ...JSON.parse(stopped) as Record }) + } + if (phase === "canonical-wordpress") { + const loaded = (await runtime.php.run({ code: " site_url(), 'wordpressVersion' => get_bloginfo('version'), 'theme' => wp_get_theme()->get('Name'), 'memoryBytes' => memory_get_usage(true), 'peakMemoryBytes' => memory_get_peak_usage(true)]);" })).text.trim() + return probeResponse(phase, { ...evidence, ...JSON.parse(loaded) as Record }) + } + return probeResponse(phase, { ...evidence, phpVersion: (await runtime.php.run({ code: "callbacks ?? array()) as $priority => $entries) { + foreach ($entries as $entry) { + $fn = $entry['function']; + if (is_array($fn)) $name = (is_object($fn[0]) ? get_class($fn[0]) : (string) $fn[0]) . '::' . (string) $fn[1]; + elseif (is_string($fn)) $name = $fn; + elseif ($fn instanceof Closure) $name = 'Closure'; + else $name = gettype($fn); + $callbacks[] = array('priority' => (int) $priority, 'name' => $name); + } +} +echo json_encode(array('callbacks' => $callbacks, 'memoryBytes' => memory_get_usage(true), 'peakMemoryBytes' => memory_get_peak_usage(true)));` +} + +function wordpressInitPrefixProbeCode(limit: number): string { + if (!Number.isSafeInteger(limit) || limit < 0 || limit > 99) throw new Error("WordPress init prefix probe limit is invalid.") + return `callbacks ?? array()) as $priority => $entries) { + if ($remaining <= 0) { + unset($GLOBALS['wp_filter']['init']->callbacks[$priority]); + continue; + } + if (count($entries) > $remaining) { + $GLOBALS['wp_filter']['init']->callbacks[$priority] = array_slice($entries, 0, $remaining, true); + $remaining = 0; + } else { + $remaining -= count($entries); + } +do_action('init'); +echo json_encode(array('executedCallbacks' => ${limit}, 'memoryBytes' => memory_get_usage(true), 'peakMemoryBytes' => memory_get_peak_usage(true)));` +} + +function wordpressHookCallbacksProbeCode(hook: "wp_loaded"): string { + const needle = `do_action( '${hook}' );` + return `callbacks ?? array()) as $priority => $entries) { + foreach ($entries as $entry) { + $fn = $entry['function']; + if (is_array($fn)) $name = (is_object($fn[0]) ? get_class($fn[0]) : (string) $fn[0]) . '::' . (string) $fn[1]; + elseif (is_string($fn)) $name = $fn; + elseif ($fn instanceof Closure) $name = 'Closure'; + else $name = gettype($fn); + $callbacks[] = array('priority' => (int) $priority, 'name' => $name); + } +} +echo json_encode(array('hook' => ${JSON.stringify(hook)}, 'callbacks' => $callbacks, 'memoryBytes' => memory_get_usage(true), 'peakMemoryBytes' => memory_get_peak_usage(true)));` +} + async function bootWordPressRuntime( wordpressInstallMode: WordPressInstallMode = "install-from-existing-files", includeSqlite = true, @@ -2287,7 +2580,7 @@ async function bootWordPressRuntime( markdownFiles?: RuntimeFile[], markdownIndexSeed?: Uint8Array, siteUrl = PROBE_SITE_URL, - authConstants: Partial> = {}, + authConstants: Record = {}, runtimeBucket?: R2Bucket, shouldPatchCanonicalRuntimePoliciesAtInit = false, uploadFiles?: RuntimeFile[], @@ -2295,20 +2588,29 @@ async function bootWordPressRuntime( wpContentDeleted: string[] = [], includeWebsiteImporter = false, trace?: CloudflarePhaseTrace, + r2OnlyWpContentPaths: string[] = [], + canonicalHydration?: CanonicalHydrationPlan, + disableOpcodeCache = false, ): Promise<{ php: PHP; requestHandler: PHPRequestHandler; wordpressVersion: string }> { if (includeSqlite && !runtimeBucket) throw new Error("SQLite integration artifact requires WORDPRESS_STATE_BUCKET.") validateWpContentDeletedPaths(wpContentDeleted, WEBSITE_IMPORTER_OWNED_WP_CONTENT_PATHS) const sqliteIntegrationPluginZip = includeSqlite ? trace ? await trace.measure("runtime.archive.sqlite.fetch-verify", () => readSqliteIntegrationArtifact(runtimeBucket!)) : await readSqliteIntegrationArtifact(runtimeBucket!) : undefined const websiteImporterZip = includeWebsiteImporter ? trace ? await trace.measure("runtime.archive.component.fetch-verify", () => readWebsiteImporterArtifact(runtimeBucket!)) : await readWebsiteImporterArtifact(runtimeBucket!) : undefined const boot = () => bootWordPressAndRequestHandler({ - createPhpRuntime: trace ? () => trace.measure("php.runtime.create", () => createPhpRuntime()) : createPhpRuntime, + createPhpRuntime: trace ? () => trace.measure("php.runtime.create", () => createPhpRuntime(disableOpcodeCache)) : () => createPhpRuntime(disableOpcodeCache), + // Avoid OPCache file-cache memory overhead in reconstructed native runtimes. + // The complete PHP/WordPress execution remains enabled. + phpIniEntries: disableOpcodeCache ? { "opcache.enable": "0", "opcache.enable_cli": "0", "opcache.file_cache": "" } : undefined, constants: { AUTOMATIC_UPDATER_DISABLED: true, CONCATENATE_SCRIPTS: false, DISABLE_WP_CRON: true, SCRIPT_DEBUG: false, + WP_DEBUG: false, + WP_DEBUG_DISPLAY: false, + WP_DEBUG_LOG: false, ...authConstants, - ...(markdownFiles ? { + ...(markdownFiles || canonicalHydration ? { 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, @@ -2322,25 +2624,54 @@ async function bootWordPressRuntime( // Browser cookies are carried by the Worker Fetch request; Playground's // internal store would overwrite that header after an isolate restart. cookieStore: false, - hooks: streamWordPressFiles || databaseSeed || markdownFiles || uploadFiles?.length || wpContentFiles?.length || wpContentDeleted.length || includeWebsiteImporter ? { - beforeWordPressFiles: streamWordPressFiles || markdownFiles || uploadFiles?.length || wpContentFiles?.length || wpContentDeleted.length || includeWebsiteImporter ? async (php: PHP) => { + hooks: streamWordPressFiles || databaseSeed || markdownFiles || canonicalHydration || uploadFiles?.length || wpContentFiles?.length || wpContentDeleted.length || r2OnlyWpContentPaths.length || includeWebsiteImporter ? { + beforeWordPressFiles: streamWordPressFiles || markdownFiles || canonicalHydration || uploadFiles?.length || wpContentFiles?.length || wpContentDeleted.length || r2OnlyWpContentPaths.length || includeWebsiteImporter ? async (php: PHP) => { + const markdownHydrationFiles = markdownFiles + const uploadHydrationFiles = uploadFiles + const wpContentHydrationFiles = wpContentFiles if (streamWordPressFiles) trace ? await trace.measure("runtime.archive.wordpress.fetch-verify-extract", () => materializeWordPressServerFiles(php, runtimeBucket)) : await materializeWordPressServerFiles(php, runtimeBucket) if (websiteImporterZip) trace ? await trace.measure("runtime.archive.component.materialize", async () => materializeRuntimeArchiveComponent(php, await websiteImporterZip, WEBSITE_IMPORTER_COMPONENT)) : await materializeRuntimeArchiveComponent(php, await websiteImporterZip, WEBSITE_IMPORTER_COMPONENT) - if (markdownFiles) { + if (markdownHydrationFiles || canonicalHydration) { if (trace) await trace.measure("runtime.archive.mdi.extract", () => materializeMarkdownDatabaseIntegration(php)) else await materializeMarkdownDatabaseIntegration(php) materializeCanonicalChangeAdapter(php) - if (trace) await trace.measure("canonical.hydration.markdown", async () => materializeRuntimeFiles(php, MARKDOWN_ROOT, markdownFiles), { files: markdownFiles.length, bytes: sumRuntimeFileBytes(markdownFiles) }) - else materializeRuntimeFiles(php, MARKDOWN_ROOT, markdownFiles) + if (markdownHydrationFiles) { + if (trace) await trace.measure("canonical.hydration.markdown", async () => materializeRuntimeFiles(php, MARKDOWN_ROOT, markdownHydrationFiles), { files: markdownHydrationFiles.length, bytes: sumRuntimeFileBytes(markdownHydrationFiles) }) + else materializeRuntimeFiles(php, MARKDOWN_ROOT, markdownHydrationFiles) + } else if (canonicalHydration) { + const hydrate = () => materializeManifestFilesBatched(runtimeBucket!, php, MARKDOWN_ROOT, canonicalHydration.markdown, "Markdown") + if (trace) await trace.measure("canonical.hydration.markdown", hydrate, { files: canonicalHydration.markdown.length, bytes: sumManifestFileBytes(canonicalHydration.markdown) }) + else await hydrate() + } if (markdownIndexSeed) php.writeFile(MARKDOWN_RESOLVED_INDEX_PATH, markdownIndexSeed) } - if (wpContentFiles?.length) trace ? await trace.measure("canonical.hydration.wp-content", async () => materializeRuntimeFiles(php, "/wordpress/wp-content", wpContentFiles), { files: wpContentFiles.length, bytes: sumRuntimeFileBytes(wpContentFiles) }) : materializeRuntimeFiles(php, "/wordpress/wp-content", wpContentFiles) + if (wpContentHydrationFiles?.length) trace ? await trace.measure("canonical.hydration.wp-content", async () => materializeRuntimeFiles(php, "/wordpress/wp-content", wpContentHydrationFiles), { files: wpContentHydrationFiles.length, bytes: sumRuntimeFileBytes(wpContentHydrationFiles) }) : materializeRuntimeFiles(php, "/wordpress/wp-content", wpContentHydrationFiles) + else if (canonicalHydration?.wpContent.length) { + const hydrate = () => materializeManifestFilesBatched(runtimeBucket!, php, "/wordpress/wp-content", canonicalHydration.wpContent, "wp-content") + if (trace) await trace.measure("canonical.hydration.wp-content", hydrate, { files: canonicalHydration.wpContent.length, bytes: sumManifestFileBytes(canonicalHydration.wpContent) }) + else await hydrate() + } + if (r2OnlyWpContentPaths.length) materializeBricksAssetStubs(php, r2OnlyWpContentPaths) if (wpContentDeleted.length) trace ? await trace.measure("canonical.hydration.tombstones", () => materializeWpContentTombstones(php, wpContentDeleted), { count: wpContentDeleted.length }) : await materializeWpContentTombstones(php, wpContentDeleted) - if (uploadFiles?.length) trace ? await trace.measure("canonical.hydration.uploads", async () => materializeRuntimeFiles(php, UPLOADS_ROOT, uploadFiles), { files: uploadFiles.length, bytes: sumRuntimeFileBytes(uploadFiles) }) : materializeRuntimeFiles(php, UPLOADS_ROOT, uploadFiles) + if (uploadHydrationFiles?.length) trace ? await trace.measure("canonical.hydration.uploads", async () => materializeRuntimeFiles(php, UPLOADS_ROOT, uploadHydrationFiles), { files: uploadHydrationFiles.length, bytes: sumRuntimeFileBytes(uploadHydrationFiles) }) : materializeRuntimeFiles(php, UPLOADS_ROOT, uploadHydrationFiles) + else if (canonicalHydration?.uploads.length) { + const hydrate = () => materializeManifestFilesBatched(runtimeBucket!, php, UPLOADS_ROOT, canonicalHydration.uploads, "upload") + if (trace) await trace.measure("canonical.hydration.uploads", hydrate, { files: canonicalHydration.uploads.length, bytes: sumManifestFileBytes(canonicalHydration.uploads) }) + else await hydrate() + } if (shouldPatchCanonicalRuntimePoliciesAtInit) { patchCanonicalRuntimePoliciesAtInit(php) patchCanonicalThemeJsonCustomCss(php) + patchBricksCloudflareRuntime(php) } + // These buffers have been copied into PHP MEMFS. The request handler + // retains this hook, so explicitly sever the references before the + // active theme loads and consumes the remainder of the 128 MB isolate. + markdownFiles = undefined + uploadFiles = undefined + wpContentFiles = undefined + wpContentDeleted = [] + r2OnlyWpContentPaths = [] } : undefined, beforeDatabaseSetup: databaseSeed ? async (php: PHP) => { const setup = async () => { php.mkdir("/wordpress/wp-content/database"); php.writeFile(DATABASE_PATH, databaseSeed) } @@ -2357,6 +2688,8 @@ async function bootWordPressRuntime( }) const requestHandler = trace ? await trace.measureComposite("playground.opaque", boot) : await boot() const php = await requestHandler.getPrimaryPhp() + const sqliteDriverPath = "/internal/shared/sqlite-database-integration/wp-includes/database/sqlite/class-wp-pdo-mysql-on-sqlite.php" + if (php.fileExists(sqliteDriverPath)) php.writeFile(sqliteDriverPath, new TextEncoder().encode(patchSqliteDeleteAlias(php.readFileAsText(sqliteDriverPath)))) const wordpressVersion = (trace ? await trace.measure("playground.version.read", () => php.run({ code: " { + for (let offset = 0; offset < files.length; offset += batchSize) { + const hydrated = await readManifestFiles(bucket, files.slice(offset, offset + batchSize), label) + materializeRuntimeFiles(php, root, hydrated) + } +} + +/** + * Bricks computes cache-busting versions with filemtime() while its browser + * assets are served straight from R2. Give PHP zero-byte filesystem entries + * for those asset paths only; the request router still serves the immutable + * R2 bytes, and the persistence collector excludes these known placeholders. + */ +function materializeBricksAssetStubs(php: PHP, paths: string[]): void { + for (const path of paths) { + if (!path.startsWith("themes/bricks/assets/")) continue + const absolute = `/wordpress/wp-content/${path}` + const directory = absolute.slice(0, absolute.lastIndexOf("/")) + if (!php.isDir(directory)) php.mkdir(directory) + php.writeFile(absolute, new Uint8Array()) + } +} + function sumRuntimeFileBytes(files: RuntimeFile[]): number { return files.reduce((total, file) => total + file.bytes.byteLength, 0) } @@ -2401,9 +2764,11 @@ foreach ($paths as $relative) { function patchCanonicalRuntimePoliciesAtInit(php: PHP): void { const settingsPath = "/wordpress/wp-settings.php" const needle = "do_action( 'init' );" + const siteHealthNeedle = "WP_Site_Health::get_instance();" const settings = new TextDecoder().decode(php.readFileAsBuffer(settingsPath)) const firstNeedle = settings.indexOf(needle) - if (firstNeedle === -1 || firstNeedle !== settings.lastIndexOf(needle)) { + const firstSiteHealthNeedle = settings.indexOf(siteHealthNeedle) + if (firstNeedle === -1 || firstNeedle !== settings.lastIndexOf(needle) || firstSiteHealthNeedle === -1 || firstSiteHealthNeedle !== settings.lastIndexOf(siteHealthNeedle)) { throw new Error("WordPress canonical runtime policy patch needle was not uniquely found.") } const replacement = `if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) { @@ -2415,12 +2780,81 @@ function patchCanonicalRuntimePoliciesAtInit(php: PHP): void { add_filter( 'markdown_database_integration_ephemeral_option_names', static function ( $names ) { return array_values( array_diff( $names, array( 'cron' ) ) ); } ); +// Bricks stores its native builder document, template role/conditions, and +// per-document settings in protected post meta. MDI excludes underscore keys +// unless they are explicitly declared portable, so preserve Bricks' complete +// document envelope in the canonical Markdown revision. +add_filter( 'markdown_db_internal_meta_allowlist', static function ( $keys ) { + return array_values( array_unique( array_merge( $keys, array( + '_bricks_page_content_2', + '_bricks_page_header_2', + '_bricks_page_footer_2', + '_bricks_page_settings', + '_bricks_template_settings', + '_bricks_template_type', + '_bricks_mega_menu_template_id', + '_wp_attached_file', + '_wp_attachment_metadata', + '_wp_attachment_backup_sizes', + '_wp_attachment_image_alt', + ) ) ) ); +} ); // Rewrite rules are derived for each runtime; keep them out of canonical MDI state. add_filter( 'pre_update_option_rewrite_rules', static function ( $value, $old_value ) { return $old_value; }, PHP_INT_MAX, 2 ); +// The Cloudflare Bricks profile intentionally exposes a reviewed native +// element palette. It covers the structures used by the governed themes while +// avoiding eager compilation of every specialty and commerce element. +add_filter( 'bricks/builder/elements', static function ( $elements ) { + $allowed = array( + 'container', 'section', 'block', 'div', 'heading', 'text-basic', 'text', + 'text-link', 'button', 'icon', 'image', 'form', 'svg', + ); + return array_values( array_intersect( $elements, $allowed ) ); +}, PHP_INT_MIN ); +// Bricks owns page composition in this profile. WordPress 7 otherwise compiles +// roughly ninety server-rendered Gutenberg block registrations during every +// dynamic boot, exhausting the isolate before Bricks can finish init. +$wp_codebox_bricks_block_callbacks = $GLOBALS['wp_filter']['init']->callbacks ?? array(); +foreach ( $wp_codebox_bricks_block_callbacks as $priority => $callbacks ) { + foreach ( $callbacks as $callback ) { + $function = $callback['function']; + if ( is_string( $function ) && ( + str_starts_with( $function, 'register_block_core_' ) || + in_array( $function, array( + '_register_core_block_patterns_and_categories', + 'wp_register_core_block_metadata_collection', + 'register_core_block_types_from_metadata', + '_register_theme_block_patterns', + 'register_legacy_post_comments_block', + 'register_block_core_footnotes_post_meta', + ), true ) + ) ) { + remove_action( 'init', $function, $priority ); + } + if ( is_array( $function ) && ( $function[0] ?? null ) instanceof \\Bricks\\Integrations\\Block_Editor && ( $function[1] ?? null ) === 'register_blocks' ) { + remove_action( 'init', $function, $priority ); + } + } +} +unset( $wp_codebox_bricks_block_callbacks ); +if ( ! wp_doing_ajax() ) { + foreach ( ( $GLOBALS['wp_filter']['wp_loaded']->callbacks ?? array() ) as $priority => $callbacks ) { + foreach ( $callbacks as $callback ) { + $function = $callback['function']; + if ( is_array( $function ) && ( $function[0] ?? null ) instanceof \\Bricks\\Database && ( $function[1] ?? null ) === 'set_ajax_page_data' ) { + remove_action( 'wp_loaded', $function, $priority ); + } + } + } +} ${needle}` - php.writeFile(settingsPath, new TextEncoder().encode(`${settings.slice(0, firstNeedle)}${replacement}${settings.slice(firstNeedle + needle.length)}`)) + const withCanonicalInit = `${settings.slice(0, firstNeedle)}${replacement}${settings.slice(firstNeedle + needle.length)}` + const siteHealthReplacement = `if ( ! defined( 'DISABLE_WP_CRON' ) || ! DISABLE_WP_CRON ) { + WP_Site_Health::get_instance(); +}` + php.writeFile(settingsPath, new TextEncoder().encode(withCanonicalInit.replace(siteHealthNeedle, siteHealthReplacement))) } function patchCanonicalThemeJsonCustomCss(php: PHP): void { @@ -2437,6 +2871,80 @@ function patchCanonicalThemeJsonCustomCss(php: PHP): void { php.writeFile(path, new TextEncoder().encode(`${source.slice(0, index)}${fastPath}${source.slice(index)}`)) } +function patchBricksCloudflareRuntime(php: PHP): void { + const path = "/wordpress/wp-content/themes/bricks/includes/init.php" + if (!php.isDir("/wordpress/wp-content/themes/bricks/includes")) return + const helpersPath = "/wordpress/wp-content/themes/bricks/includes/helpers.php" + const helpers = new TextDecoder().decode(php.readFileAsBuffer(helpersPath)) + php.writeFile(helpersPath, new TextEncoder().encode(patchBricksRandomIds(helpers))) + let source = new TextDecoder().decode(php.readFileAsBuffer(path)) + const patchedMarkers = [ + "$is_interactive = is_admin() || bricks_is_builder() || wp_doing_ajax()", + "if ( defined( 'WP_CLI' ) && WP_CLI ) $this->cli = new CLI();", + "if ( $is_interactive ) $this->license = new License();", + "if ( class_exists( '\\\\WooCommerce', false ) )", + "if ( is_admin() || bricks_is_builder() )", + "if ( $is_interactive ) $this->block_editor = new Integrations\\Block_Editor();", + "if ( $is_interactive ) $this->api = new Api();", + "if ( $is_interactive ) $this->heartbeat = new Heartbeat();", + ] + if (source.includes(patchedMarkers[0])) { + if (!patchedMarkers.every((marker) => source.includes(marker))) throw new Error("Bricks Cloudflare runtime patch is only partially applied.") + php.writeFile(path, new TextEncoder().encode(restoreBricksSettings(source))) + return + } + const replaceUnique = (needle: string, replacement: string): void => { + const index = source.indexOf(needle) + if (index === -1 || index !== source.lastIndexOf(needle)) throw new Error("Bricks Cloudflare runtime patch needle was not uniquely found.") + source = `${source.slice(0, index)}${replacement}${source.slice(index + needle.length)}` + } + replaceUnique( + "\tpublic function init() {\n\t\tCompatibility::register();", + "\tpublic function init() {\n\t\t$is_interactive = is_admin() || bricks_is_builder() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST );\n\t\tCompatibility::register();", + ) + replaceUnique( + "\t\t$this->cli = new CLI();", + "\t\tif ( defined( 'WP_CLI' ) && WP_CLI ) $this->cli = new CLI();", + ) + replaceUnique( + "\t\t$this->license = new License();\n\t\t$this->setup = new Setup();\n\t\t$this->search = new Search();", + "\t\tif ( $is_interactive ) $this->license = new License();\n\t\t$this->setup = new Setup();\n\t\tif ( $is_interactive ) $this->search = new Search();", + ) + replaceUnique( + "\t\t$this->conditions = new Conditions();\n\n\t\t$this->auth_redirects = new Auth_Redirects();", + "\t\tif ( $is_interactive ) {\n\t\t\t$this->conditions = new Conditions();\n\t\t\t$this->auth_redirects = new Auth_Redirects();\n\t\t}", + ) + replaceUnique( + "\t\t$this->woocommerce = new Woocommerce();\n\n\t\t// Woo Setup Wizard (@since 2.4)\n\t\t$this->woo_setup_wizard = new Woo_Setup_Wizard();", + "\t\t// Load commerce only when WooCommerce is active.\n\t\tif ( class_exists( '\\\\WooCommerce', false ) ) {\n\t\t\t$this->woocommerce = new Woocommerce();\n\t\t\t$this->woo_setup_wizard = new Woo_Setup_Wizard();\n\t\t}", + ) + replaceUnique( + "\t\t$this->media_browser_bulk = new Media_Browser_Bulk();\n\t\t$this->media_browser_health = new Media_Browser_Health();\n\t\t$this->media_browser_query = new Media_Browser_Query();", + "\t\t// Frontend requests read immutable R2 media directly. Builder and wp-admin\n\t\t// retain the complete native media tooling.\n\t\tif ( is_admin() || bricks_is_builder() ) {\n\t\t\t$this->media_browser_bulk = new Media_Browser_Bulk();\n\t\t\t$this->media_browser_health = new Media_Browser_Health();\n\t\t\t$this->media_browser_query = new Media_Browser_Query();\n\t\t}", + ) + replaceUnique( + "\t\t$this->ajax = new Ajax();\n\t\t$this->svg = new Svg();\n\t\t$this->templates = new Templates();\n\t\t$this->settings = new Settings();", + "\t\tif ( $is_interactive ) {\n\t\t\t$this->ajax = new Ajax();\n\t\t\t$this->svg = new Svg();\n\t\t\t$this->settings = new Settings();\n\t\t}\n\t\t$this->templates = new Templates();", + ) + replaceUnique( + "\t\t$this->polylang = new Integrations\\Polylang\\Polylang();\n\t\t$this->wpml = new Integrations\\Wpml\\Wpml();\n\t\t$this->instagram = new Integrations\\Instagram\\Instagram();\n\t\t$this->rank_math = new Integrations\\Rank_Math\\Rank_Math();\n\t\t$this->yoast = new Integrations\\Yoast\\Yoast();", + "\t\tif ( defined( 'POLYLANG_VERSION' ) ) $this->polylang = new Integrations\\Polylang\\Polylang();\n\t\tif ( defined( 'ICL_SITEPRESS_VERSION' ) ) $this->wpml = new Integrations\\Wpml\\Wpml();\n\t\t$this->instagram = new Integrations\\Instagram\\Instagram();\n\t\tif ( defined( 'RANK_MATH_VERSION' ) ) $this->rank_math = new Integrations\\Rank_Math\\Rank_Math();\n\t\tif ( defined( 'WPSEO_VERSION' ) ) $this->yoast = new Integrations\\Yoast\\Yoast();", + ) + replaceUnique( + "\t\t$this->block_editor = new Integrations\\Block_Editor();", + "\t\tif ( $is_interactive ) $this->block_editor = new Integrations\\Block_Editor();", + ) + replaceUnique( + "\t\t$this->api = new Api();", + "\t\tif ( $is_interactive ) $this->api = new Api();", + ) + replaceUnique( + "\t\t$this->heartbeat = new Heartbeat();", + "\t\tif ( $is_interactive ) $this->heartbeat = new Heartbeat();", + ) + php.writeFile(path, new TextEncoder().encode(restoreBricksSettings(source))) +} + function collectRuntimeFiles(php: PHP, root: string, paths?: string[]): RuntimeFile[] { if (paths) { return paths.map((path) => { @@ -2514,6 +3022,7 @@ foreach (array('plugins', 'themes', 'languages', 'mu-plugins') as $root) { if (str_ends_with($runtime_owned_path, '/') ? str_starts_with($path, $runtime_owned_path) : ($path === $runtime_owned_path || str_starts_with($path, $runtime_owned_path . '/'))) continue 2; } $size = $file->getSize(); + if (str_starts_with($path, 'themes/bricks/assets/') && $size === 0) continue; $total += $size; if ($size > ${MAX_WP_CONTENT_FILE_BYTES} || $total > ${MAX_WP_CONTENT_TOTAL_BYTES} || count($files) >= ${MAX_WP_CONTENT_FILES}) { throw new RuntimeException('Canonical wp-content files exceed their budget.'); @@ -2586,7 +3095,18 @@ async function materializeMarkdownDatabaseIntegration(php: PHP): Promise { const { done, value: entry } = await reader.read() if (done) break const relative = entry.name - const bytes = new Uint8Array(await entry.arrayBuffer()) + let bytes = new Uint8Array(await entry.arrayBuffer()) + // MySQL exposes SELECT DISTINCT(table.column) under the column name; + // SQLite exposes the expression. Bricks' native dynamic-data provider + // legitimately reads $row->meta_key from that query. Adapt the MDI driver + // at materialization time so it preserves WordPress' MySQL result shape. + if (relative === "inc/class-wp-markdown-driver.php") { + const source = new TextDecoder().decode(bytes) + const marker = "\tpublic function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) {\n" + const insertion = `${marker}\t\t// The Cloudflare coordinator already serializes each site's PHP\n\t\t// request under a fenced lease. Map MySQL session-lock probes to a\n\t\t// successful scalar result so Bricks' guarded ability transactions can\n\t\t// run on SQLite without weakening the outer site mutation fence.\n\t\tif ( preg_match( '/^\\s*SELECT\\s+(?:GET_LOCK|RELEASE_LOCK)\\s*\\(/i', $query ) ) {\n\t\t\t$query = 'SELECT 1 AS lock_status';\n\t\t}\n\n\t\t$query = preg_replace_callback(\n\t\t\t'/^\\s*SELECT\\s+DISTINCT\\s*\\(\\s*((?:\`[^\`]+\`|[A-Za-z0-9_]+)\\.(?:\`[^\`]+\`|[A-Za-z0-9_]+))\\s*\\)(?!\\s+AS\\b)/i',\n\t\t\tstatic function ( array $matches ): string {\n\t\t\t\t$parts = explode( '.', $matches[1] );\n\t\t\t\t$column = trim( (string) end( $parts ), '\`' );\n\t\t\t\treturn 'SELECT DISTINCT(' . $matches[1] . ') AS \`' . $column . '\`';\n\t\t\t},\n\t\t\t$query\n\t\t);\n\n` + if (!source.includes(marker)) throw new Error("MDI driver no longer exposes its expected query method.") + bytes = new TextEncoder().encode(source.replace(marker, insertion)) + } const destination = `/wordpress/wp-content/plugins/markdown-database-integration/${relative}` php.mkdir(destination.slice(0, destination.lastIndexOf("/"))) php.writeFile(destination, bytes) @@ -2949,10 +3469,16 @@ async function serveWordPressStaticAsset(request: Request, bucket: R2Bucket): Pr return response } -function createPhpRuntime() { +function createPhpRuntime(nativeRuntime = false) { return loadPHPRuntime( { dependencyFilename: "php_8_5.wasm", dependenciesTotalSize, phpWasmAsyncMode: "asyncify", init }, - { instantiateWasm: instantiatePrecompiledWasm(phpWasmModule) }, + { + instantiateWasm: instantiatePrecompiledWasm(phpWasmModule), + // Zend's chunk allocator substantially increases the native Bricks frontend's + // required WASM address space. PHP's system allocator avoids that cost. + // PHP memory_get_usage/peak are unavailable in this mode; use Worker telemetry. + ...(nativeRuntime ? { ENV: { USE_ZEND_ALLOC: "0" } } : {}), + }, ) } @@ -2963,3 +3489,96 @@ function instantiatePrecompiledWasm(module: WebAssembly.Module) { function probeResponse(phase: string, evidence: Record): Response { return Response.json({ schema: "wp-codebox/cloudflare-boot-probe/v1", phase, completed: true, evidence }) } + +async function runNativeBricksMutation(env: RuntimeEnv, site: SiteContext, input: NativeMutation, artifact: NativeBricksArtifact | null, lease: RevisionLease, prepare: (result: NativeExecution) => Promise): Promise { + if (!lease.pointer) throw new Error("Native mutation requires an operator-prepared allocation.") + const start = Date.now() + const restoredFrom = input.authority.restoreTarget + const source = restoredFrom ? { revision: restoredFrom.canonical_state_revision, manifestKey: restoredFrom.canonical_manifest_key, persistedAt: restoredFrom.canonical_persisted_at } : lease.pointer + const checkpoint = async (stage: string) => { + await env.WORDPRESS_STATE_BUCKET.put(`${siteStorageKeys(site).root}/native-diagnostics/${input.operationId}.json`, JSON.stringify({ stage, operation_id: input.operationId, deployment_version: env.WORDPRESS_VERSION_METADATA?.id ?? env.WORDPRESS_NATIVE_DEPLOYMENT_VERSION ?? null, wall_ms: Date.now() - start }), { httpMetadata: { contentType: "application/json" } }) + } + await checkpoint("boot-started") + let runtime: Runtime | undefined = await bootRuntime(env.WORDPRESS_STATE_BUCKET, source, site.origin, await canonicalWordPressAuthConstants(env, site), false, site, undefined, checkpoint, true) + try { + runtime.php.writeFile("/tmp/native-bricks-input.json", new TextEncoder().encode(JSON.stringify({ action: restoredFrom ? "restore-read" : input.action, artifact }))) + runtime.php.writeFile(MARKDOWN_CHANGES_PATH, new TextEncoder().encode(JSON.stringify({ created: [], changed: [], deleted: [] }))) + initializePublicationChanges(runtime.php) + const response = await runtime.php.run({ code: NATIVE_BRICKS_APPLY_PHP }) + await checkpoint("native-php-applied") + let observed: { native_records: { page_ids: number[]; template_ids: number[]; media_ids: number[] }; native_document_hashes: string[]; design_system_version: string; peak_php_bytes: number } + try { observed = JSON.parse(response.text) } catch { throw new Error("Native execution did not produce a clean JSON observation.") } + if (!observed.native_document_hashes?.length || observed.native_document_hashes.some(hash => !/^[a-f0-9]{64}$/.test(hash))) throw new Error("Native execution did not return document digests.") + let pointer: MarkdownPointer + if (restoredFrom) { + if (stableJson(observed.native_document_hashes) !== stableJson(restoredFrom.native_document_hashes) || observed.design_system_version !== restoredFrom.design_system_version) throw new Error("Retained native restore content differs from its exact receipt.") + const manifest = await readMarkdownManifest(env.WORDPRESS_STATE_BUCKET, source, site) + if (!manifest) throw new Error("Retained restore manifest is unavailable.") + await disposeRequestHandler(runtime.requestHandler); runtime = undefined + // New immutable revision, same restored content. The coordinator never rewinds. + pointer = await persistMarkdownManifest(env.WORDPRESS_STATE_BUCKET, manifest.files, site, manifest.uploads ?? [], manifest.wpContent ?? [], manifest.wpContentDeleted ?? [], { metadata: manifest.restorePack }) + } else { + pointer = await persistRuntime(env.WORDPRESS_STATE_BUCKET, runtime, readCanonicalChanges(runtime.php), site, Date.now(), new MutationRetainedBytes(), undefined, { + checkpoint, + releaseBeforePack: async () => { if (runtime) await disposeRequestHandler(runtime.requestHandler); runtime = undefined }, + }) + if (runtime) await disposeRequestHandler(runtime.requestHandler); runtime = undefined + } + await checkpoint("canonical-persisted") + const revision: NativePointer = { + receipt_id: `native_${crypto.randomUUID().replace(/-/g, "")}`, + canonical_state_version: lease.version + 1, + canonical_state_revision: pointer.revision, + canonical_manifest_key: pointer.manifestKey, + canonical_persisted_at: pointer.persistedAt, + artifact_sha256: restoredFrom?.artifact_sha256 ?? input.artifact!.sha256, + native_document_hashes: observed.native_document_hashes, + design_system_version: observed.design_system_version, + } + const measurements = JSON.stringify({ schema: "wp-codebox/native-bricks-resource-observation/v1", operation_id: input.operationId, wall_ms: Date.now() - start, peak_php_bytes: observed.peak_php_bytes > 0 ? observed.peak_php_bytes : null, php_allocator: "system", unmeasured: ["peak_php_bytes", "worker_cpu_ms", "peak_isolate_bytes", "d1_reads_writes", "r2_operations", "subrequests"] }) + const measurementsHash = await sha256Hex(new TextEncoder().encode(measurements)) + await putImmutableJson(env.WORDPRESS_STATE_BUCKET, `${siteStorageKeys(site).root}/native-evidence/${measurementsHash}.json`, measurements) + const empty = { receipt_id: null, canonical_state_version: null, canonical_state_revision: null, canonical_manifest_key: null, canonical_persisted_at: null, artifact_sha256: null, native_document_hashes: null, design_system_version: null } + const restoreReceiptId = restoredFrom ? `restore_${crypto.randomUUID().replace(/-/g, "")}` : null + const preview = nativePreviewReceipt(site, revision.receipt_id, site.origin) + const receipt = { + schema_version: 1, target_runtime: "wordpress_bricks_cloudflare_v1", action: input.action, operation_id: input.operationId, target_request_sha256: await sha256Hex(new TextEncoder().encode(stableJson(input.targetRequest))), site_id: site.id, customer_id: input.customerId, + state: preview.state === "ready" ? "protected_preview" : "draft", site_allocation: { allocation_id: site.id, canonical_state_revision: pointer.revision }, + native_records: observed.native_records, native_document_hashes: observed.native_document_hashes, design_system_version: observed.design_system_version, + bricks_artifact_sha256: input.targetRequest.bricks_artifact.sha256, dossier_hash: input.targetRequest.dossier.sha256, + creative_provenance: input.targetRequest.creative_provenance, authorized_asset_hashes: input.targetRequest.authorized_assets.map((asset: {sha256:string}) => asset.sha256), + target_role: { ...input.targetRequest.editing.target_role, editability_verified: false }, + acceptance_sequence: input.targetRequest.editing.acceptance_sequence, + protected_preview: preview, + acceptance_results: input.targetRequest.editing.acceptance_sequence.map((step: string) => ({ step, status: "not_run", evidence_sha256: null })), + acceptance_transaction_sha256: null, + deployment: { version: env.WORDPRESS_VERSION_METADATA?.id ?? env.WORDPRESS_NATIVE_DEPLOYMENT_VERSION ?? "unreported-runtime-version", runtime_artifact_version: input.targetRequest.runtime_artifact.version, runtime_artifact_sha256: input.targetRequest.runtime_artifact.sha256, artifact_hashes: [input.targetRequest.runtime_artifact.sha256, revision.artifact_sha256] }, + evidence: { desktop_sha256: null, mobile_sha256: null, client_edit_transaction_sha256: null, resource_measurements_sha256: measurementsHash }, + revision_receipt: revision, rollback_receipt: { ...(input.authority.expectedBase ?? empty) }, + restoration: restoredFrom ? { restore_receipt_id: restoreReceiptId, restored_from: restoredFrom, pre_restore_base: input.authority.expectedBase } : null, + } + const result = { pointer, receipt } + await prepare(result) + return result + } finally { + if (runtime) await disposeRequestHandler(runtime.requestHandler) + await discardCachedRuntime(site.id) + } +} + +// A throwaway runtime renders the exact supplied canonical revision. It never +// commits, caches PHP state, bootstraps a site, or persists request side effects. +async function renderNativeBricksPreview(request: Request, env: RuntimeEnv, site: SiteContext, pointer: MarkdownPointer): Promise { + const pinned = { state: async () => ({ schema: "wp-codebox/cloudflare-wordpress-state/v2", store: "d1", pointer, version: 0 }) } as RevisionCoordinator + const asset = await serveWordPressWpContent(request, env.WORDPRESS_STATE_BUCKET, pinned, site) + ?? await serveWordPressUpload(request, env.WORDPRESS_STATE_BUCKET, pinned, site) + ?? await serveWordPressStaticAsset(request, env.WORDPRESS_STATE_BUCKET) + if (asset) return asset + if (/^\/wp-(?:content|includes)\//.test(new URL(request.url).pathname)) return new Response("Native preview asset not found.", { status: 404 }) + const startedAt = Date.now() + const checkpoint = async (stage: string) => { await env.WORDPRESS_STATE_BUCKET.put(`${siteStorageKeys(site).root}/native-diagnostics/preview.json`, JSON.stringify({ stage, revision: pointer.revision, deployment_version: env.WORDPRESS_VERSION_METADATA?.id ?? null, wall_ms: Date.now() - startedAt })) } + await checkpoint("preview-started") + const runtime = await bootRuntime(env.WORDPRESS_STATE_BUCKET, pointer, site.origin, await canonicalWordPressAuthConstants(env, site), false, site, undefined, checkpoint, true) + try { const response = toFetchResponse(request, await runtime.requestHandler.request(await toPHPRequest(request))); await checkpoint("preview-rendered"); return response } + finally { await disposeRequestHandler(runtime.requestHandler); await checkpoint("preview-disposed") } +} diff --git a/packages/runtime-cloudflare/src/wp-content-persistence.ts b/packages/runtime-cloudflare/src/wp-content-persistence.ts index 21b36b91..e802ced0 100644 --- a/packages/runtime-cloudflare/src/wp-content-persistence.ts +++ b/packages/runtime-cloudflare/src/wp-content-persistence.ts @@ -3,7 +3,10 @@ import { DEFAULT_SITE_CONTEXT, siteStorageKeys, type SiteContext } from "./site- export const R2_WP_CONTENT_OBJECT_PREFIX = siteStorageKeys(DEFAULT_SITE_CONTEXT).wpContentObjectPrefix export const MAX_WP_CONTENT_FILES = 5000 export const MAX_WP_CONTENT_FILE_BYTES = 8 * 1024 * 1024 -export const MAX_WP_CONTENT_TOTAL_BYTES = 64 * 1024 * 1024 +// This is an R2-backed canonical-storage limit, not an in-isolate memory +// allocation. The Bricks 2.4 probe persists its public asset tree in R2 and +// hydrates only the files PHP needs below. +export const MAX_WP_CONTENT_TOTAL_BYTES = 160 * 1024 * 1024 export interface WpContentFileMetadata { path: string @@ -28,7 +31,9 @@ export function validateWpContentMetadata(value: unknown, runtimeOwnedPaths: str for (const file of value) { if (!file || typeof file !== "object" || typeof file.path !== "string" || !isCanonicalWpContentPath(file.path, runtimeOwnedPaths) || paths.has(file.path) || !Number.isSafeInteger(file.size) || file.size < 0 || file.size > MAX_WP_CONTENT_FILE_BYTES) { - throw new Error("Canonical wp-content metadata contains an invalid file.") + const path = file && typeof file === "object" && "path" in file ? String(file.path) : "unknown" + const size = file && typeof file === "object" && "size" in file ? String(file.size) : "unknown" + throw new Error(`Canonical wp-content metadata contains an invalid file: ${path} (${size} bytes).`) } paths.add(file.path) total += file.size @@ -57,6 +62,20 @@ export function isCanonicalWpContentPath(path: string, runtimeOwnedPaths: string return !runtimeOwnedWpContentPaths(runtimeOwnedPaths).some((owned) => owned.endsWith("/") ? path.startsWith(owned) : path === owned || path.startsWith(`${owned}/`)) } +/** + * Files required by PHP-WASM at boot. Bricks' asset and translation trees are + * intentionally left in the canonical R2 manifest: the Worker already serves + * public wp-content assets directly from that manifest, and materializing + * those 62 MB of browser-only files into PHP-WASM would exceed the Cloudflare + * Worker memory ceiling before WordPress can run. + * + * This is deliberately narrow to the exact Bricks 2.4 package under test; + * it is not a claim that arbitrary theme/plugin paths are safe to omit. + */ +export function isWorkerMaterializedWpContentPath(path: string): boolean { + return !path.startsWith("themes/bricks/assets/") && !path.startsWith("themes/bricks/languages/") +} + export function runtimeOwnedWpContentPaths(additional: string[] = []): string[] { const paths = [...new Set([...DEFAULT_RUNTIME_OWNED_PATHS, ...additional])] if (paths.some((path) => !/^(?:plugins|mu-plugins)\//.test(path) || path === "plugins/" || path === "mu-plugins/" || path.includes("\\") || path.split("/").some((segment, index, parts) => !segment && index !== parts.length - 1 || segment === "." || segment === ".."))) throw new Error("Runtime-owned wp-content path is invalid.") diff --git a/packages/runtime-cloudflare/tests/cloudflare-bricks-clock.test.ts b/packages/runtime-cloudflare/tests/cloudflare-bricks-clock.test.ts new file mode 100644 index 00000000..fb99213b --- /dev/null +++ b/packages/runtime-cloudflare/tests/cloudflare-bricks-clock.test.ts @@ -0,0 +1,24 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { patchBricksRandomIds, restoreBricksSettings } from "../src/bricks-clock-compatibility.js"; + +test("Bricks ID compatibility replaces only entropy generation and is idempotent", () => { + const source = `public static function generate_random_id( $echo = true ) { + $hash = self::generate_hash( md5( uniqid( rand(), true ) ) ); + if ( $echo ) echo $hash; + return $hash; + }`; + const patched = patchBricksRandomIds(source); + assert.equal(patched, source.replace("md5( uniqid( rand(), true ) )", "bin2hex( random_bytes( 16 ) )")); + assert.equal(patchBricksRandomIds(patched), patched); + assert.throws(() => patchBricksRandomIds(source + source), /Unsupported/); + assert.throws(() => patchBricksRandomIds("different implementation"), /Unsupported/); +}); + +test("Settings dependency initialization moves outside interactive guard", () => { + const source = "\t\tif ( $is_interactive ) {\n\t\t\t$this->ajax = new Ajax();\n\t\t\t$this->settings = new Settings();\n\t\t}\n\t\t$this->templates = new Templates();"; + const patched = restoreBricksSettings(source); + assert.match(patched, /}\n\t\t\$this->templates = new Templates\(\);\n\t\t\$this->settings = new Settings\(\);$/); + assert.equal(restoreBricksSettings(patched), patched); + assert.throws(() => restoreBricksSettings(source + source), /Unsupported/); +}); diff --git a/packages/runtime-cloudflare/tests/cloudflare-bricks-preview.test.ts b/packages/runtime-cloudflare/tests/cloudflare-bricks-preview.test.ts new file mode 100644 index 00000000..66c1e482 --- /dev/null +++ b/packages/runtime-cloudflare/tests/cloudflare-bricks-preview.test.ts @@ -0,0 +1,66 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { DatabaseSync } from 'node:sqlite' +import { D1OperationRepository } from '../src/d1-operation-repository.js' +import { isProtectedNativeSite, nativePreviewPathAllowed, nativePreviewReceipt, rewriteNativePreview, routeNativeBricksPreview, type NativePreviewBackend } from '../src/bricks-preview.js' +import type { NativeEnv } from '../src/native-bricks-api.js' +import type { RevisionState } from '../src/revision-coordinator.js' +function database() { + const sqlite=new DatabaseSync(':memory:') + const db={prepare(query:string){const statement=sqlite.prepare(query);let values:any[]=[];return{bind(...args:any[]){values=args;return this},async run(){return{meta:{changes:statement.run(...values).changes}}},async first(){return statement.get(...values)??null},async all(){return{results:statement.all(...values)}}}},async batch(statements:any[]){return Promise.all(statements.map(statement=>statement.run()))}} as unknown as D1Database + return {db,sqlite} +} +const site={id:'native-engine',hostname:'native.example',origin:'https://native.example'} +const pointer={revision:'revision-a',manifestKey:'sites/native-engine/markdown/revisions/a.json',persistedAt:'2026-09-10T00:00:00Z'} +async function fixture(scope='sites:read',principal='owner',sites=['native-engine']) { + const {db,sqlite}=database();await new D1OperationRepository(db).initialize() + sqlite.exec('CREATE TABLE wp_codebox_native_operations(site_id TEXT,principal TEXT,state TEXT,receipt_json TEXT,generation INTEGER)') + sqlite.prepare("INSERT INTO wp_codebox_sites(site_id,hostname,origin,state,created_at,activated_at,updated_at,generation) VALUES(?,?,?,'active',1,1,1,1)").run(site.id,site.hostname,site.origin) + const receipt={site_id:site.id,revision_receipt:{receipt_id:'receipt_a',canonical_state_version:2,canonical_state_revision:pointer.revision,canonical_manifest_key:pointer.manifestKey,canonical_persisted_at:pointer.persistedAt}} + sqlite.prepare("INSERT INTO wp_codebox_native_operations VALUES(?,'owner','succeeded',?,1)").run(site.id,JSON.stringify(receipt)) + const env:NativeEnv={WORDPRESS_STATE_DATABASE:db,WORDPRESS_STATE_BUCKET:{} as R2Bucket,WORDPRESS_SITE_CONTEXTS:JSON.stringify([site]),WORDPRESS_BRICKS_PREPARED_ALLOCATIONS:JSON.stringify({[site.id]:{}}),WORDPRESS_API_TOKENS:JSON.stringify([{id:'preview',principal,digest:createHash('sha256').update('credential').digest('hex'),scopes:[scope],sites,expiresAt:'2099-01-01T00:00:00.000Z',maxSites:1}])} + let renders=0;let state:RevisionState={schema:'wp-codebox/cloudflare-wordpress-state/v2',store:'d1',version:2,pointer} + const backend:NativePreviewBackend={state:async()=>state,render:async(request,_site,received)=>{renders++;assert.deepEqual(received,pointer);assert.equal(request.headers.has('authorization'),false);assert.equal(request.headers.has('cookie'),false);return new Response('

Native content

',{headers:{'content-type':'text/html','cache-control':'public,max-age=60','set-cookie':'logged_in=bad','etag':'old'}})}} + const request=(path='/',headers:Record={},method='GET')=>new Request(`https://control.example/v1/bricks/sites/${site.id}/previews/receipt_a${path}`,{method,headers:{authorization:'Bearer credential','x-wp-codebox-preview-base':'/preview/protected_view',...headers}}) + return {env,sqlite,backend,request,renders:()=>renders,setState:(value:RevisionState)=>{state=value},state} +} +test('native preview authenticates exact receipt and rewrites HTML assets under private customer mount',async()=>{ + const f=await fixture();const response=(await routeNativeBricksPreview(f.request('/',{cookie:'wordpress_logged_in=secret'}),f.env,f.backend))! + assert.equal(response.status,200);assert.equal(response.headers.get('cache-control'),'private, no-store');assert.equal(response.headers.get('set-cookie'),null);assert.equal(response.headers.get('etag'),null) + assert.equal(response.headers.get('x-wp-codebox-native-receipt'),'receipt_a');assert.equal(response.headers.get('x-wp-codebox-canonical-version'),'2') + const html=await response.text();assert.match(html,/brxe-heading/);assert.match(html,/src="\/preview\/protected_view\/wp-content\/uploads\/a.jpg"/);assert.match(html,/href="\/preview\/protected_view\/wp-content\/themes\/bricks\/a.css"/) + assert.equal(html.includes('native.example'),false);assert.equal(f.renders(),1) +}) +test('native preview denies wrong credentials, scope, principal, site and uncommitted receipt before PHP',async()=>{ + for(const [scope,principal,sites,status] of [['sites:create','owner',[site.id],403],['sites:read','foreign',[site.id],404],['sites:read','owner',['elsewhere'],404]] as const){const f=await fixture(scope,principal,[...sites]);assert.equal((await routeNativeBricksPreview(f.request(),f.env,f.backend))!.status,status);assert.equal(f.renders(),0)} + const f=await fixture();assert.equal((await routeNativeBricksPreview(f.request('/',{authorization:''}),f.env,f.backend))!.status,401) + f.sqlite.exec("UPDATE wp_codebox_native_operations SET state='running'");assert.equal((await routeNativeBricksPreview(f.request(),f.env,f.backend))!.status,404);assert.equal(f.renders(),0) +}) +test('native preview rejects stale or concurrently replaced canonical revision and inactive allocation',async()=>{ + for(const phase of ['before','during']) {const f=await fixture();if(phase==='before')f.setState({...f.state,version:3});else f.backend.render=async()=>{f.setState({...f.state,version:3});return new Response('stale body')};const response=(await routeNativeBricksPreview(f.request(),f.env,f.backend))!;assert.equal(response.status,409);assert.equal((await response.text()).includes('stale body'),false)} + const f=await fixture();f.sqlite.exec("UPDATE wp_codebox_sites SET generation=2");assert.equal((await routeNativeBricksPreview(f.request(),f.env,f.backend))!.status,410);assert.equal(f.renders(),0) +}) +test('native preview denies admin/PHP/actions/encoded paths and writes while permitting browser assets and frontend reads',async()=>{ + for(const path of ['/wp-admin/','/wp-login.php','/index.php','/wp-json/','/__internal','/wp-content/secret.json','/wp-content/a.php','/%77p-admin/','/?bricks=run','/?action=delete','/?rest_route=/wp/v2/users']) {const f=await fixture();assert.equal((await routeNativeBricksPreview(f.request(path),f.env,f.backend))!.status,403,path);assert.equal(f.renders(),0)} + for(const path of ['/','/services/','/?p=13','/wp-content/themes/bricks/a.css?ver=2.4','/wp-content/uploads/a.webp','/wp-includes/js/jquery/jquery.min.js']){const url=new URL(path,'https://example.test');assert.equal(nativePreviewPathAllowed(url.pathname,url.searchParams),true,path)} + const f=await fixture();assert.equal((await routeNativeBricksPreview(f.request('/',{},'POST'),f.env,f.backend))!.status,405) +}) +test('native preview streams binary assets privately and rewrites only authorized frontend redirects',async()=>{ + const f=await fixture();f.backend.render=async()=>new Response(new Uint8Array([1,2,3]),{headers:{'content-type':'image/jpeg'}}) + const asset=(await routeNativeBricksPreview(f.request('/wp-content/uploads/a.jpg'),f.env,f.backend))!;assert.deepEqual(new Uint8Array(await asset.arrayBuffer()),new Uint8Array([1,2,3]));assert.equal(asset.headers.get('cache-control'),'private, no-store') + f.backend.render=async()=>new Response(null,{status:301,headers:{location:'https://native.example/services/'}});assert.equal((await routeNativeBricksPreview(f.request(),f.env,f.backend))!.headers.get('location'),'/preview/protected_view/services/') + f.backend.render=async()=>new Response(null,{status:302,headers:{location:'/wp-admin/'}});assert.equal((await routeNativeBricksPreview(f.request(),f.env,f.backend))!.status,409) +}) +test('native protection includes prepared and previously owned allocations while public default is unchanged',async()=>{ + const f=await fixture();assert.equal(await isProtectedNativeSite(f.env,site),true) + delete f.env.WORDPRESS_BRICKS_PREPARED_ALLOCATIONS;assert.equal(await isProtectedNativeSite(f.env,site),true) + f.sqlite.exec('DELETE FROM wp_codebox_native_operations');assert.equal(await isProtectedNativeSite(f.env,site),false) + f.env.WORDPRESS_BRICKS_PREPARED_ALLOCATIONS=JSON.stringify({[site.id]:{},default:{}});assert.equal(await isProtectedNativeSite(f.env,site),true);assert.equal(await isProtectedNativeSite(f.env,{...site,id:'default'}),false) + assert.equal(nativePreviewReceipt(site,'receipt_a','https://runtime.example').url,'https://runtime.example/v1/bricks/sites/native-engine/previews/receipt_a/') + assert.equal(nativePreviewReceipt({...site,id:'default'},'receipt_a','https://runtime.example').state,'requested') +}) +test('native URL rewriting handles CSS/srcset/escaped JSON without adding mount twice',()=>{ + const value='' + const output=rewriteNativePreview(value,site.origin,'/preview/token');assert.equal(output.includes('native.example'),false);assert.match(output,/url\(\/preview\/token\/wp-content\/c.jpg\)/);assert.match(output,/, \/preview\/token\/wp-content\/b.jpg/);assert.equal(output.includes('/preview/token/preview/token'),false) +}) diff --git a/packages/runtime-cloudflare/tests/cloudflare-native-bricks-api.test.ts b/packages/runtime-cloudflare/tests/cloudflare-native-bricks-api.test.ts new file mode 100644 index 00000000..b79fb8f4 --- /dev/null +++ b/packages/runtime-cloudflare/tests/cloudflare-native-bricks-api.test.ts @@ -0,0 +1,20 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { validateNativeMutation, type NativeMutation } from '../src/native-bricks-api.js' +const hash='a'.repeat(64) +const empty={receipt_id:null,canonical_state_version:null,canonical_state_revision:null,canonical_manifest_key:null,canonical_persisted_at:null,artifact_sha256:null,native_document_hashes:null,design_system_version:null} +const pointer={receipt_id:'receipt_1',canonical_state_version:1,canonical_state_revision:'revision-1',canonical_manifest_key:'sites/default/revision-1.json',canonical_persisted_at:'2026-09-10T00:00:00.000Z',artifact_sha256:hash,native_document_hashes:[hash],design_system_version:'design-v1'} +function input(action:NativeMutation['action']):NativeMutation { + const base=action==='provision'?null:pointer, target=action==='restore'?{...pointer,canonical_state_version:2}:null + return {schema:'wp-codebox/native-bricks-mutation-request/v1',action,idempotencyKey:'operation_1',operationId:'operation_1',siteId:'default',customerId:'customer_1',artifact:action==='restore'?null:{sha256:hash,size:10,r2Key:`sites/provisioning/bricks-artifacts/${hash}.json`},authority:{expectedBase:base,restoreTarget:target},targetRequest:{schema_version:1,target_runtime:'wordpress_bricks_cloudflare_v1',operation_id:'operation_1',idempotency_key:'operation_1',site_id:'default',customer_id:'customer_1',bricks_artifact:{version:'wp-codebox/bricks-site-artifact/v1',sha256:hash},license_secret_ref:'BRICKS_LICENSE_KEY',authorized_assets:[{sha256:hash,usage_status:'approved'}],revision_receipt:base??empty,rollback_receipt:{required:true,...(target??base??empty)}}} +} +test('native authority binds exact Build pointer and restore target',()=>{ + for(const action of ['provision','revise','restore'] as const)assert.doesNotThrow(()=>validateNativeMutation(input(action),action,'operation_1')) + const changed=input('revise');changed.targetRequest.revision_receipt={...pointer,native_document_hashes:['b'.repeat(64)]};assert.throws(()=>validateNativeMutation(changed,'revise','operation_1'),/exact authority/) + const restore=input('restore');restore.targetRequest.rollback_receipt={required:true,...pointer};assert.throws(()=>validateNativeMutation(restore,'restore','operation_1'),/exact authority/) +}) +test('native admission rejects foreign staging keys, non-approved assets and mismatched identities',()=>{ + const wrong=input('provision');wrong.artifact!.r2Key='sites/another-site/private.json';assert.throws(()=>validateNativeMutation(wrong,'provision','operation_1'),/staged artifact/) + const unsafe=input('provision');unsafe.targetRequest.authorized_assets[0].usage_status='pending';assert.throws(()=>validateNativeMutation(unsafe,'provision','operation_1'),/agency-approved/) + const mismatch=input('provision');mismatch.targetRequest.site_id='another-site';assert.throws(()=>validateNativeMutation(mismatch,'provision','operation_1'),/identity mismatch/) +}) diff --git a/packages/runtime-cloudflare/tests/cloudflare-native-bricks-artifact.test.ts b/packages/runtime-cloudflare/tests/cloudflare-native-bricks-artifact.test.ts new file mode 100644 index 00000000..676f91f8 --- /dev/null +++ b/packages/runtime-cloudflare/tests/cloudflare-native-bricks-artifact.test.ts @@ -0,0 +1,73 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { validateNativeBricksArtifact, stableJson, sha256Hex, MAX_NATIVE_BRICKS_ARTIFACT_BYTES } from "../src/native-bricks-artifact.js" + +const hash = "a".repeat(64) +const encoder = new TextEncoder() +const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a5e8AAAAASUVORK5CYII=" +const hashed = async (value: any) => ({ ...value, sha256: await sha256Hex(stableJson(value)) }) +const bytes = (value: unknown) => encoder.encode(stableJson(value)) +async function fixture(): Promise { + const imageBytes = Uint8Array.from(atob(png), c => c.charCodeAt(0)) + const imageHash = await sha256Hex(imageBytes) + return { + schema: "wp-codebox/bricks-site-artifact/v1", + site: { site_id: "native-fixture", customer_id: "fixture-customer", title: "Fixture", starter_id: "fixture" }, + runtime: { version: "2.4-rc", sha256: hash }, + evidence_inputs: { dossier: { revision: 1, sha256: hash }, creative_provenance: { version: "v1", canonicalCommit: "b".repeat(40), guideSha256: hash, catalogSha256: hash, references: [{ id: "reference-one", sha256: hash }] }, authorized_asset_hashes: [imageHash] }, + design_system: { version: "v1", palette: [{ id: "primary", name: "Primary", value: "#123456" }], typography: { body_font_family: "sans-serif", heading_font_family: "serif", root_font_size: "16px" }, global_classes: [await hashed({ id: "content", name: "content", settings: {} })], global_variables: [await hashed({ id: "color", name: "color", type: "color", value: "#123456" })], theme_style: await hashed({ settings: {} }) }, + documents: { pages: [await hashed({ logical_id: "home", title: "Home", slug: "home", status: "draft", elements: [{ id: "image1", name: "image", settings: { image: { asset_ref: "logo", size: "full" } }, children: [] }] })], templates: [await hashed({ logical_id: "header", title: "Header", type: "header", status: "draft", conditions: [], elements: [{ id: "head1", name: "heading", settings: { text: "Fixture" }, children: [] }] })] }, + assets: [{ logical_id: "logo", filename: "logo.png", mime_type: "image/png", alt_text: "Fixture", content_base64: png, bytes: imageBytes.length, sha256: imageHash }], + editing: { granularity: "native_bricks_elements", target_role: { role_slug: "editor", required_capabilities: ["edit_pages"] }, acceptance_sequence: ["edit", "publish", "public_observation", "restore", "reopen"] }, + } +} + +test("canonical native artifact preserves authored payload and independent runtime identity", async () => { + const value = await fixture() + assert.deepEqual(await validateNativeBricksArtifact(bytes(value)), value) + assert.notEqual(await sha256Hex(bytes(value)), value.runtime.sha256) + assert.equal(stableJson({ z: 1, a: { d: 3, b: 2 } }), '{"a":{"b":2,"d":3},"z":1}') + value.evidence_inputs.creative_provenance.references = [] + assert.deepEqual((await validateNativeBricksArtifact(bytes(value))).evidence_inputs.creative_provenance.references, []) + delete value.evidence_inputs.creative_provenance.guideSha256 + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /expected fields/) +}) + +test("rejects noncanonical, oversized, unknown-field and deeply nested envelopes before execution", async () => { + const value = await fixture() + await assert.rejects(validateNativeBricksArtifact(encoder.encode(JSON.stringify(value, null, 2))), /noncanonical/) + await assert.rejects(validateNativeBricksArtifact(new Uint8Array(MAX_NATIVE_BRICKS_ARTIFACT_BYTES + 1)), /byte limit/) + await assert.rejects(validateNativeBricksArtifact(bytes({ ...value, extra: true })), /expected fields/) + let nested: any = {}; for (let i = 0; i < 90; i++) nested = { child: nested } + value.documents.pages[0].elements[0].settings.extra = nested + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /complexity/) +}) + +test("rejects changed document, design and media bytes and unbound asset authority", async () => { + for (const mutate of [ + (v: any) => { v.documents.pages[0].title = "Altered" }, + (v: any) => { v.design_system.theme_style.settings.color = "red" }, + (v: any) => { v.assets[0].sha256 = hash }, + (v: any) => { v.evidence_inputs.authorized_asset_hashes = [hash] }, + ]) { const value = await fixture(); mutate(value); await assert.rejects(validateNativeBricksArtifact(bytes(value)), /hash mismatch|authority mismatch/) } +}) + +test("rejects unresolved, raw-ID and unused media without substituting foreign assets", async () => { + let value = await fixture(); value.documents.pages[0].elements[0].settings.image.asset_ref = "missing" + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /unknown asset_ref/) + value = await fixture(); value.documents.pages[0].elements[0].settings.image.id = 99 + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /expected fields/) + value = await fixture(); const page = value.documents.pages[0]; delete page.sha256; page.elements[0] = { id: "text1", name: "heading", settings: { text: "Hello" }, children: [] }; value.documents.pages[0] = await hashed(page) + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /unused staged asset/) +}) + +test("rejects duplicate native IDs, unsupported code elements and invalid media files", async () => { + let value = await fixture(); value.documents.templates[0].elements[0].id = "image1" + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /duplicate element ID/) + value = await fixture(); value.documents.pages[0].elements[0].name = "code" + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /enum/) + value = await fixture(); value.assets[0].filename = "../logo.png" + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /filename/) + value = await fixture(); value.assets[0].mime_type = "image/jpeg" + await assert.rejects(validateNativeBricksArtifact(bytes(value)), /MIME/) +}) diff --git a/packages/runtime-cloudflare/tests/cloudflare-runtime.test.ts b/packages/runtime-cloudflare/tests/cloudflare-runtime.test.ts index aacf26c3..a9472dbe 100644 --- a/packages/runtime-cloudflare/tests/cloudflare-runtime.test.ts +++ b/packages/runtime-cloudflare/tests/cloudflare-runtime.test.ts @@ -496,10 +496,14 @@ test("Cloudflare canonical runtime patches the unique init call with runtime per assert.match(patcher, /function patchCanonicalThemeJsonCustomCss/) assert.match(patcher, /if \( array\( 'custom-css' \) === \$types \)/) assert.match(patcher, /return \(string\) _wp_array_get\( \$this->theme_json, array\( 'styles', 'css' \), '' \)/) - assert.doesNotMatch(patcher, /mu-plugins|add_action\(|WP_Site_Health|wp_schedule_site_health_cron|\$GLOBALS\['wp_filter'\]/) + assert.match(patcher, /WP_Site_Health::get_instance\(\)/) + assert.match(patcher, /\$GLOBALS\['wp_filter'\]\['init'\]/) + assert.match(patcher, /register_block_core_/) + assert.match(patcher, /Bricks\\\\Database/) + assert.doesNotMatch(patcher, /mu-plugins|wp_schedule_site_health_cron/) assert.doesNotMatch(worker, /materializeCanonicalCronAdapter|wp-codebox-canonical-cron-policy/) assert.match(worker, /runtimeBucket\?: R2Bucket,\n shouldPatchCanonicalRuntimePoliciesAtInit = false,\n uploadFiles\?: RuntimeFile\[\],\n wpContentFiles\?: RuntimeFile\[\],/) - assert.match(worker, /authConstants, bucket, true, revision\.uploads, revision\.wpContent, revision\.wpContentDeleted, includeWebsiteImporter, trace\)/) + assert.match(worker, /authConstants, bucket, true, revision\.uploads, revision\.wpContent, revision\.wpContentDeleted, includeWebsiteImporter, trace, revision\.wpContentR2OnlyPaths, undefined, nativeRuntime\)/) assert.match(worker, /env\.WORDPRESS_STATE_BUCKET, true, undefined, undefined, \[\], false, trace\)/) assert.match(worker, /canonicalBootstrapPasswordCode/) assert.match(worker, /canonicalBootstrapUrlCode/) @@ -640,7 +644,7 @@ test("Cloudflare runtime injects composable coordinators without moving PHP out assert.match(worker, /collectUploadFiles\(runtime\.php\)/) assert.match(worker, /hash_file\('sha256', \$path\)/) assert.match(worker, /persistRuntimeObjects\(bucket, runtime\.php, UPLOADS_ROOT/) - assert.match(worker, /materializeRuntimeFiles\(php, UPLOADS_ROOT, uploadFiles\)/) + assert.match(worker, /materializeRuntimeFiles\(php, UPLOADS_ROOT, uploadHydrationFiles\)/) assert.match(worker, /"x-wp-codebox-static": "r2-upload"/) assert.match(worker, /collectWpContentFiles\(runtime\.php\)/) assert.match(worker, /wordpressWpContentBaselineHashes\.get\(file\.path\) !== file\.sha256/) diff --git a/packages/runtime-cloudflare/wrangler.bricks-canary.jsonc b/packages/runtime-cloudflare/wrangler.bricks-canary.jsonc new file mode 100644 index 00000000..7d32b9ad --- /dev/null +++ b/packages/runtime-cloudflare/wrangler.bricks-canary.jsonc @@ -0,0 +1,47 @@ +// Isolated native Bricks 2.4 RC canary. No production route or customer hostname. +{ + "name": "bricks24-native-canary-20260910", + "main": "src/worker-d1.ts", + "compatibility_date": "2026-07-18", + "compatibility_flags": ["nodejs_compat"], + "workers_dev": true, + "limits": { "cpu_ms": 300000 }, + "observability": { + "enabled": true, + "logs": { "invocation_logs": true, "head_sampling_rate": 1 } + }, + "triggers": { "crons": [] }, + "vars": { + "WORDPRESS_SITE_CONTEXTS": "[{\"id\":\"default\",\"hostname\":\"bricks24-native-canary-20260910.mavnewlife.workers.dev\",\"origin\":\"https://bricks24-native-canary-20260910.mavnewlife.workers.dev\"}]", + "WORDPRESS_PHASE_PROGRESS_LOGS": "1" + }, + "queues": { + "producers": [ + { "binding": "WORDPRESS_RUNTIME_QUEUE", "queue": "bricks24-native-canary-20260910" } + ], + "consumers": [ + { + "queue": "bricks24-native-canary-20260910", + "max_batch_size": 10, + "max_batch_timeout": 5, + "max_retries": 3, + "max_concurrency": 1, + "dead_letter_queue": "bricks24-native-canary-20260910-dlq" + } + ] + }, + "r2_buckets": [ + { "binding": "WORDPRESS_STATE_BUCKET", "bucket_name": "bricks24-native-canary-20260910" } + ], + "d1_databases": [ + { + "binding": "WORDPRESS_STATE_DATABASE", + "database_name": "bricks24-native-canary-20260910", + "database_id": "fe4d6486-6cd6-4a2e-9542-4f2dd8af7d8a" + } + ], + "rules": [ + { "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false }, + { "type": "Data", "globs": ["**/*.sqlite", "**/*-runtime.zip", "**/*-canonical-seed.zip"], "fallthrough": false } + ] +} diff --git a/scripts/verify-cloudflare-sqlite-delete-alias.ts b/scripts/verify-cloudflare-sqlite-delete-alias.ts new file mode 100644 index 00000000..3ec78a81 --- /dev/null +++ b/scripts/verify-cloudflare-sqlite-delete-alias.ts @@ -0,0 +1,41 @@ +// Run with the repository development dependencies: npx tsx scripts/verify-cloudflare-sqlite-delete-alias.ts +// Uses the generated pinned SQLite archive and PHP-WASM; no native php executable required. +import { readFile } from 'node:fs/promises'; +import { PHP } from '@php-wasm/universal'; +import { loadNodeRuntime } from '@php-wasm/node'; +import { decodeZip } from '@php-wasm/stream-compression'; +import { patchSqliteDeleteAlias } from '../packages/runtime-cloudflare/src/sqlite-delete-alias-compatibility.js'; +import assert from 'node:assert/strict'; +const zip=await readFile(new URL('../packages/runtime-cloudflare/artifacts/cloudflare-sqlite-database-integration.zip', import.meta.url)); +const php = new PHP(await loadNodeRuntime('8.4', {emscriptenOptions:{processId:process.pid}})); +let driverPath='';let original=''; +for await(const entry of decodeZip(new Blob([zip]).stream())){ + if(entry.name.endsWith('/'))continue; + const p='/tmp/sqlite-alias-proof/'+entry.name; + php.mkdir(p.slice(0,p.lastIndexOf('/'))); + const data=new Uint8Array(await entry.arrayBuffer()); + php.writeFile(p,data); + if(p.endsWith('class-wp-pdo-mysql-on-sqlite.php')) {driverPath=p;original=new TextDecoder().decode(data);} +} +const code=String.raw`exec('CREATE TABLE wp_options (option_id bigint unsigned NOT NULL AUTO_INCREMENT, option_name varchar(191) NOT NULL, option_value longtext NOT NULL, PRIMARY KEY(option_id), UNIQUE KEY option_name(option_name))'); +$db->exec("INSERT INTO wp_options(option_name,option_value) VALUES ('target','before'),('design_version','v1'),('untouched','keep')"); +$prefix="DELETE target FROM wp_options AS target LEFT JOIN wp_options AS design_version ON design_version.option_name='design_version' WHERE target.option_name='target' AND BINARY target.option_value=BINARY 'before' AND BINARY design_version.option_value=BINARY "; +try { + $stale=$db->exec($prefix."'stale'"); + $before=$db->query("SELECT option_value FROM wp_options WHERE option_name='target'")->fetchColumn(); + $deleted=$db->exec($prefix."'v1'"); + $remaining=$db->query('SELECT option_name,option_value FROM wp_options ORDER BY option_name')->fetchAll(PDO::FETCH_ASSOC); + echo json_encode(compact('stale','before','deleted','remaining')); +} catch(Throwable $e) {echo json_encode(['error'=>$e->getMessage()]);} +`; +try{ + const before=await php.run({code}); const observed=JSON.parse(before.text); assert.match(observed.error,/target/); console.log(JSON.stringify({unpatched:observed})); + const patched=patchSqliteDeleteAlias(original);assert.equal(patchSqliteDeleteAlias(patched),patched);php.writeFile(driverPath,patched); + const after=await php.run({code});const result=JSON.parse(after.text); + assert.equal(result.stale,0);assert.equal(result.before,'before');assert.equal(result.deleted,1);assert.deepEqual(result.remaining,[{option_name:'design_version',option_value:'v1'},{option_name:'untouched',option_value:'keep'}]); + console.log(JSON.stringify({patched:result,status:'actual-sqlite-compare-and-delete-passed'})); +}finally{php.exit();}