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/CHANGELOG.md b/CHANGELOG.md index 0213edcd3..9613312dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,50 @@ Current truth lives in: Historical changelog details remain available through git history and release evidence. +## 0.42.0-alpha.5 + +- Audit round 1 remediation (TP-5.5, ADR-0121): the first implementation + audit of the 0.42 line (#539–#573) is closed — protocol hardening, a + morph client rewrite, security hygiene and an evidence-honesty sweep. +- Headline root cause: the alpha.3 morph enhancement never fired — the + `submit` event is not composed and page content lives inside + page-element DSD shadow roots, so the document-level listener never saw + enhanced forms. The client is rewritten around shadow-root submit + interception and shadow-content morphing; the island-survival claims + are now mechanically true (errata appended to the alpha.3/alpha.4 + notes). +- Protocol (ADR-0121): the prerender hard rule covers named `actions`; + named-action dispatch is own-key gated; one `x-openelement-action` + header (`true` = ActionResult JSON, `enhance` = HTML morph) with + `Vary`; an action returning a `Response` is a contract violation; the + default PRG strips the `?/name` marker; every 3xx coerces to 303 on + POST and `redirect()` validates its status; fetch callers always + receive ActionResult JSON (404/500 included, production-scrubbed); + request-time responses carry `Cache-Control: no-store`; POST takes the + same error boundary as GET; action POSTs get a 10 MB body limit; + non-GET/POST methods answer 405. +- Morph continuity: form-scoped `data-open-region` targeting with + navigation fallback, id-keyed + lookahead identity matching, popstate + reload, `open:action-failure` restored (cancelable), submitter + name/value preserved in the enhanced body, 500/cross-origin responses + navigate instead of morphing, double-submit guard, fragment + preservation, `
`/media state protection. The full survival + matrix is documented in `docs/current/MORPH_CONTRACT.md`. +- Evidence: the request-time fixture suite (42 tests) runs on Chromium, + Firefox and WebKit in the ci and release gate tiers; a static-output + determinism gate ships (`check:static-output-freeze`); a dev(hono) vs + build(Nitro) parity contract test boots both real servers; the perf + baseline records its environment + (`docs/release/v0.42.0-alpha.5-performance.json`). +- Also fixed along the way: dev SSR crashed on every route (missing + `customElements` polyfill — now shipped to the dev entry); + `[...path]` request-time routes; zero-island apps with enhanced forms + (the enhancement layer is emitted only when enhanced forms exist, so + island-only sites keep their lean bundle); the starter's `/contact` + route (it was never shipped in `TEMPLATE_FILES`) now builds and is + POST-smoked in consumer CI; SPA vs request-time loader/action types + and docs are honest; a CSRF threat-model page ships in the guide. + ## 0.42.0-alpha.4 - Hardening and recipes (TP-5, ADR-0120): the 0.42 line closes with diff --git a/README.md b/README.md index dcd1d92d4..92a438dff 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Elements are the durable application contract; JSX and Basic Element are the authoring layer; Declarative Shadow DOM is the default server representation; interactive regions upgrade selectively. -Published package line: `0.42.0-alpha.4` (`v0.42.0-alpha.4`) — the stable five-package +Published package line: `0.42.0-alpha.5` (`v0.42.0-alpha.5`) — the stable five-package release under ADR-0119's scoped interface freeze; the abandoned beta naming is not an active line. diff --git a/README.zh.md b/README.zh.md index 7f4e9636e..053739b84 100644 --- a/README.zh.md +++ b/README.zh.md @@ -6,7 +6,7 @@ Elements 是可长期保存的应用组件模型;JSX 与 Basic Element 是作者层; Declarative Shadow DOM 是默认服务端表示;交互区域按需升级。 -已发布包线为 `0.42.0-alpha.4`(`v0.42.0-alpha.4`)——ADR-0119 范围化接口冻结下的 +已发布包线为 `0.42.0-alpha.5`(`v0.42.0-alpha.5`)——ADR-0119 范围化接口冻结下的 stable 五包版本;已放弃的 beta 命名不再是当前版本线。 ## 当前产品 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 `