Skip to content

fix(popup): keep hand-off values, report departures, expose connectionId - #79

Merged
Wondertan merged 9 commits into
mainfrom
fix/popup-fixes
Oct 2, 2026
Merged

Wondertan merged 9 commits into
mainfrom
fix/popup-fixes

Conversation

@Wondertan

@Wondertan Wondertan commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Popup fixes from the last review passes, plus the double ceremony ID from xgreenx's review of #28. The expired kept port is tracked separately in #78.

Changes

  • Hand-offs keep their values (6cead13). A started port cannot be stopped, so values arriving while a document looked up the Service Worker were dropped, and WebKit also dropped values it had already taken when the port was transferred. The departing document now collects what the port still dispatches and hands it to the worker as a backlog, which the destination delivers first. Also:

    • a messageerror fails the connection as decode-rejected instead of silently closing the carrier;
    • a connected navigate to the popup's own document is rejected instead of stranding the port;
    • every carrier the application may hold reports departure before the popup gives it up;
    • on and send reject unroutable types; the application's ready rejects with connection-closed when it closes before any carrier; one keeper reply deadline covers both registration waits.
  • Naming, API surface, docs and tests (c90d073). Every "fallback" says which one; departure helpers are consolidated; the main entry stops exporting internal hooks; docs match the code, including the unimplemented WebRTC carrier; the testing double follows the real connection's contract.

  • connectionId (416039b, comment). client.new took the ceremony ID beside a connection that already has one, and checked them only for connections client.connect made, so a mismatched ID elsewhere never completed the handshake. Connections now expose their validated connectionId, and client.new uses it; the parameter and the WeakMap go.

    Breaking: client.new(conn, id, platformId, …) becomes client.new(conn, platformId, …).

  • The client generates the connection ID (2df78f3). Callers generated a UUID only to hand it to client.connect, which made freshness their job. client.connect now generates it, as the carried protocol (REQ-POPUP-ID-02), and ignores one a JavaScript caller passes. An application can still connect a popup itself, for example to show its own pages before and after a ceremony, and pass that connection to client.new.

    Breaking: client.connect(popup, { connectionId, … }) becomes client.connect(popup, { … }); read the ID from connection.connectionId.

  • Anchor launches (517b2ec). An anchor launch made every caller invent a window name, mirror it on the anchor, set the anchor's href, prevent its navigation only when the scripted window opened, and keep rel=noopener off unchecked. PopupWindow.fromAnchor(event, features?) does all of it: it names the popup, tries the scripted window first, and suppresses the anchor only when that opened. While a blocked popup awaits binding, a navigate during the activation points the anchor at its destination, so the anchor needs no target; keep a placeholder href so it stays focusable. An activation without an opener-keeping anchor, or with noopener/noreferrer in any letter case or in features, is refused and navigates nowhere.

    anchor.addEventListener('click', (event) => {
      const popup = PopupWindow.fromAnchor(event, 'width=480,height=720')
      const ceremony = client.new(client.connect(popup), 'google', ledger, domain, data)
      void ceremony.proveUserIdentity()   // opens the popup, or points the blocked anchor
    })

    Breaking: PopupWindow.open(target, features?) becomes open(features?), which names its window too; anchor launches use fromAnchor.

  • Review fixes (5073ae5, c2764a6, 166dfc6, fe9c561).

    • The backlog was replayed from a local copy, so a delivered value that made the document leave handed on an empty backlog and dropped the rest, a queued close included. Values now leave the backlog one at a time.
    • A popup-side close() during a hand-off now reports departure over the port in transit; before, the application waited forever.
    • rel="NoOpEnEr" passed the check (relList.contains is case-sensitive, link types are not) and opened an openerless popup; noopener in features threw without suppressing the anchor. Both are refused now.
    • The blocked-anchor e2e now navigates to a destination the anchor's static href doesn't carry, so it proves navigate points the anchor.
    • specs/popup-transport.md now allows the behaviours this PR adds: the backlog in a continuity record (SP-POPUP-03, REQ-POPUP-CONT-06), departure while continuity is pending (REQ-POPUP-CONTROL-08), and pointing the activating anchor while binding is pending (REQ-POPUP-CONTROL-04/04A). Stale test-plan and doc lines match the code.

Verification

Rebased onto main after #74, #80 and #13; ceremony/docs/client.md keeps #80's origin rules beside the connectionId change.

  • build, typecheck, test (popup 153, ceremony 994), typecheck:e2e, lint, fmt:check, test:distribution (32 passed, 7 skipped without a native SWS).
  • Playwright: popup 173 passed, 2 skipped; ceremony 193/193; dev 60/60.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://fix-popup-fixes.previews.lib.id, https://fix-popup-fixes-libid.grounded-systems.workers.dev (commit fe9c561)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://fb5e0039.previews.lib.id, https://fb5e0039-libid.grounded-systems.workers.dev fe9c561 2026-10-02T14:11:59.888Z Visit the dashboard ↗
  • Build: Failed ❌

View logs ↗
d4ae849 2026-10-02T13:28:15.273Z View logs ↗

…ture

A started port cannot be stopped: clearing its handler before the
Service Worker lookup dropped whatever arrived in that gap, and WebKit
also drops values it has already taken from the channel when the port
is transferred. The departing document now collects what the port still
dispatches until the transfer and hands it to the worker as the port's
backlog; the destination delivers it before the port's own queue, so a
value or a close sent right after navigate reaches the destination.

Also:
- a messageerror fails the connection as decode-rejected instead of
  silently closing the carrier;
- a connected navigate to the popup's own document is rejected rather
  than stranding the port with the worker;
- every carrier the application may already hold reports departure
  before the popup gives it up: an unadmitted restored or handshaken
  carrier, a malformed claim answer (claims no longer leak a valid port
  through Promise.all), and closure before the worker takes the port;
- on and send reject types routing would reject;
- the application's ready rejects with connection-closed when it
  closes before any carrier;
- one keeper reply deadline covers both registration waits.

Assisted-by: Claude Opus 5.5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
- qualify every fallback as the fallback constructor or carrier or the
  isolation fallback; resolveIsolationFallback; native-anchor binding;
- leaving, routes and depart replace controlsDone, registrations and the
  four departure builders; retire only means dropping a carrier locally;
- opener-timeout is a diagnostic only, operations on an ended endpoint
  throw connection-closed, and the main entry stops exporting the
  navigation-carrier hooks, ConnectionVersion and the unused count;
- docs match the code: ignored versus rejected handshakes, the unstarted
  echo, worker validation, departure senders, the unimplemented WebRTC
  carrier, pending handshakes, and the keeper API sketch;
- the testing double follows the real connection's contract; tests move
  to their modules, carry the right tags, and assert what they claim.

Assisted-by: Claude Opus 5.5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
client.new took the ceremony ID beside a connection that already has
one. It checked the two matched only for connections client.connect
made, through a WeakMap, so a mismatched ID on any other connection
never completed the handshake instead of failing at once. Connections
now expose their validated connectionId, and client.new uses it as the
ceremony ID; the separate parameter and the WeakMap go.

BREAKING CHANGE: client.new(conn, id, platformId, ...) becomes
client.new(conn, platformId, ...).

Assisted-by: Claude Opus 5.5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Callers generated a UUID only to hand it to client.connect, which made
freshness their job. client.connect now generates it, as the carried
protocol (REQ-POPUP-ID-02), and a JavaScript caller cannot supply one.
client.new keeps running the ceremony under the connection's ID, so an
application can still connect a popup itself, show its own pages, and
pass that connection in.

BREAKING CHANGE: client.connect takes no connectionId; read it from
the returned connection.

Assisted-by: Claude Opus 5.5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
An anchor launch made every caller invent a window name, mirror it on
the anchor, set the anchor's href, prevent its navigation only when the
scripted window opened, and keep rel=noopener off unchecked.
PopupWindow.fromAnchor(event, features?) does all of it: it names the
popup, tries the scripted window first, and suppresses the anchor only
when that opened. While a blocked popup awaits binding, a navigate
during the activation points the anchor at its destination, so the
anchor needs no target or href. An activation without an
opener-keeping anchor is refused and navigates nowhere.
PopupWindow.open(features?) names its window too.

BREAKING CHANGE: PopupWindow.open(target, features?) becomes
open(features?); anchor launches use fromAnchor.

Assisted-by: Claude Opus 5.5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
The backlog was replayed from a local copy, so a delivered value that made the document leave handed on an empty backlog and dropped the rest, a queued close included. Values now leave the backlog one at a time.

An explicit popup-side close() during a hand-off discarded only the selected carrier, which is null while the port is in transit, so the application never learned of the departure. depart() now reports it over the in-transit port as well.

Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Assisted-by: Claude Opus 5.5
Link types are ASCII case-insensitive, but relList.contains is not, so rel="NoOpEnEr" passed the check and opened an openerless popup that could not connect. A noopener or noreferrer in features threw without suppressing the anchor, so the application page followed it. Every refusal now suppresses the navigation.

The docs advise keeping a placeholder href, which keeps the anchor focusable and keyboard-activatable.

Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Assisted-by: Claude Opus 5.5
The application page navigated to the anchor's own href, so a blocked activation connected whether or not navigate pointed the anchor. The destination now comes from the spec, and the static href carries no connection. The noopener refusal uses a mixed-case token, and the open helper drops its unused rel option.

Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Assisted-by: Claude Opus 5.5
The transport spec now allows the hand-off backlog in a continuity record, a departure report while continuity preparation is pending, and pointing the activating anchor during pending native-anchor binding. The test plans and docs match: the keeper holds undelivered values, navigate points a blocked anchor, a messageerror fails the connection through decoding, and client.connect/new take the connection ID from the connection.

Signed-off-by: Wondertan <hlibwondertan@gmail.com>
Assisted-by: Claude Opus 5.5
@Wondertan
Wondertan merged commit fe9c561 into main Oct 2, 2026
19 checks passed
@Wondertan
Wondertan deleted the fix/popup-fixes branch October 2, 2026 14:17
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.

1 participant