Skip to content

fix: enforce multi-user lifecycle boundaries - #72

Merged
pietrodicaprio merged 6 commits into
mainfrom
feat/multiuser-ownership-contracts
Sep 1, 2026
Merged

pietrodicaprio merged 6 commits into
mainfrom
feat/multiuser-ownership-contracts

Conversation

@pietrodicaprio

@pietrodicaprio pietrodicaprio commented Aug 30, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR closes four framework gaps needed by authenticated, multi-user RFW applications without requiring application-specific lifecycle or routing workarounds:

  1. bind SSC session resume to the authenticated identity/revocation policy of the reconnecting request;
  2. bind AddHostComponent registrations to the mounted component lifecycle and make late delivery unable to mutate a released DOM root or RFW host signal;
  3. add declarative route guard outcomes for allow, forbidden, redirect, and history replacement while preserving fail-closed ordering;
  4. revalidate the currently mounted route after authority changes and release revoked routed ownership without unmounting the persistent shell.

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

  • Adds host.ResumeAuthorizer and host.WithSSCResumeAuthorizer.
  • Runs resume authorization against the exact retained session candidate before attachment, timer cancellation, connection replacement, or replay.
  • Commits the reattach only if the token still maps to that candidate and it remains resumable; concurrent attempts can authorize concurrently, but only one can attach.
  • A refused or stale token gets a fresh session and the existing protocol-level resume_rejected control; the retained session remains detached for its owner.
  • WithSSCSessionInitializer continues to bind identity/context only for new sessions. Resumed sessions preserve that identity.
  • ReleaseSession remains idempotent and final: a released token never reaches the authorizer and cannot be revived.
  • Historical token-only resume remains the default for compatibility. Authenticated multi-user deployments must add a resume authorizer or use 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

  • Adds cleanup-capable core.SetHostRegistrar and hostclient.RegisterComponentOwned, while preserving SetHostRegister and RegisterComponent signatures.
  • HTMLComponent registers each declared host component once per mounted lifecycle and releases it idempotently on Unmount; render-only components do not create subscriptions.
  • Remounting, including a template-cache hit, creates a fresh owned registration.
  • Cleanup removes reconnect initialization, emits/queues one unsubscribe, and cannot remove a newer registration under the same host name.
  • Offline route churn coalesces registration controls per host component instead of retaining one control per mount/unmount.
  • Delivery validates a monotonic registration token under lifecycle serialization, targets only the exact [data-component-id] root, and never falls back to #app for an absent owner. Component IDs are escaped safely, with an attribute-scan fallback where CSS.escape is unavailable.
  • RFW host signals carry the registration gate into the signal value lock. Cleanup closes the gate and fences already-authorized writes before returning, without holding framework binding locks across effects/listeners. A setter may unmount or remount its component without deadlock; that ends the old frame.

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

  • Adds router.ResultGuard, Route.ResultGuards, GuardResult, and helpers:
    • router.Allow()
    • router.Forbid()
    • router.RedirectTo(path)
    • router.ReplaceWith(path)
  • Adds ErrNavigationForbidden and ErrInvalidGuardResult.
  • Preserves Route.Guards, router.Page, and the Group builder. For each route, legacy bool guards run before result guards; parent guards run before child guards.
  • The first non-allow result stops evaluation. A forbidden or invalid result runs no loader, factory, unmount, history mutation, or DOM commit.
  • Redirects use the existing bounded redirect context. Empty, relative, scheme-relative, and unknown outcomes fail closed.
  • Browser history tests cover replace, push, Back/Forward, and direct deep links. The native stub follows the same guard semantics without browser history.

D. Mounted-route revalidation after authority changes

  • Adds router.Revalidate() and router.RevalidateContext(ctx).
  • Re-runs the exact parent-before-child legacy/result guard chain for the committed ActivePath, with the same route and query parameters, without re-running loaders, factories, OnParams, renders, history, data, or metadata when access remains allowed.
  • Cancels an in-flight navigation before guard evaluation so a loader authorized under the previous authority snapshot cannot commit late.
  • Forbid(), a legacy block, or an invalid guard outcome unmounts the routed owner exactly once, closes its Scope and owned host registrations, fences late host frames/signals, removes only its routed DOM subtree, and clears its route data/metadata. The MountRoot shell and outlet nodes remain identical.
  • HostReplace preserves its host-handoff contract: no owner, state, history, or DOM mutation before document replacement. The native stub returns the typed HostNavigationError.
  • RedirectTo and ReplaceWith replace 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.
  • Revalidation is a no-op when there is no mounted routed owner, no matching committed route, or a newly registered route does not own the mounted component.

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:

  • resume stays token-only unless WithSSCResumeAuthorizer is configured;
  • SetHostRegister and RegisterComponent remain available with their legacy lifetime;
  • bool guards and existing route builders are unchanged.

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 on main at 3f2813dd0b3cbddd7a867deeb25c737477a2868c:

  • go test ./...
  • go test -race ./...
  • go vet ./...
  • go build ./...
  • GOOS=js GOARCH=wasm go vet ./...
  • GOOS=js GOARCH=wasm go build ./...
  • CI-equivalent full browser/WASM suite through .github/scripts/wasmbrowsertest-retry
  • targeted browser/WASM ownership/signal race tests, 20 repetitions
  • slow-client write-deadline/broadcast isolation tests from fix: bound SSC outbound delivery #71, 10 repetitions
  • resume identity/revocation/concurrent-release tests under -race, 20 repetitions
  • declarative guard ordering/fail-closed tests under -race, 20 repetitions
  • mounted-route revalidation native stress, 20 repetitions, and Chrome/WASM stress, 10 repetitions
  • client-only forbidden import gate (net, net/http, crypto/tls): clean

All local gates pass. GitHub Actions is green on the immutable head, including native and browser-WASM tests, native/WASM golangci, secret scan, and EUProvGuard.

@pietrodicaprio
pietrodicaprio requested a review from a team as a code owner August 30, 2026 02:01
Copilot AI lite review requested due to automatic review settings August 30, 2026 02:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / GuardResult routing 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.

Comment thread router/router.go
Comment thread core/hosthook_test.go
@pietrodicaprio

Copy link
Copy Markdown
Member Author

Validation note: the PR is temporarily retargeted to main because this repository runs pull-request CI only for that base. PR #71 remains the logical dependency and its two commits are unchanged; after the head SHA is CI-green, the PR will be returned to the stacked fix/ssc-outbound-queue base.

@pietrodicaprio
pietrodicaprio changed the base branch from fix/ssc-outbound-queue to main August 30, 2026 02:09
@pietrodicaprio
pietrodicaprio changed the base branch from main to fix/ssc-outbound-queue August 30, 2026 02:13
@pietrodicaprio

Copy link
Copy Markdown
Member Author

Validation complete on immutable head dbc282d756cd1b806f319d3019a11df720fb03be: native tests, browser/WASM tests, native+WASM golangci, secret scan, and EUProvGuard all passed in run 33287385737. The PR is now restored to its logical stacked base, fix/ssc-outbound-queue / #71.

@pietrodicaprio
pietrodicaprio changed the base branch from fix/ssc-outbound-queue to main August 30, 2026 04:33
@pietrodicaprio
pietrodicaprio changed the base branch from main to fix/ssc-outbound-queue August 30, 2026 04:35
@pietrodicaprio

Copy link
Copy Markdown
Member Author

Validation complete on immutable head 28ad4f7c693364515835efee8fb7e0c69a93333a: native tests/build/vet, full race suite, browser/WASM build/vet/tests, native+WASM golangci, secret scan, EUProvGuard, slow-client queue regression, resume-authorization stress, and HostReplace negative/stress coverage all passed. CI run: https://github.com/rfwlab/rfw/actions/runs/33292753813. The PR is restored to its logical stacked base, fix/ssc-outbound-queue / #71.

@pietrodicaprio
pietrodicaprio changed the base branch from fix/ssc-outbound-queue to main August 30, 2026 05:48
@pietrodicaprio
pietrodicaprio changed the base branch from main to fix/ssc-outbound-queue August 30, 2026 05:54
@pietrodicaprio

Copy link
Copy Markdown
Member Author

Validation complete on immutable head 516a8e44d42c823a1ecaf7fe64b22b5145bbb42f: native tests/build/vet, full race suite, browser/WASM build/vet/tests, native+WASM golangci, secret scan, EUProvGuard, slow-client queue regression, resume-authorization stress, and mounted-route revalidation ownership/security stress all passed. CI runs: https://github.com/rfwlab/rfw/actions/runs/33295596060 and https://github.com/rfwlab/rfw/actions/runs/33295603445. The PR is restored to its logical stacked base, fix/ssc-outbound-queue / #71.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.

mirkobrombin
mirkobrombin previously approved these changes Aug 31, 2026
Base automatically changed from fix/ssc-outbound-queue to main August 31, 2026 14:41
@pietrodicaprio
pietrodicaprio dismissed mirkobrombin’s stale review August 31, 2026 14:41

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.
@pietrodicaprio
pietrodicaprio force-pushed the feat/multiuser-ownership-contracts branch from 516a8e4 to 3144368 Compare August 31, 2026 14:41
@mirkobrombin
mirkobrombin force-pushed the feat/multiuser-ownership-contracts branch from 3144368 to 0756650 Compare August 31, 2026 15:49
@pietrodicaprio
pietrodicaprio merged commit d188713 into main Sep 1, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants