Repository navigation
feat(compiler): intrinsic event/ref channels on the generated leaf (M09-2c) - #1788
Merged
Merged
Conversation
…eaf.
Bounded compiler-private codegen capability (not full authored CE parity): the
authored JSX keeps the existing onClick/ref attributes on INTRINSIC elements;
no new callback API, runtime module or package export is introduced.
- frontend: event and ref dynamic attributes are lowered in handler
(attach-once) mode, so their reads are recorded once without reactive
dependencies (a ref value is evaluated at attach, not treated as a tracked
slot).
- admission: the intrinsic channel set gains event/ref while the
component-reference prop set does not (a child callback/ref prop stays a
second-API refusal), and custom-element hosts and list entry templates fail
closed (host ownership / the M06b2c per-entry update hook own those shapes).
- mount/claim installation: handlers are evaluated once, validated callable,
and installed as addEventListener listeners whose removal is queued on the
mount/claim LifetimeScope; refs call the handler with the element, queue the
class-path detach (returned unsubscribe when callable, then handler(null) in
a finally), and are likewise scope-owned. Claim installation is emitted only
in the phase-2 tail after the structural mismatch walk, and event/ref names
are skipped by the static attribute seed. A throwing later installation step
is released by the existing owner rollback (shell connect catch,
claimRecipe rollback) while borrowed DOM is retained — bounded,
nontransactional.
- SSR: event/ref render no bytes and are skipped by both the static and the
dynamic producer, preserving the M08-1a static no-argument form/bytes. Local
setup resources are now emitted only when the server output transitively
reads them (a reachability walk over server-visible expression records plus
recipe targets), so a helper reachable only from a client channel never makes
__ssr request a callable; surviving locals keep their authored order.
- expression-local reads: a binding declared inside the lowered expression
(handler/ref parameter, nested local) is no longer recorded as a host read,
so onClick={(event) => ...} / ref={(el) => ...} do not create requireBinding
lookups for their own parameters.
- tests: Node contract suite (26) covers admission/refusals, validation
ordering, ref cleanup, SSR exclusion + transitive pruning, claim phase-2
ordering, carry boundaries (imported handler / owned signal still refused by
the generated-element lane), and the M08 byte-shape regression. The existing
generated-leaf event-refusal row is converted to the new admission contract.
Signed-off-by: Zhi <146103794+SisyphusZheng@users.noreply.github.com>
Real authored sidecar -> real generated-element lane -> emitted module -> Node __ssrHost bytes -> browser DSD parse -> generated class claim, on Chromium: - the claimed listener fires and the ref callback received the element; no onClick/ref DOM attributes and no attachShadow call exist; - disconnect removes the listener and runs the ref teardown (unsubscribe then handler(null)); reconnect re-claims the retained root and re-installs both under the fresh scope; dispose(false) keeps the claimed tree and dispose(true) removes only the owned region; - CSR mount installs both, and disconnect/reconnect re-installs them on the retained claim; - a corrupted borrowed DOM fails the claim with OEIR_LEAF_CLAIM_MISMATCH and installs nothing (no listener, no ref), borrowed DOM retained; - a phase-2 installation throw (non-callable ref binding supplied by the test through the shell's createBindings) releases the already-installed listener through the claim owner rollback while borrowed DOM stays — the bounded, nontransactional failure contract. The hand-supplied binding exercises the runtime precondition only and does not claim production binding carry. One engine (Chromium), LOCAL; imported-handler/owned-signal carry remains refused by the generated-element lane and is not claimed here. Signed-off-by: Zhi <146103794+SisyphusZheng@users.noreply.github.com>
…e's report-only/meta contradiction
The matrix stated a blanket fact ("CSP covers prerendered pages") and the
security guide said reportOnly swaps the SSG meta tag. The source does neither:
CSP is opt-in per app on both channels, and the static meta channel refuses
report-only at build time.
Matrix (docs/architecture/security-baseline-matrix.md):
- CSP is a configured capability: request-time middleware only when
middleware.csp is set (entry-descriptor.ts:370); SSG meta only when
middleware.csp.policy is set (ssg-render.ts:470-478).
- The header channel carries the full directive set, report-only and
per-request nonce (entry-server-codegen.ts:87-131).
- The static meta channel is policy-only: postprocess.ts refuses reportOnly and
any policy carrying frame-ancestors/report-uri/report-to/sandbox with
OE_SSG_CSP_META_UNSUPPORTED; nonce:true warns and falls back to policy-only.
- Static file responses write only content-type/cache-control
(static-serve.ts:260-262); the 11 security headers and CSP headers stay
request-time, and the pure-static 405 carries Allow (:384-388).
- Recorded as a dated correction (section 4) with the channel ownership
(framework request-time / framework static meta when configured / host
infrastructure) unchanged.
Security guide (en+zh): reportOnly now documents the request-time header only
and the SSG fail-closed refusal; the CSP intro states the opt-in condition and
the meta directive limits. content-dates stamps for guide/security move to
2026-11-14 (the content commit carries its own manifest edit).
No capability, policy, API, CI job, scanner or generated surface change. The
architecture README is untouched: the matrix front-matter topic/summary did not
change, so docs-index:check passes without regeneration (event lane coordination
noted in the report).
Checks (LOCAL, this worktree, base 089ace0): docs-index:check pass (33 active /
8 historical / 15 entries); check:content-dates pass (27 articles, en/zh);
check:doc-claims pass (68 files, 45 versions, 36 gates, 10 CI jobs);
markdownlint-cli2 on the four changed files 0 issues. No source, public API,
snapshot or generated artifact change; no push.
Signed-off-by: Zhi <146103794+SisyphusZheng@users.noreply.github.com>
…ient-only semantics.
Addresses the independent review's three must-fix items (no push; recheck
pending):
- Invalid callable event/ref values now throw the shared internal coded
`OpenElementError` through `leafRuntimeError(code, detail, origin)` =
`frameworkError(..., { phase: 'csr' })` with the ONE
`locatedDetail`/`originArgs` authored `(file:line:column)` suffix. Codes stay
internal identities (`OEIR_LEAF_*_HANDLER_INVALID`); no public catalogue
growth. Node text tests pin the helper call + origin literal; Chromium pins
name/code/phase/severity/recoverable/authored suffix.
- Client-only setup pruning is recorded as an explicit phase-labelled SSR
contract in `jsx-value-and-children-model.md` §4.2a and the new M09-2c
tracked record: setup reachable only from event/ref is 0 in SSR, 1 per
client mount/claim; surviving locals keep authored order. Reachability tests
cover component-reference prop records, children recipes, insertion-site
targets and typed condition tests, plus dead-local exclusion and the
SSR0/CSR1 counters in Chromium.
- Conditional-branch event/ref widening is documented and tested on the mount
and SSR->claim->toggle paths: branch-owned scopes detach/reinstall exactly
once; list/custom/component-prop refusals unchanged.
- Registers the new intrinsic event/ref seam row (coded-error formatter,
install/pruning owner, guard tests) and the M09-2c tracked record; regenerates
the docs index and the internal-runtime interface snapshot (`leafRuntimeError`).
- M08 S2/maps suites and full compiler/element/router suites re-run; no
class-lane semantics changed; no version/tag/push.
Signed-off-by: Zhi <146103794+SisyphusZheng@users.noreply.github.com>
Deploying openelement with
|
| Latest commit: |
b6644d3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4e723881.lessjs.pages.dev |
| Branch Preview URL: | https://feat-m09-event-ref-impl.lessjs.pages.dev |
…y (CodeQL 4254) CodeQL alert 4254 (js/bad-code-sanitization, CWE-79/94/116, medium) reports that the intrinsic event/ref install emitter interpolates a JSON.stringify-ed value into constructed JavaScript (generate.ts:1737, columns 72-97 = \"JSON.stringify(eventName)\"). The rule's documented remediation is post-stringify escaping of the script-breaking characters; JSON.stringify alone is not accepted as a sanitizer even when the value is allow-listed. Assessment: not an exploitable injection in this lane. The event name derives from the authored TSX attribute name; the frontend admits the event channel only for /^on[A-Z]/, and clientOnlyEventName further narrows the value to /^[a-z][a-z0-9-]*$/ (probed: onClick -> click; onX-foo -> x-foo; onX\$, onX_foo and onX:y are refused with OEIR_LEAF_UNSUPPORTED_ATTRIBUTE before any code is built). Quotes, backslashes, '<', '>', '/', line terminators and U+2028/U+2029 cannot be represented by an admitted name, and the generated artifact is an ES module chunk rather than inline script HTML. The finding is therefore a codegen-hygiene true positive, not a runtime vulnerability - but it is a real merge-gate finding and is fixed as such (no dismissal, no thread resolution). Fix: add the compiler-local codegen literal boundary semantic-ir/generated-leaf/codegen-literals.ts, a mirror of the router's accepted quoteGeneratedJavaScriptValue (JSON.stringify plus \\u003C, \\u003E, \\u2028, \\u2029 escapes; the router dependency direction forbids importing that helper). Use it at the four event/ref code-construction sites: the invalid-callable detail literals and the add/removeEventListener names. No public export, no package/snapshot change, no behaviour change for admitted names. Regression: unit escaping round-trip with '</script>', quote, backslash, newline/tab and U+2028/U+2029 payload; emitted-code safe-literal assertions; non-DOM-shaped event names refused before construction. Evidence: targeted event-ref Node suite 35 tests, Chromium vertical 8 tests, full compiler project 1119 tests, compiler tsc, oxfmt/oxlint clean. Not pushed; PR #1788 remains open and unmerged. Signed-off-by: Zhi <146103794+SisyphusZheng@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bounded, compiler-private M09-2c capability: the generated native-element leaf now admits the existing authored JSX intrinsic
onClick/refchannels, installs them as client-only attachments on mount and claim phase 2, keeps SSR handler/ref-free, and reports invalid callables through the repository's coded-error dialect with authored origin.The branch also carries one accepted factual documentation correction: the security baseline matrix / security guide CSP claims (CSP is opt-in; static meta is policy-only and refuses report-only plus the meta-ignored directives; header channel carries nonce/report-only).
Commits
fd90c0c86fe6d3cdf2480513d1a560fb9fc)e144c799dSemantics pinned (explicit deltas)
Client-only handler setup is phase-labelled: SSR 0 / client mount 1. Event/ref render no SSR bytes and are skipped by both producers, preserving the M08-1a static
__ssr()no-argument form and bytes. A local setup resource reachable only from a client-only channel is pruned from the SSR producer and is not evaluated on the server; the client mount/claim evaluates it once. A server-read local is evaluated once at SSR as well, in authored order. This is recorded in the §4.2a addendum ofdocs/architecture/jsx-value-and-children-model.mdand asserted by phase-labelled counts (SSRsetup 0 / dead 0 / order ['first','second']; CSRsetup 1 / dead 1).Conditional branch widening (explicit). Event/ref inside a conditional branch are admitted; installation is branch-scope-owned, so activation attaches exactly once and branch disposal detaches listener/ref exactly once (mount and claim toggle covered in Chromium). This is a deliberate widening over the class lane's narrower branch refusal; it is recorded in
docs/architecture/m09-2c-intrinsic-event-ref.md. List entry templates, custom-element hosts and component-reference event/ref props remain refused.Remaining unsupported carry (explicit). Imported/module function handlers and closures over owned signals still become host bindings, so the generated-element lane refuses the module with
OEIR_GENERATED_ELEMENT_UNBOUND_BINDING. No binding/import carry channel and no second callback API are invented; production wiring is not claimed.Coded errors + authored origin. Invalid callables throw the shared internal helper
leafRuntimeError=frameworkError(code, locatedDetail(detail, origin), { phase: 'csr' }):OpenElementError, internal identity (OEIR_LEAF_EVENT_HANDLER_INVALID/OEIR_LEAF_REF_HANDLER_INVALID, not public catalogue members),severity: 'error',recoverable: false, authored(authored file:line:column)suffix. The helper is exported through the internal./internal/compiled/runtimesubpath; no public API/export change.Lifetime/rollback. Listener/ref cleanup is queued on the mount/claim scope. A non-callable value throws before any listener/ref side effect; a claim mismatch installs nothing; a phase-2 installation throw releases the already-installed listener while borrowed DOM stays; ref detach calls the returned unsubscribe (if any) then
ref(null)even when the unsubscribe throws. These remain bounded/nontransactional (no byte transaction, no borrowed-DOM restore).Exact snapshot delta
docs/release/public-interface-snapshot.json—@openelement/elementsubpath./internal/compiled/runtimeonly:publicShapeSha256:343dc604d0d38ca28bc2e07f9fdf736fd9fb9cd1398ce2bf6d25ddf8a1f21097→15d9f4acca2540677f614a0fe9dca304010658936f684f152796884ebf7e99e3leafRuntimeErrorinterface:snapshotmatches (6 packages).Tests and evidence (LOCAL, Chromium 147.0.7727.15 unless noted)
generated-leaf.event-ref.test.ts(Node)generated-element.event-ref.browser.test.ts(real Chromium)generated-leaf.claim.test.ts(M07-5 regression)vitest run --project compiler(worktree, final head)vitest run --project element(shell/class-path regression baseline)check-public-interface-snapshot.tsdocs-index:check/seams:checkvp runfmt/lint/markdown/docs-index/typecheck, release graph, pack dry-run, pack-surface, export-files)Review record: independent review requested changes on (1) coded-error dialect/authored origin, (2) undocumented SSR setup-count delta + reachability coverage, (3) undocumented/untested conditional widening. All three are addressed in
e144c799d, and the independent re-review passed (targeted suites + full compiler + snapshot/docs/seams + independent lane/Chromium reproductions). Review documents live outside the repository as task artifacts.Non-claims
Not full authored Custom Element parity and not the M09 exit; not M13 (one Chromium engine, LOCAL); no version, tag, publish, Starter default or release claim; no merge. Imported handler/owned-signal carry, listener replacement/update semantics and component-reference callbacks remain out of scope.
Checklist
main@089ace0a8(currentorigin/maintip)