Repository navigation
fix: enforce multi-user lifecycle boundaries - #72
Conversation
There was a problem hiding this comment.
Pull request overview
This PR tightens multi-user lifecycle boundaries across the SSC runtime, host component bindings, and router navigation by (1) adding resume-time authorization tied to request identity, (2) making host component registrations strictly mount/unmount-owned and preventing late delivery from mutating released DOM/signals, and (3) introducing declarative route-guard outcomes (allow/forbid/redirect/replace) with fail-closed ordering.
Changes:
- Add SSC resume authorization hooks and session-level machinery to authorize a specific retained-session candidate before attachment.
- Make host component binding + delivery lifecycle-safe via owned registrations, registration tokens, delivery gates, and signal-level gated writes/barriers.
- Add
ResultGuard/GuardResultrouting semantics (forbid/redirect/replace) and expand browser-history tests for these outcomes.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| state/signal.go | Adds gated host signal writes and a barrier to fence in-flight gated writes. |
| ssc/server_test.go | Adds coverage that SSC server refuses unauthorized resume attempts. |
| router/router.go | Integrates result-guard execution into navigation flow with redirect/replace handling. |
| router/router_stub.go | Mirrors result-guard navigation semantics in the non-browser stub implementation. |
| router/router_history_test.go | Adds browser history behavior tests for guard redirect/replace scenarios. |
| router/router_guard_result_test.go | Adds unit tests for ordering, fail-closed behavior, and forbidden/invalid results. |
| router/data.go | Introduces ResultGuard, GuardResult, actions, validation, and new navigation errors. |
| hostclient/runtime_foundation.go | Implements owned component bindings, lifecycle-safe delivery, DOM ownership resolution, and signal gating. |
| hostclient/host_signal_delivery_wasm_test.go | Adds WASM tests for signal gating vs cleanup/re-entrancy edge cases. |
| hostclient/coreinit.go | Switches core hook wiring to the cleanup-capable host registrar. |
| hostclient/component_binding_wasm_test.go | Adds WASM tests for owned binding lifecycle, offline queue coalescing, and DOM targeting safety. |
| host/ws_runtime.go | Adds ResumeAuthorizer + option to authorize retained-session resume per upgrade request. |
| host/ssc_protocol_test.go | Adds protocol-level tests ensuring unauthorized resume is rejected and owner can still resume. |
| host/session.go | Refactors resume into candidate/commit phases for authorization-safe attachment. |
| host/resume_authorization_test.go | Adds targeted tests for authorization, revocation, concurrency, and released/unknown token behavior. |
| core/html_component.go | Makes host component registration mount-owned with idempotent per-lifecycle cleanup. |
| core/hosthook.go | Adds cleanup-capable HostRegistrar + SetHostRegistrar, preserving legacy hook behavior. |
| core/hosthook_test.go | Updates/extends tests for mount-owned host registration and legacy hook compatibility. |
| docs/articles/guide/ssc.md | Documents mount-owned host component bindings and resume authorization configuration. |
| docs/articles/guide/ssc-security.md | Documents resume authorization and client-side delivery scoping implications. |
| docs/articles/guide/routing.md | Documents result guards, ordering, and fail-closed semantics. |
| CHANGELOG.md | Records new APIs and the lifecycle/security fixes introduced by this PR. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Validation note: the PR is temporarily retargeted to |
|
Validation complete on immutable head |
|
Validation complete on immutable head |
|
Validation complete on immutable head |
The merge-base changed after approval.
Authorize SSC resume against the retained session before reattachment, bind host component subscriptions to mounted ownership, and add declarative route guard outcomes for redirect, replace, and forbidden navigation.\n\nPreserve the legacy resume, host registration, and bool guard APIs while adding migration guidance and native/WASM regressions for cross-user resume, route teardown, late delivery, guard ordering, and browser history.
Treat declarative redirects reached through initial-load or popstate navigation as replacements, avoiding denied entries that bounce under Back/Forward. Clarify the lifecycle test name and document the browser-history exception.
Re-run the committed route guard chain after authority changes, release revoked routed ownership without disturbing MountRoot shells, and preserve declarative redirect and host handoff semantics. Clear revoked route data and cover cancellation, cleanup, history, and browser ownership invariants.
516a8e4 to
3144368
Compare
3144368 to
0756650
Compare
Summary
This PR closes four framework gaps needed by authenticated, multi-user RFW applications without requiring application-specific lifecycle or routing workarounds:
AddHostComponentregistrations to the mounted component lifecycle and make late delivery unable to mutate a released DOM root or RFW host signal;PR #71 is merged into
main; this branch is rebased directly on that integrated history, so its six commits contain only the multi-user lifecycle and routing work described below.A. Resume authorization and identity binding
host.ResumeAuthorizerandhost.WithSSCResumeAuthorizer.resume_rejectedcontrol; the retained session remains detached for its owner.WithSSCSessionInitializercontinues to bind identity/context only for new sessions. Resumed sessions preserve that identity.ReleaseSessionremains idempotent and final: a released token never reaches the authorizer and cannot be revived.WithoutSSCResume.Security regressions cover cross-user token presentation, same-user revocation, an owner resuming after a refused foreign attempt, concurrent resume attempts, unknown/released tokens, the bundled host mux, and
ssc.NewSSCServer.B. Mounted ownership for host components
core.SetHostRegistrarandhostclient.RegisterComponentOwned, while preservingSetHostRegisterandRegisterComponentsignatures.HTMLComponentregisters each declared host component once per mounted lifecycle and releases it idempotently onUnmount; render-only components do not create subscriptions.unsubscribe, and cannot remove a newer registration under the same host name.[data-component-id]root, and never falls back to#appfor an absent owner. Component IDs are escaped safely, with an attribute-scan fallback whereCSS.escapeis unavailable.Browser/WASM regressions cover 100 mount/unmount cycles, cached remounts, idempotent and stale cleanups, offline queue coalescing/order, late delivery, exact DOM ownership, CSS metacharacters, cleanup overlapping DOM delivery, cleanup between signal ownership check and write, and reentrant unmount/remount from a signal setter.
C. Declarative guard results
router.ResultGuard,Route.ResultGuards,GuardResult, and helpers:router.Allow()router.Forbid()router.RedirectTo(path)router.ReplaceWith(path)ErrNavigationForbiddenandErrInvalidGuardResult.Route.Guards,router.Page, and theGroupbuilder. For each route, legacy bool guards run before result guards; parent guards run before child guards.D. Mounted-route revalidation after authority changes
router.Revalidate()androuter.RevalidateContext(ctx).ActivePath, with the same route and query parameters, without re-running loaders, factories,OnParams, renders, history, data, or metadata when access remains allowed.Forbid(), a legacy block, or an invalid guard outcome unmounts the routed owner exactly once, closes itsScopeand owned host registrations, fences late host frames/signals, removes only its routed DOM subtree, and clears its route data/metadata. TheMountRootshell and outlet nodes remain identical.HostReplacepreserves its host-handoff contract: no owner, state, history, or DOM mutation before document replacement. The native stub returns the typedHostNavigationError.RedirectToandReplaceWithreplace the refused current history entry, route normally, and release the previous owner only after the destination commits; a denied or failed destination leaves the owner intact.Native and Chrome/WASM tests cover allow/no-op, Forbid, legacy block, invalid outcomes, HostReplace, redirect/replace success and failure, nil/cancelled contexts, in-flight cancellation, exact-once teardown, shell/outlet identity, no-outlet ownership, Back/Forward, 50-cycle idempotence, Scope/host/noVNC-style cleanup, and late-frame rejection.
Compatibility and migration
Existing code continues to compile and behave as before:
WithSSCResumeAuthorizeris configured;SetHostRegisterandRegisterComponentremain available with their legacy lifetime;The changelog and SSC/routing guides include multi-user configuration and migration examples. No sport-arbs code or application-specific behavior is included.
Verification
Run locally against immutable head
07566500ba03a7fdbc0143bcda886059ad48bfd1, based directly onmainat3f2813dd0b3cbddd7a867deeb25c737477a2868c:go test ./...go test -race ./...go vet ./...go build ./...GOOS=js GOARCH=wasm go vet ./...GOOS=js GOARCH=wasm go build ./....github/scripts/wasmbrowsertest-retry-race, 20 repetitions-race, 20 repetitionsnet,net/http,crypto/tls): cleanAll local gates pass. GitHub Actions is green on the immutable head, including native and browser-WASM tests, native/WASM golangci, secret scan, and EUProvGuard.