Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/autoflow-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion .github/workflows/autoflow-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, `<details>`/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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 命名不再是当前版本线。

## 当前产品
Expand Down
2 changes: 2 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
Expand Down
146 changes: 146 additions & 0 deletions docs/adr/ADR-0121-0-42-action-protocol-hardening-amendment.md
Original file line number Diff line number Diff line change
@@ -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 `<template shadowrootmode>` child morphs its shadow root
against the template content. Submits are intercepted at every shadow
root (the submit event is not reliably composed across engines), with
the document-level listener covering light-DOM forms. Within a parent,
children carrying an `id` attribute match by `id` (tag must also
match); the remaining children match structurally (node type + tag)
with a bounded lookahead, so insertions and removals between matched
anchors are preserved instead of cascading into wholesale replacement.
Hydrated-island survival still requires the light-DOM surface
comparison (`__islandIntact`). Rows rendered in dynamic lists should
carry stable `id`s to survive reorders.
10. **Popstate (amends rule 6).** The enhanced client reloads the
document on `popstate`. There is no client-side state cache in 0.42;
back/forward therefore never shows content that disagrees with the
URL.
11. **Action-failure event (restores the alpha.2 hook; amends rule 6).**
On a 422 response the enhanced client dispatches a cancelable
`open:action-failure` `CustomEvent` on the form with
`detail: { status, form, response }` before morphing; calling
`preventDefault()` skips the default morph (the page handles the
failure itself). Network and non-HTML failures keep the reload
fallback.
12. **CSRF threat model (new; records the standing assumption).** The
framework's form/action loop relies on the browser `SameSite=Lax`
default for cookie-based authentication (sessions are 0.44 scope).
Applications using ambient authentication (Basic, mTLS,
`SameSite=None` cookies) must add Origin/Fetch-Metadata validation;
a documented middleware recipe ships in the guide. A built-in check
is deferred to the 0.44 session work.

## Consequences

Positive:

- Every protocol outcome is deterministic and mechanically testable on
both channels; the two channels can no longer diverge silently.
- The header negotiation becomes honest: one header, documented values,
cache-safe via `Vary`.
- The audit's high-severity protocol holes (#539–#542, #544, #547–#549)
become contract violations with clear errors instead of silent
misbehavior.

Negative:

- `0.42.0-alpha.2`–`alpha.4` clients sending `x-openelement-enhance`
are tolerated (treated as the HTML path) but the header no longer
appears in the shipped client; the wire was never consumed, so no
migration is needed.
- Forbidding returned `Response` objects removes an escape hatch; users
needing full response control use API routes (Hono) instead of page
actions.

Neutral:

- Items 6, 7 and 12 record or implement promises ADR-0120 already made;
the rest are protocol amendments proper. The fixture e2e and the
codegen contract tests are the mechanical gate for every item.
2 changes: 2 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ documents:
| Alpha.16 scope | ADR-0116 | Audit-driven correctness reset before stable freeze. |
| Stable 0.41 freeze | ADR-0119 | Scoped interface freeze for the 0.41.x line. |
| 0.42 loop scope | ADR-0120 | WC Application Loop scope boundary and action protocol. |
| 0.42 protocol fix | ADR-0121 | Audit round 1 hardening amendment to the ADR-0120 protocol. |
| Cleanup governance | ADR-0105, ADR-0106 | Approved cleanup train and audit-driven quality work. |
| Active stack truth | `docs/current/STACK_CONTRACT.md`, `docs/current/VERSION_PLAN.md` | Operational version and stack contract for current work. |

Expand Down Expand Up @@ -136,6 +137,7 @@ decision set above to decide which records are authoritative for new work.
| 0118 | Third Audit Round Alpha.19 Cleanup Sweep | Accepted |
| 0119 | Stable 0.41.0 Scoped Interface Freeze | Accepted |
| 0120 | 0.42.0 WC Application Loop Scope and Action Protocol | Accepted |
| 0121 | 0.42 Action Protocol Hardening Amendment (Audit Round 1) | Proposed |

## Superseded / Historical

Expand Down
Loading
Loading