Skip to content

feat(auth)!: an auth handle that outlives its session - #6109

Merged
kixelated merged 48 commits into
mainfrom
quest/m1/auth/issuer
Oct 24, 2026
Merged

kixelated merged 48 commits into
mainfrom
quest/m1/auth/issuer

Conversation

@kixelated

@kixelated kixelated commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest m1/auth/issuer (its file is deleted in this PR). An auth handle now outlives its session, so a route naming a token keeps it across a reconnect, in both Rust and JS.

Problem

A token added on session.auth() ended with its session, so after a reconnect the application had to add it again and re-announce every route naming it. moq-tokio papered over this with a second connection::auth handle that replayed tokens onto each new session, which a Route could not name.

Approach

moq-net (rs/moq-net/src/auth.rs, client, session, goaway, lite/ietf drivers)

  • auth::Handle is session-independent: Handle::new() plus Client::with_auth(handle). Each session built on it presents every token the handle holds, so a Route naming one of its Auths keeps it across a reconnect with no re-add or re-announce.
  • A handle backs one active session at a time, plus any draining after a GOAWAY. Attaching another while one is active fails with Error::Duplicate.
    • A draining session opens no new AUTH streams or requests, on lite and moq-transport, so a token added or still queued at the GOAWAY rides only the successor.
    • A token still unanswered on the draining session can still be answered there, and its issued grant stays open.
    • A draining session's verdict defers to an active session that still presents the token, even one mid-retry with no grant.
  • Auth::grant() and Handle::grant() follow the newest open session, and are None while disconnected.
  • Only the peer's verdict ends a token: a non-transient refusal, Unsupported, or a clean close after a grant. A session ending does not.
  • On lite, a transient refusal (Internal, TooManyRequests, Timeout) is presented again after a backoff of 1s doubling to 30s. moq-transport AUTH_ERROR has no transient code, so its presenter has no retry loop. This logic moved here from moq-tokio.
    • Through the backoff the token counts as unanswered, so a publication or request it might cover waits instead of being judged unauthorized.
    • The peer already replied, so a retry stream lost before its reply is not Unsupported, and a shared handle keeps the token.
  • Handle::abort(err) ends a handle; a handle a session creates for itself still ends with it.
    • That handle has no successor: any end of a token's stream is final, and a token added after its GOAWAY fails with GoingAway.
    • A retry that wakes once its session drains ends with GoingAway, never Cancel, so it never reads as the peer's clean close.
  • Handle::add resolves with (Auth, Answer): Answer::Granted when the peer granted the token on its AUTH stream, Answer::Requests when only the requests naming it carry it (moq-transport without AUTH). The answer is fixed once given, so it holds after the session drops.
    • moq-ffi refuses only an Answer::Requests token, instead of inferring that from a missing grant, which a dropped session also reads.
    • js/net has no such consumer, so it keeps add() resolving with the token alone.

moq-tokio

  • Connection::auth() returns moq_net::auth::Handle, and every dial is built on it. Aborting it ends the loop at its next reconnect.
  • Shared::end ends the handle with the loop: Closed for a stop, Transport for a non-moq-net failure, Unauthorized for a rejection, or the session's error. Never Cancel, so moq-ffi's MoqAuthToken::closed() reports it as an error, as before.
  • The connection::auth module, its replay loop, and the tokens replay list in the connection state are deleted.

moq-ffi

  • MoqAuth wraps moq_net::auth::Handle directly; the Handle/Watch/Token enums and their Connection variants are gone.
  • A browser dial creates Handle::new() up front and passes it through Client::with_auth, so a token added while dialing waits for the session. The dial ends the handle when it fails, is stopped, or its session closes, so that token never waits forever. This replaces the separate wasm Dial variant.

js/net mirrors the design:

  • Auth.Handle is a class.
  • connect({ auth }) builds a session on one.
  • A reconnecting Connection owns one as connection.auth.
  • Auth.Duplicate is new.
  • Transient refusals retry with the same backoff.
  • A moq-transport GOING_AWAY reply is no verdict, as in Rust, so a shared handle's token survives it.
  • Aborting connection.auth gives the loop up at its next reconnect.
  • A second requests() throws Auth.Duplicate.

Consumers and docs

  • moq-relay:
    • The benches are updated.
    • Two auth/refresh.rs tests now expect the retry instead of an error.
  • Docs no longer tell a reconnect to re-add the token and re-announce: doc/concept/{moq-lite,standard}.md, doc/lib/{js/net,rs/moq-net}.md, doc/bin/relay/auth.md.

Impact

Breaking, with no wire change.

  • moq-net
    • auth::Handle::new() creates a handle that backs no session yet; pass it to Client::with_auth and every session that client connects presents the tokens it holds. A Route naming one of its Auths keeps that token across a reconnect, so drop the code that added the token again and re-announced after one.
    • New: Client::with_auth, Handle::abort and auth::Answer (Granted, Requests).
    • A handle backs one active session at a time, plus the ones draining after a GOAWAY: connecting another while one is still active fails with Error::Duplicate. Give independent concurrent sessions separate handles.
    • Handle::grant() and Auth::grant() follow the newest open session, so they read None while disconnected instead of keeping the last session's grant.
    • Auth::closed() resolves only on the peer's verdict (a refusal, Unsupported, or a clean close after a grant) or the handle's end, which Handle::abort triggers; a session ending is not one. A handle a session creates for itself still ends with that session.
    • On moq-lite, a token refused with a code that may pass (Internal, TooManyRequests, Timeout) is presented again after a backoff of 1s doubling to 30s, instead of add failing with that code.
    • Handle::add resolves with (Auth, Answer) instead of Auth: replace let token = handle.add(jwt).await?; with let (token, answer) = handle.add(jwt).await?;. Read the answer rather than inferring support from a missing grant: it holds after the session drops, unlike Auth::grant().
  • moq-tokio
    • Connection::auth() returns a moq_net::auth::Handle that every session of the connection is built on, and the connection::auth module (Handle, Auth, Watch) is removed. Replace moq_tokio::connection::auth::{Handle, Auth, Watch} with moq_net::auth::{Handle, Auth, Watch}.
    • A token it returns can now be named by a Route, and keeps riding that route's announcements on every reconnect.
    • Its grant reads None while disconnected. A transiently refused token retries on a fixed backoff of 1s doubling to 30s rather than the reconnect backoff.
  • js/net
    • Auth.Handle is now a class: new Auth.Handle() backs no session yet, and connect({ auth }) builds the session on it, so every session built on one presents the tokens it holds.
    • A reconnecting Connection owns one as connection.auth; a route naming one of its Auth.Auths keeps that token across a reconnect, so drop the code that added the token again and re-announced after one.
    • New: connect({ auth }), Connection.auth and Auth.Duplicate. Connecting another session while one is still active rejects with Auth.Duplicate, and so does a second requests() call, which threw a plain Error before.
    • Established.auth returns the session's handle, so its grant and each Auth.Auth's grant read undefined once the session closes, instead of an empty grant.
    • Auth.Auth.closed settles only on the peer's verdict or the handle's end (handle.abort()); a session ending is not one, except for a handle the session created for itself.
    • On moq-lite, a token refused with a code that may pass (Internal, TooManyRequests, Timeout) is presented again after a backoff of 1s doubling to 30s, instead of add rejecting with that code.
  • moq-ffi and bindings: no API change. In the browser, a token added while a dial is still running waits for its session, and rejects with the dial's failure if it never establishes.

Decisions

The maintainer confirmed all six, each the ✅ option this PR implements.

  1. Server equivalent of with_auth. A server's sessions are separate peers, so a shared handle would merge their authorize() and requests().
    • ✅ (a) Skip it until a consumer needs it.
    • (b) Add Server::with_auth.
  2. Retry pacing. moq-tokio previously reused its configurable reconnect backoff.
    • ✅ (a) Fixed at 1s doubling to 30s.
    • (b) Make it configurable on Handle.
  3. Error when a handle already backs an active session.
    • ✅ (a) Reuse Error::Duplicate (JS Auth.Duplicate).
    • (b) Add a dedicated variant.
  4. Setup or handle token on moq-lite without AUTH. ✅ present succeeds and the token ends with Unsupported. add() still fails at once with Unsupported.
  5. Single-session add() on a transient lite refusal. ✅ It retries instead of returning the code, matching the replay loop it replaces.
  6. JS Connection.auth on a pooled connection. ✅ Expose it as a Getter<Auth.Handle | undefined> that follows the URL, like origin.
    • A pooled connection's handle is shared by every user of that URL.
    • A private loop keeps one handle across URL changes, so its tokens go to the new relay.

Settled while addressing reviews on c6f331e, each the recommended option:

  1. A draining session's late refusal. ✅ It defers to any active session still presenting the token, rather than only one that holds a grant right now, so a successor mid-retry keeps it.
  2. A token waiting out a transient refusal. ✅ It counts as unanswered, as on its first presentation. A request it might cover parks through the backoff rather than failing fast, and enforcement no longer closes the session as Unauthorized.
  3. An aborted connection handle. ✅ The reconnect loop ends with the abort's error at its next reconnect. It does not tear down the live session at once.

Settled 2026-10-24 when merging main past moq-dev/moq#6487, which had built a token replay and a Handle/Watch/Token enum on the module this PR removes:

  1. ✅ Drop the replay: sessions built on the handle present its tokens themselves. The auth-abort error mapping moves into Shared::end.
  2. ✅ moq-ffi keeps only the session handle; a browser dial creates its handle up front instead of a separate Dial variant.

Tests

  • moq-net integration:
    • a_shared_handle_presents_its_token_on_every_session
    • a_handle_backs_one_active_session, including the draining cases
    • a_token_queued_before_our_goaway_rides_only_the_successor: fails on moqt-17 and moqt-22 if the presenter checks only a received GOAWAY.
    • lite_07_presents_a_transiently_refused_token_again
    • lite::session::tests::a_lost_retry_keeps_a_shared_token
    • moqt_{16,22}_route_auth_survives_a_reconnect
  • moq-net unit tests:
    • only_a_verdict_ends_a_shared_token
    • the_answer_outlives_its_session
  • moq-ffi a_grant_survives_its_session_dropping_before_add_returns: fails if add infers request-only from a missing grant.
    • a_transient_refusal_backs_off
  • moq-tokio a_token_is_presented_on_every_session (token refresh across reconnects) and an_ended_auth_handle_ends_the_loop.
  • js/net auth.test.ts:
    • A shared handle presents its token on every session.
    • A handle backs one active session.
    • A lite-07 transient refusal is retried, on a mocked clock.
    • A shared token whose retry stream is reset is kept.
  • js/net migrate.test.ts: the connection's tokens ride every session across a GOAWAY.
  • wasm UniFFI: a token added while a browser dial runs rejects once the dial is refused.

Checks

  • 37fb3dc merges main again at 3b17a0f; just check origin/main passed there (8853 Rust tests).
    • Main's new uring_workers_offer_the_configured_extensions (moq-dev/moq#6646) expected the replaced connection handle to refuse a token without MoQ Auth. It now reads Answer::Requests there and Answer::Granted with the extension, the same distinction.
    • The quest audit (moq-dev/moq#6383) linked the issuer quest this PR deletes; the link is dropped.
    • Codex reviewed a52d00b with no findings.
  • At a52d00b, after merging main at f086280:
    • just check origin/main: every selected module passed, including 8839 Rust tests.
    • just test interop --all: all 164 checks passed.
    • just test wasm --uniffi: every case passed, including the new mid-dial token case on lite, ietf and setup.
    • cargo clippy -p moq-ffi --target wasm32-unknown-unknown -- -D warnings.
    • The merge ports main's server-side Handshake::requests (moq-dev/moq#6470) onto auth::Link, drops the moq-transport prefix tests that moq-dev/moq#6621 made obsolete, takes main's removal of the lite cap tests, and moves this PR's migration notes into Impact, where main now records how to migrate.
  • Earlier heads: cc4c139 passed just check origin/main (8426 Rust tests, including nix flake check) and just test interop --all (164 checks); 2f6b5dd passed 20 reruns of the JS reload and migration tests.
  • Flaky and unrelated: moq-cli export::redundant_importers_switch_through_bufferbloat_on_lite_07, tracked by quest/m1/test-flakes-2/redundant-failover-drift.md.

Follow-ups

  • Server::with_auth, once a consumer needs it (decision 1).
  • Re-present a shared handle's token on the live session when its stream is lost without a verdict, with the retry backoff on both wires. Today it waits for the next session (mriise's review, bug 1, raised again by Codex on 081b756), now quest/m1/auth/token-represent.md.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 9 commits October 20, 2026 15:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix the reconnect test, which shadowed its token and never compiled under
`--all-targets` with tcp; its OOM was the build on a loaded machine, not the
test (it now runs in 0.15s). Hold issued grants instead of leaking them.

Extend `a_handle_backs_one_active_session` to cover a draining session
keeping an unanswered token beside its successor, and a token added after the
drain riding only the successor.

Point the relay auth bench at `moq_net::auth`, update the relay refresh tests
for a transient refusal that now retries instead of failing `add()`, add
MIGRATE entries for moq-net and moq-tokio, and apply rustfmt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirror the moq-net change in js/net. `Auth.Handle` is now a class: a
session-independent handle passed to `connect({ auth })` presents every
token it holds on each session built on it, so a route naming one of its
`Auth`s keeps it across a reconnect. A handle backs one active session at a
time, plus the ones draining after a GOAWAY; attaching another rejects with
`Auth.Duplicate`. Only the peer's verdict, or the handle's end, ends a token.

The reconnect loop owns one handle (`Reload.auth`), surfaced as
`Connection.auth`, and aborts it when the loop closes. A transiently refused
token on moq-lite is presented again after a 1s backoff doubling to 30s, as
in Rust. `Auth.None` is removed.

Update the docs that said to add the token again and re-announce after a
reconnect, and add a MIGRATE entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mark the wire view's auth field internal so the declaration build no longer imports an internal type, and satisfy clippy's field_reassign_with_default in the moq-tokio reconnect test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 21, 2026 00:48
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T20:50:58.638738Z 37fb3dc Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 476f2359-8599-46d4-a3d0-8e58e6f54604
Commits

Reviewing files that changed from the base of the PR and between dfc997d and 37fb3dc.

Files selected for processing: 48
  • dart/moq_ffi/lib/src/moq.dart
  • doc/bin/relay/auth.md
  • doc/concept/moq-lite.md
  • doc/concept/standard.md
  • doc/lib/rs/moq-net.md
  • go/wrapper/session.go
  • js/net/src/auth.test.ts
  • js/net/src/auth.ts
  • js/net/src/auth_session.ts
  • js/net/src/connection/connect.ts
  • js/net/src/connection/migrate.test.ts
  • js/net/src/connection/pool.ts
  • js/net/src/connection/reload.test.ts
  • js/net/src/connection/reload.ts
  • js/net/src/ietf/connection.ts
  • js/net/src/lite/connection.ts
  • js/net/src/wire.ts
  • py/moq-rs/moq/session.py
  • quest/m1/api-audit/net-exports.md
  • quest/m1/api-audit/net-names.md
  • quest/m1/auth/README.md
  • quest/m1/auth/bindings-polish.md
  • quest/m1/auth/token-represent.md
  • rs/moq-ffi/src/auth.rs
  • rs/moq-ffi/src/session.rs
  • rs/moq-ffi/src/session/dial.rs
  • rs/moq-net/src/auth.rs
  • rs/moq-net/src/client.rs
  • rs/moq-net/src/goaway.rs
  • rs/moq-net/src/ietf/auth.rs
  • rs/moq-net/src/ietf/publisher.rs
  • rs/moq-net/src/ietf/publisher/tracks.rs
  • rs/moq-net/src/ietf/session.rs
  • rs/moq-net/src/ietf/subscriber.rs
  • rs/moq-net/src/lite/publisher.rs
  • rs/moq-net/src/lite/session.rs
  • rs/moq-net/src/lite/subscriber.rs
  • rs/moq-net/src/model/origin.rs
  • rs/moq-net/src/server.rs
  • rs/moq-net/src/session.rs
  • rs/moq-net/tests/integration/auth.rs
  • rs/moq-relay/benches/auth.rs
  • rs/moq-relay/src/auth/refresh.rs
  • rs/moq-relay/tests/integration/runtime_uring.rs
  • rs/moq-tokio/src/connection.rs
  • rs/moq-tokio/src/connection/auth.rs
  • swift/Sources/Moq/Auth.swift
  • test/wasm/uniffi/cases.ts
Files with no reviewable changes: 2
  • quest/m1/auth/bindings-polish.md
  • rs/moq-tokio/src/connection/auth.rs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

Walkthrough

The change moves authentication state into reusable handles in JavaScript and Rust. Tokens can persist across reconnecting sessions, while session links track active and draining sessions. Transient refusals retry with capped backoff. Connection pools, reconnect loops, protocol sessions, FFI bindings, tests, and documentation now use the shared handle model. Answer::Granted and Answer::Requests distinguish authentication outcomes.

Priority: Normal

Merge Risk: Minimal · up to 37fb3

This change keeps authentication tokens across reconnects, and the issues raised earlier in review are now addressed. Tokens are only sent to a different host when the application opts into following redirects, and that behavior is documented. No merge-blocking risk remains.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 77.88% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 339 functions across 42 files. (12 skippe… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: authentication handles now outlive individual sessions.
Description check Passed The description directly explains the session-independent auth handle, reconnect behavior, API changes, tests, and follow-ups covered by the changeset.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 77.88% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 339 functions across 42 files. (12 skipped: 10 unsupported, 2 too large.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 Clippy (1.99.0)

Clippy execution timed out

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88e31cd9fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread js/net/src/connection/migrate.test.ts Resolved
kixelated and others added 2 commits October 20, 2026 17:55
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@mriise mriise left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Read-only review of 4cf66d6d6. I ran no tests. The happy path meets the quest: routes keep their Auth across a reconnect without a re-add or re-announce, and there is no wire change. The findings are at the edges.

Bugs

1. A token that ends without a verdict leaves add() and closed() pending.

Link::ended (rs/moq-net/src/auth.rs:1036-1066) records a non-verdict end only on the link's slot. state.end_token runs only for a verdict. Auth::poll_answered (auth.rs:1344) and Selector::ended read the handle-level state.tokens[id], which stays unset.

  • Per-session handle after a GOAWAY: session.auth().add(t) ends with Error::GoingAway (lite/session.rs:583-586, and the ietf loop). add() then stays pending until the drained session closes. Before this PR it rejected at once.
  • ietf stream reset before an answer: Present::run returns Error::Stream(_), which is not a verdict, so Rust add() hangs. JS maps the same case to Unsupported and rejects (auth.test.ts:869).
  • Shared handle, stream lost mid-session: a lite reset with a non-Cancel code after a grant, or a failed poll_open, leaves the token unpresented on the live session. grant() reads None, and closed() waits for a reconnect that may never come.

Suggested fix:

  • When the handle is bound to one session, make any end final (end_token).
  • For a shared handle, re-queue the token on the live link, or end it.
  • A Rust test for the ietf reset case, mirroring the JS one, would pin this.

2. JS presents on a draining session; Rust does not.

Rust refuses new AUTH streams after a GOAWAY, and the PR says "a token added after the GOAWAY rides only the successor".

JS does not. HandleState.add offers to every session (js/net/src/auth_session.ts:231). AuthSession.offer (:586) and the retry in #run check #closed but never #draining. If the draining peer resets that stream before answering, #run maps the StreamError to Unsupported. That is a verdict, so the token ends on the successor too.

Suggested fix: have offer and the post-backoff retry skip draining sessions, and add a JS test for a token added after a GOAWAY.

Risks

3. abort on a connection-owned handle quietly breaks reconnects.

  • moq-tokio Connection::auth(), JS Reload.auth and pooled Connection.auth all expose abort.
  • After an abort, Link::attach and JS vacant() reject every later connect. They do so only after dialing the relay, while the current session keeps running.
  • With pooling, one consumer of a URL breaks reconnects for every other consumer of it.

Consider returning a view without abort, or having the loop close loudly when its handle ends.

4. A clean close after a grant counts as a verdict (auth.rs:1046-1049).

  • A server that drops its Issued before the transport closes ends the client's token on every future session.
  • The moq-tokio test a_token_is_presented_on_every_session already has to keep issued alive past session.abort to avoid this.
  • A natural per-session server task that drops Issued on GOAWAY drain would trigger it.

Consider treating only an explicit AUTH_ERROR as a verdict, or documenting the requirement on acceptors.

5. Docs.

  • Requests::next / poll_next still say "None once the session ends". With a shared handle it is the handle's end.
  • doc/lib/js/net.md says "A Connection's auth handle", but pooled Connection.auth is a Getter<Handle | undefined>.

Questions

  1. A JS reload loop keeps one handle across URL changes, so a token minted for relay A is presented to relay B. Is that intended, or should a URL change abort the handle?
  2. Handle::authorize and the acceptor queue span sessions, so a limit set for one peer carries over to the next. That is fine for the same relay. Is it intended after a GOAWAY redirect to another one?
  3. Rust wraps the ietf presenter in the retry loop (ietf/auth.rs:~298), but from_code never yields a transient code there. Delete the loop, or keep it for parity with lite?
  4. JS Handle.requests() throws a plain Error where Rust returns Error::Duplicate. Should JS throw Auth.Duplicate now that it exists?

(Written by Claude Opus 5.5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated added this pull request to the merge queue Oct 21, 2026
mriise added a commit to mriise/moq that referenced this pull request Oct 21, 2026
moq-dev#6109 replaces moq-tokio's connection auth with one moq-net auth handle that
spans a connection's sessions, so:

- moq-ffi's temporary dispatch enum collapses onto moq_net::auth::{Handle,
  Watch, Auth}.
- Configured tokens are held once per connection on that handle
  (connection/tokens.rs) instead of re-added per session, which would have
  presented each one twice while a GOAWAY'd session drained. Each dial tells
  the holder its connection credential.
- A configured token a moq-transport session cannot send is kept for a later
  session with AUTH rather than dropped, and logged once per dial.
- js/net holds configured tokens once per auth handle, so a reloading
  connection's shared handle presents each once across reconnects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 21, 2026
Resolves the conflicts with #6137: moq-tokio's connection::auth
stays deleted, so moq-ffi's auth wrapper collapses onto the one
moq_net::auth::Handle, as #6137 decided for whichever landed second. Tests
from main that build a session's handle use auth::Link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Merged origin/main after moq-dev/moq#6137. moq-tokio's connection::auth stays deleted, so rs/moq-ffi/src/auth.rs collapses its session/connection enum onto the one moq_net::auth::Handle, as #6137 decided for whichever PR landed second. The Unsupported check for a token nothing carries now applies to client sessions through that same path. quest/m1/auth/bindings-polish.md drops its FFI bullet. Main's new subscriber revocation tests build their handle with auth::Link.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

The review requested for 73bc832 has shown as running for about 90 minutes; requesting it again.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73bc832ed9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread js/net/src/auth_session.ts Resolved
Comment thread rs/moq-net/src/auth.rs Resolved
Comment thread js/net/src/auth_session.ts Resolved
A token added after a GOAWAY was offered to the draining session too: a
draining moq-lite session without AUTH refused it as Unsupported, which
ended it on the successor, and in JS a draining session opened a fresh
AUTH stream for it. Offer new tokens only to active sessions, and in JS
stop a transient retry once the session drains. JS also points the
handle's grants at the successor as soon as it attaches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

3ed5932 fixes the three findings on 73bc832. A token added after a GOAWAY is now offered only to active sessions, in Rust and JS. A JS transient retry stops once its session drains. A JS successor's grant takes over as soon as it attaches.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ed59324f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-net/src/auth.rs Resolved
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

d53b13b fixes the four open findings (c0e3034) and merges main:

  • A lite retry that wakes on a draining session ends with GoingAway, not Cancel.
  • The moq-transport presenter checks the session's own draining state, so a token queued before this side's GOAWAY opens no request on it. New test: a_token_queued_before_our_goaway_rides_only_the_successor.
  • A connection's auth handle ends with Closed or Transport, never Cancel, so moq-ffi's MoqAuthToken::closed() reports the connection's end as an error again.
  • The FFI, Python, Swift and Go docs say a reconnect does not end a token.

The merges port main's erased transport (moq-dev/moq#6176, moq-dev/moq#6539) to auth::Link. Main's an_invalid_attachment_is_refused_without_losing_the_existing_grant (moq-dev/moq#6448) now expects a retry: the relay refuses an invalid label as an auth server fault, which may pass.

just check origin/main passed (8426 Rust tests) at cc4c139. just test interop --all passed all 164 checks there too. The last merge touched only JS UI and quests.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d53b13bad9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread js/net/src/connection/reload.test.ts Resolved
…lock

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 2f6b5dd6c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kixelated

Copy link
Copy Markdown
Collaborator Author

Ready to merge at 2f6b5dd.

  • Since the queue drop, this branch merged main twice: it ports the PR to main's erased transport, adds main's lint reasons, regenerates the Dart FFI checksums, and removes links to the deleted issuer quest. Main's relay test for an invalid attachment label now expects a retry, because the relay refuses the label as Internal, which is transient.
  • Codex's mock-time finding on the reload test is fixed: the test now runs on a Manual clock.
  • Checks: just check (8426 Rust tests) and just test interop --all (164/164) passed at cc4c139. The later heads touched only JS UI, quests and one JS test. At 2f6b5dd, just js check-changed, 20 reruns of the reload and migration tests, and quest check all passed. Codex found no issues at 2f6b5dd.
  • Follow-up: since moq-dev/moq#6448, the relay refuses an invalid stats label as an auth server fault (Internal). With this PR's transient retry, a client keeps re-presenting that token every 30s or less. A non-transient code for an invalid label would stop that.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 24, 2026
kixelated and others added 2 commits October 24, 2026 12:47
Resolve main's connection auth replay (#6487) onto the session-independent
handle: drop the replay, fold the auth-abort mapping into Shared::end, and
build the wasm dial on a Handle::new() passed through Client::with_auth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d while a browser dial fails

Main's Manual.install returns an Installed to close. The wasm UniFFI case
checks a token added mid-dial rejects once the dial is refused, rather than
waiting forever on the dial's handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(auth): an auth handle that outlives its session feat(auth)!: an auth handle that outlives its session Oct 24, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

a52d00b merges main past moq-dev/moq#6487, which had built a token replay on the module this PR deletes:

  • moq-tokio drops the tokens replay; Shared::end now ends the connection's auth handle with the loop's error.
  • moq-ffi drops the Handle/Watch/Token enums. A browser dial creates auth::Handle::new() up front, passes it through Client::with_auth, and ends it when the dial fails, stops, or its session closes.
  • Main's Manual.install() returns Installed, so the PR's JS tests close it.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: a52d00bfb2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

kixelated and others added 4 commits October 24, 2026 13:28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's uring extensions test expected the replaced connection handle to refuse
a token without MoQ Auth. The session-independent handle keeps it for the
requests that name it instead, which Answer::Requests reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

37fb3dc merges main again. The one semantic change: main's new uring relay test (moq-dev/moq#6646) expected Connection::auth().add to fail with Unsupported on moq-transport-22 without MoQ Auth. On the session-independent handle it resolves with Answer::Requests, so the test asserts that, and Answer::Granted with the extension.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 37fb3dc0cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kixelated
kixelated added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit aff2e25 Oct 24, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m1/auth/issuer branch October 24, 2026 22:14
kixelated added a commit that referenced this pull request Oct 24, 2026
The tests #6109 added set backoff through the removed MIN_BACKOFF
and destructure a driver from a server request's ok(), which now returns
the session alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit to mriise/moq that referenced this pull request Oct 25, 2026
Takes moq-dev#6109 as it landed, resolved against the c0b800c head this branch
had merged, and main's transport-config and binding-config refactors.

- Token moves to Setup Parameter 0x7: main gave 0x6 to Implementation.
- Handle::add returns how the peer accepted a token, so the connection's
  token holder reads Answer::Requests instead of inferring it from an
  unknown grant.
- MoqClient::set_tokens becomes MoqClient::tokens() -> MoqTokens with
  set(tokens), mirrored in every wrapper and matching moq-tokio's
  Client::tokens().
- A refused replacement still withdraws the token it replaced (tested).
- Drops the finished token-in-band quest's remaining links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 25, 2026
Since #6109, Handle::add resolves once the peer grants the token, so a
watch created afterwards already holds that grant and changed() waited
forever. relay_auth_refresh hung before its first case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants