From e9c16259b23c0da6143d55dab5c33c579ffddf49 Mon Sep 17 00:00:00 2001 From: DevBot Date: Tue, 28 Jul 2026 02:50:32 +0800 Subject: [PATCH 1/5] feat(app,adapter-vite,element,create,docs): 0.42.0-alpha.5 audit round 1 remediation (TP-5.5) Protocol hardening per ADR-0121: hasAction covers named actions, own-key named-action dispatch, unified x-openelement-action header (true/enhance) with Vary, Response-return contract violation, PRG action-marker strip, 3xx-to-303 coercion on POST + redirect() status validation, fetch-channel 404/500 JSON symmetry with PROD scrub, no-store default on request-time responses, POST error-boundary parity, bodyLimit on action POSTs, defined 405 for non-GET/POST, renderIntent.mode validation. Morph client rewrite (alpha.3 enhancement never fired: submit is not composed and page content lives in page-element DSD): per-shadow-root submit interception, shadow-content morph, id-keyed + bounded-lookahead matching, form-scoped regions with navigation fallback, popstate reload, open:action-failure restored, submitter-preserving body, 500/cross-origin navigation fallback, double-submit guard, fragment preservation, state-mirroring attribute skip list. Evidence and gates: fixture three-engine e2e in ci+release gate tiers, static-output freeze tool (release tier), dev/build parity contract test, dev SSR customElements polyfill (dev SSR 500 fix), perf baseline with environment fields, alpha.3/alpha.4 release-note errata (false survival claims), MORPH_CONTRACT survival matrix doc. Manifest and surface: self-contained matchRequestTimeRoute host contract, [...path] wildcard fix, /item/[id] fixture, zero-island enhancement with conditional enhancement layer (lean bundles for form-less sites), Spa* loader/action type split, PACKAGE_SURFACE completion, starter contact.tsx shipping fix + Nitro-node POST smoke, CSRF threat-model guide page, dev customElements polyfill shared with the build banner. Refs #539-#573 (fix evidence posted per issue). --- .github/workflows/autoflow-ci.yml | 8 +- .github/workflows/autoflow-release.yml | 5 +- deno.json | 2 + ...-42-action-protocol-hardening-amendment.md | 146 +++++++ docs/adr/README.md | 2 + docs/current/MORPH_CONTRACT.md | 97 +++++ docs/current/PACKAGE_SURFACE.md | 19 +- docs/current/VERSION_PLAN.md | 89 ++++- docs/integrations/validation.md | 9 +- docs/release/v0.41.0-interface-snapshot.json | 4 +- docs/release/v0.42.0-alpha.3.md | 14 + docs/release/v0.42.0-alpha.4.md | 19 + docs/release/v0.42.0-alpha.5-performance.json | 31 ++ .../request-time/app/routes/boom.tsx | 53 +++ .../request-time/app/routes/item/[id].tsx | 65 ++++ .../request-time/app/routes/items.tsx | 44 +++ .../request-time/app/routes/ping.tsx | 66 ++++ .../request-time/app/routes/regions.tsx | 72 ++++ .../request-time/e2e/live.spec.ts | 295 +++++++++++++- .../__fixtures__/request-time/e2e/server.ts | 20 +- .../__tests__/entry-generators.test.ts | 47 ++- .../__tests__/entry-renderer.test.ts | 48 ++- .../__tests__/request-time-parity.test.ts | 172 +++++++++ .../__tests__/ssg-helpers.test.ts | 17 +- .../adapter-vite/__tests__/ssg-render.test.ts | 46 +++ packages/adapter-vite/src/build.ts | 9 +- packages/adapter-vite/src/cli/build-client.ts | 15 +- packages/adapter-vite/src/cli/build-ssg.ts | 17 +- .../src/internal/ssg/entry-descriptor.ts | 2 + .../src/internal/ssg/entry-generators.ts | 365 ++++++++++++++---- .../src/internal/ssg/entry-render-helpers.ts | 119 ++++-- .../src/internal/ssg/entry-render-ssg.ts | 2 +- .../adapter-vite/src/internal/ssg/index.ts | 2 +- .../src/internal/ssg/route-scanner.ts | 15 +- .../src/internal/ssg/ssg-helpers.ts | 81 +++- .../src/internal/ssg/ssg-render.ts | 8 +- .../src/internal/ssg/ssr-polyfills.ts | 22 ++ packages/adapter-vite/src/plugin.ts | 20 +- packages/app/src/authoring.ts | 21 +- packages/app/src/index.ts | 4 + .../app/src/internal/router/client-router.ts | 10 +- packages/create/__tests__/cli.test.ts | 1 + packages/create/src/template-builder.ts | 1 + packages/create/templates/deno.json.tmpl | 2 +- packages/element/src/index.ts | 4 + packages/element/src/internal/core/index.ts | 4 + .../element/src/internal/protocol/data.ts | 42 +- .../src/internal/protocol/framework.ts | 6 + tools/autoflow/policy.ts | 22 ++ tools/check-static-output-freeze.ts | 266 +++++++++++++ tools/consumer-local.ts | 53 ++- tools/perf-request-time.ts | 126 ++++-- www/app/data/_generated-nav.ts | 5 + www/app/routes/guide/routing-and-data.tsx | 14 + www/app/routes/guide/security.tsx | 178 +++++++++ www/public/search-index.json | 6 + 56 files changed, 2616 insertions(+), 216 deletions(-) create mode 100644 docs/adr/ADR-0121-0-42-action-protocol-hardening-amendment.md create mode 100644 docs/current/MORPH_CONTRACT.md create mode 100644 docs/release/v0.42.0-alpha.5-performance.json create mode 100644 packages/adapter-vite/__fixtures__/request-time/app/routes/boom.tsx create mode 100644 packages/adapter-vite/__fixtures__/request-time/app/routes/item/[id].tsx create mode 100644 packages/adapter-vite/__fixtures__/request-time/app/routes/items.tsx create mode 100644 packages/adapter-vite/__fixtures__/request-time/app/routes/ping.tsx create mode 100644 packages/adapter-vite/__fixtures__/request-time/app/routes/regions.tsx create mode 100644 packages/adapter-vite/__tests__/request-time-parity.test.ts create mode 100644 tools/check-static-output-freeze.ts create mode 100644 www/app/routes/guide/security.tsx diff --git a/.github/workflows/autoflow-ci.yml b/.github/workflows/autoflow-ci.yml index 29d1a3cb7..38dd3b69f 100644 --- a/.github/workflows/autoflow-ci.yml +++ b/.github/workflows/autoflow-ci.yml @@ -38,17 +38,15 @@ jobs: fetch-depth: 0 - uses: ./.github/actions/setup-deno-workspace - name: Install Playwright browsers - run: ./node_modules/.bin/playwright install --with-deps chromium + # All three engines up front: the gate's fixture:request-time:gate + # runs the request-time fixture suite on Chromium, Firefox and WebKit. + run: ./node_modules/.bin/playwright install --with-deps chromium firefox webkit - name: AutoFlow3 CI gate run: deno task autoflow:ci # Cross-browser smoke: the main gate is chromium-only. Run the core # DSD/island-hydration/theme specs on Firefox and WebKit against the # www/dist the gate's build step already produced. - - name: Install Playwright Firefox - run: ./node_modules/.bin/playwright install --with-deps firefox - name: Firefox smoke E2E run: deno task test:e2e:firefox-smoke - - name: Install Playwright WebKit - run: ./node_modules/.bin/playwright install --with-deps webkit - name: WebKit smoke E2E run: deno task test:e2e:webkit-smoke diff --git a/.github/workflows/autoflow-release.yml b/.github/workflows/autoflow-release.yml index f8ee06bab..c4172ff75 100644 --- a/.github/workflows/autoflow-release.yml +++ b/.github/workflows/autoflow-release.yml @@ -38,7 +38,10 @@ jobs: node-version: '22' registry-url: 'https://registry.npmjs.org' - name: Install Playwright browsers - run: ./node_modules/.bin/playwright install --with-deps chromium + # All three engines: the release tier includes fixture:request-time:gate, + # which runs the request-time fixture suite on Chromium, Firefox and + # WebKit (release evidence requirement). + run: ./node_modules/.bin/playwright install --with-deps chromium firefox webkit - name: Configure Git run: | git config user.name "github-actions[bot]" diff --git a/deno.json b/deno.json index 96b67458e..5c7cb9d30 100644 --- a/deno.json +++ b/deno.json @@ -91,6 +91,7 @@ "fixture:request-time:build": "cd packages/adapter-vite/__fixtures__/request-time && deno run --config ../../../../deno.json --allow-read --allow-write --allow-net --allow-env --allow-ffi --allow-sys --allow-run ../../src/cli/build.ts", "fixture:request-time:e2e": "deno run -A npm:@playwright/test@1.59.1 test --config packages/adapter-vite/__fixtures__/request-time/e2e/playwright.config.ts --project=chromium", "fixture:request-time:e2e:browsers": "deno run -A npm:@playwright/test@1.59.1 test --config packages/adapter-vite/__fixtures__/request-time/e2e/playwright.config.ts", + "fixture:request-time:gate": "deno task fixture:request-time:build && deno task fixture:request-time:e2e:browsers", "nitro:proof:node": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/nitro-proof.ts node", "nitro:proof:workers": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/nitro-proof.ts workers", "test:coverage:check": "deno run --allow-read --allow-run --allow-write --allow-env --allow-net --allow-ffi --allow-sys tools/check-coverage.ts --threshold 73 --branch-threshold 82 --function-threshold 77", @@ -112,6 +113,7 @@ "autoflow:release-prepare": "deno run --allow-read --allow-write --allow-run --allow-env tools/autoflow/mod3.ts release-prepare", "autoflow:publish-existing": "deno run --allow-read --allow-write --allow-run --allow-env tools/autoflow/mod3.ts publish-existing", "autoflow:test": "deno test --allow-read --allow-write --allow-run --allow-env tools/autoflow/__tests__/", + "check:static-output-freeze": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net --allow-ffi --allow-sys tools/check-static-output-freeze.ts", "verify:configs": "deno run --allow-read --allow-env tools/verify-package-configs.ts", "verify:configs:fix": "deno run --allow-read --allow-write --allow-env tools/verify-package-configs.ts --fix" }, diff --git a/docs/adr/ADR-0121-0-42-action-protocol-hardening-amendment.md b/docs/adr/ADR-0121-0-42-action-protocol-hardening-amendment.md new file mode 100644 index 000000000..571c2f87b --- /dev/null +++ b/docs/adr/ADR-0121-0-42-action-protocol-hardening-amendment.md @@ -0,0 +1,146 @@ +# ADR-0121: 0.42 Action Protocol Hardening Amendment (Audit Round 1) + +- Status: PROPOSED — implementation proceeds under this amendment in + `0.42.0-alpha.5`; maintainer acceptance to be recorded at the alpha.5 + release +- Date: 2026-07-27 +- Amends: ADR-0120 (action protocol rules 2, 3, 4, 6, 7) + +## Context + +The first implementation audit round of the 0.42 WC Application Loop +(GitHub issues #539–#573, milestone `v0.42.0-alpha.5`) found the ADR-0120 +protocol skeleton faithfully implemented in its main lines, but with +specific holes, bypasses and undocumented behaviors that are +protocol-visible. ADR-0120 requires an amendment for protocol changes; +this amendment records all of the protocol-visible decisions in one batch +so the protocol document stays coherent. Implementation bugs with no +protocol visibility (dead branches, escaping, codegen details) are fixed +directly under the issues and are not recorded here. + +The round's deepest finding concerns rule 6 itself: the alpha.3 morph +enhancement never functioned. Page content lives inside page-element DSD +shadow roots, the `submit` event is not composed, and the document-level +submit listener therefore never fired — the published survival-matrix +results were silently measuring the native no-JS path, and the two +survival specs fail deterministically at both the alpha.3 and alpha.4 +tags. Items 8–11 are written against the rewritten client that fixes +this at the architecture level. + +## Decision + +1. **Request-header negotiation (amends rule 2).** One framework request + header, `x-openelement-action`, with two values: `true` marks a + programmatic caller and selects the serialized `ActionResult` union; + `enhance` marks the built-in morph enhancement and selects the same + full-HTML responses the no-JS path receives. The alpha.2–alpha.4 + `x-openelement-enhance` header (never consumed by any server code) is + retired. The POST endpoint answers `Vary: x-openelement-action` so the + two response classes never share a cache entry. +2. **Response-return policy (amends rule 3).** Actions must not return + `Response` objects. The return channel is data or `fail(status, data)`; + the redirect channel is `redirect()`; everything else is a thrown + error. A returned `Response` is a contract violation and answers 500 + (clear message in dev, generic in production). This makes "a + successful mutation never answers 200 with a rendered page" a + mechanical property instead of a convention. +3. **Redirect algebra (amends rule 3).** `redirect()` validates at + construction that the status is one of 301/302/303/307/308 and throws + otherwise. In the POST action context every 3xx is coerced to 303 + (post/redirect/get must be method-safe and non-cacheable); in the + GET/loader context the author's status is kept (permanent moves remain + legitimate there). The `ActionResult` redirect variant is answered + with HTTP 200 carrying `{ type: 'redirect', status: 303, location }` — + it is a data message for the client, not an HTTP redirect (an HTTP 3xx + would be transparently followed by `fetch` and the JSON body would be + unreadable). +4. **Default PRG target (amends rule 3).** The default PRG location is + the route pathname plus the query string with `/`-prefixed + action-marker keys removed; all other query parameters are preserved. + The action marker never reaches the address bar, history or referer. +5. **Fetch-channel symmetry (amends rule 2).** On the JSON channel every + protocol outcome is an `ActionResult`: an unknown named action and a + POST to a route without actions both answer + `{ type: 'error', status: 404, error: { message } }` with HTTP 404, + with distinct messages for "route does not accept submissions" and + "no action named X". The HTML channel keeps the framework 404 page. +6. **Caching (implements the rule-7 no-cache promise).** Every + request-time response kind (200/422/303/404/500) carries + `Cache-Control: no-store`; the POST endpoint additionally carries + `Vary: x-openelement-action` per item 1. +7. **Error-channel parity (amends rule 4).** Thrown values on POST take + the same nearest-error-boundary channel as GET: the page's `error` + component renders with status 500, with the bare 500 page as fallback. + The audit's accepted window is recorded: an action whose mutation + commits and whose revalidation then fails (loader throw on the + follow-up GET, or a mutate-then-`fail()` contract violation) leaves + the mutation committed while the user sees an error page. "Validate + first, mutate after" remains the action contract; the guide documents + the window. +8. **Region semantics (amends rule 6).** A form targets a named region + via `data-open-region-target="name"` (on the submitter or the form, + submitter wins); absent that attribute the morph targets the nearest + ancestor `[data-open-region]` of the form; absent any region the body + morphs. If the target region is missing on either side the client + navigates to the response URL (never a silent full-body morph). + Non-targeted regions are never touched. +9. **Morph architecture and identity (amends rule 6).** Page content + lives inside the page element's DSD shadow root, so the morph descends + into shadow trees: a non-island host with a live shadow root and an + incoming `