Skip to content

feat(net): exchange grants over the moq-transport MoQ Auth extension - #4124

Merged
kixelated merged 21 commits into
quest/m1/auth/READMEfrom
quest/m1/auth/moq-transport
Sep 25, 2026
Merged

kixelated merged 21 commits into
quest/m1/auth/READMEfrom
quest/m1/auth/moq-transport

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Problem

moq-transport sessions carry no AUTH exchange: Session::auth() stays None, a publisher outside its grant waits forever, and a session can't learn which role its peer will ever let it play (moq-wg #1854). Completes the moq-transport child of the in-band auth line (#4039).

Approach

  • New drafts/draft-lcurley-moq-auth.md: an AUTH Setup Option (0x40B60, value 1, draft-17+) negotiates AUTH / AUTH_OK / AUTH_ERROR (0x40B61..0x40B63) on request streams, one per token, living as long as the token. Grants are namespace-prefix tuples; AUTH_ERROR reuses REQUEST_ERROR codes. An unrepresentable grant is refused with NOT_SUPPORTED (an update revokes that token only), never widened.
  • Rust ietf/auth.rs: codec, presenter, and acceptor over the shared auth::Handle. ietf::start presents the empty token after SETUP when both sides offered the option, otherwise fails tokens as Unsupported. run_dispatch routes AUTH only when negotiated; otherwise it stays an unexpected stream (PROTOCOL_VIOLATION).
  • Enforcement is shared: auth::Enforce (moved out of the lite publisher) closes a dialing session with Unauthorized naming the path. The IETF announce loops wait for the setup token's answer, then advertise only what the grant covers; a shrink withdraws PUBLISH_NAMESPACE/NAMESPACE, ends served subscriptions with PUBLISH_DONE UNAUTHORIZED, and cancels our own subscriptions it no longer covers.
  • JS mirrors it: the lite AuthSession moved to auth_session.ts behind an AuthWire interface with lite and IETF bindings, plus a shared enforceGrant. exchangeSetup reads/writes the option, ietf/connection.ts dispatches AUTH.
  • Lite fix found by the shared tests: the lite announce loop now also waits for the setup token's answer, so a pre-existing out-of-grant broadcast never reaches the peer before the abort.

Impact

  • Wire: new MoQ Auth extension on moq-transport draft-17+ (setup option 0x40B60, message types 0x40B61-0x40B63). Peers without the option see nothing new. Additive.
  • Rust API: none new. Session::auth() now works on negotiated moq-transport sessions. Internal PublishDoneStatus::Unauthorized (0x1).
  • JS API: connection.auth is live on negotiated moq-transport sessions (was Auth.None). Ietf.Auth namespace added to the internal ietf module exports.
  • Lite behavior: a lite-06 publisher no longer answers announce interest before its setup token is answered.

Alternatives

  • Carry full patterns on the IETF wire now. Rejected per the quest: that needs a separately negotiated revision; prefixes match SUBSCRIBE_NAMESPACE.
  • Reset the stream for an unrepresentable grant (as lite does). Chose AUTH_ERROR NOT_SUPPORTED since the extension has a reply for it.
  • Map EXPIRED/MALFORMED_AUTH_TOKEN to distinct errors. The public API only speaks SessionError, so every non-NOT_SUPPORTED code surfaces as Session(Unauthorized); revisit if the API grows an expiry variant.

Tests

  • rs/moq-net/tests/auth.rs now runs every case on lite-06, moqt-17, and moqt-22, plus unrepresentable grants/updates and a regression that nothing outside the grant reaches the peer before the answer. Unit tests cover the option on every draft, negotiation requiring both sides, and a peer without the option seeing no AUTH.
  • js/net/src/auth.test.ts runs the session cases on lite-06, moqt-17, moqt-22; ietf/auth.test.ts covers the codec.
  • just check, just drafts check, bun run --cwd doc check pass. just test interop --all passes except the C subscriber, whose libmoq header was missing from this local build (untouched here). The rust/js/python matrix also passes against a relay restricted to moq-transport-22.

Follow-ups

  • Interop grants (quest/m1/auth/interop.md) now also covers moq-transport cells.
  • Surfacing EXPIRED_AUTH_TOKEN distinctly would need a public error variant.

🤖 Generated with Claude Code

(written by Claude Opus 5.5)

kixelated and others added 15 commits September 25, 2026 05:56
…4097)

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>
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>
Co-authored-by: Grok 4.7 <noreply@x.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…racks (#4073)

Co-authored-by: Brad Greenway <brad.greenway@surveillx.com>
Co-authored-by: Luke Curley <kixelated@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#4084)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review September 25, 2026 07:48
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 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-09-25T13:50:54.527679Z 43fc828 New commits
ℹ️ 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.

@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: 35eb0027e2

ℹ️ 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 on lines +430 to +432
const disposeGrant = this.#grant.subscribe(() => {
if (this.#denied(name)) revoke();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recheck the publish grant when installing the watcher

If the grant is revoked while this handler is awaiting broadcast demand, track metadata, or the SUBSCRIBE_OK writes above, the signal notification can complete before this subscription is installed. subscribe() only observes future changes, so revoked then never resolves and the publisher can continue serving the track after its grant has been withdrawn. Recheck #denied(name) after registering the watcher, or arm the watcher before the asynchronous setup work.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 9845b6b: the grant is rechecked right after the watcher is installed, so a shrink during setup revokes the subscription. Rust polls the gate level-triggered and was not affected.

(Written by Claude Opus 5.5)

Comment on lines +606 to +608
disposeGrant = this.#grant?.subscribe(() => {
if (this.#denied(broadcast)) revoke();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recheck the subscribe grant after setup completes

If the grant shrinks while #openSubscribe is waiting for the stream or SUBSCRIBE_OK, its notification can be delivered before this listener is registered. Because subscribing does not invoke the callback with the current value, the completed subscription then remains active indefinitely even though #denied(broadcast) is already true. Check the current grant immediately after installing the listener, or watch it throughout setup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 9845b6b: the grant is rechecked right after the listener is installed, so a shrink during setup revokes the subscription.

(Written by Claude Opus 5.5)

Comment thread js/net/src/ietf/auth.ts Outdated
Comment on lines +136 to +137
await w.u53(AuthOk.id);
return Message.encode(w, async (wr) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preflight AUTH_OK before emitting its type

When a valid prefix grant serializes to more than the 65,535-byte message limit, this writes the AUTH_OK type before Message.encode discovers the oversized body and throws. IssuedGrant.#write then handles the failure by appending an AUTH_ERROR to the same stream, so the presenter interprets the AUTH_ERROR type bytes as AUTH_OK's length and receives a malformed stream instead of the promised NOT_SUPPORTED refusal. Buffer or size the complete message before writing its type, or reset the partially written stream rather than continuing.

AGENTS.md reference: AGENTS.md:L16-L18

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 9845b6b: Message.encode now takes the type id and writes it only after the body fits, so an oversized AUTH_OK leaves nothing on the stream and the NOT_SUPPORTED refusal goes out clean. Regression test in ietf/auth.test.ts.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

MERGE

Positive improvement, complexity justified, approach looks right.

moq-transport sessions with Session::auth() stuck at None and out-of-grant publishers waiting forever is exactly the gap moq-wg #1854 describes. Completing the transport half of in-band auth (#4039) with a negotiated Setup Option and per-token request streams mirrors lite's Auth Stream without inventing a parallel grant model.

What looks right:

  • Draft (0x40B60 / 0x40B61–0x40B63) is clear: both sides must offer value 1; AUTH without negotiation is PROTOCOL_VIOLATION; empty token = connection credential; prefixes only, refuse unrepresentable grants with NOT_SUPPORTED rather than widen.
  • Shared auth::Handle / Enforce across lite and IETF; announce waits for the setup token's answer (lite regression fix included) so nothing outside the grant reaches the peer first; shrink withdraws / PUBLISH_DONE UNAUTHORIZED / cancels uncovered subs without aborting the session.
  • JS AuthSession + AuthWire split is the right reuse; IETF codec + session matrix tests (lite-06, moqt-17, moqt-22) cover negotiation, unrepresentable grants, and the pre-answer leak.
  • Peers without the option see nothing new—additive wire.

Alternatives considered (full patterns now, reset-vs-AUTH_ERROR, distinct EXPIRED API) are correctly deferred. Size is large but coherent (draft + Rust + JS + shared enforcement). CI Check/Test/WASM green.

This is an automated review, not the maintainer's decision
(Written by Grok)

kixelated and others added 5 commits September 25, 2026 05:25
…4128)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4127)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…eir type

Addresses Codex review on #4124: an IETF subscription whose grant shrank
during setup was never revoked, and an oversized AUTH_OK left its type on
the stream before the NOT_SUPPORTED refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	js/net/src/lite/publisher.ts
#	js/net/src/lite/subscriber.ts

@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: cf801e20b1

ℹ️ 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 on lines +48 to +49
/** Our tokens and grants, when the peer negotiated MoQ Auth; see {@link Established.auth}. */
get auth(): Auth.Auth {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update the public JS auth documentation

This getter now exposes live grants on negotiated moq-transport draft-17+ sessions, but the exported Established.auth JSDoc in js/net/src/connection/established.ts still says the grant remains undefined outside moq-lite-06, and doc/lib/js/net.md likewise describes authorization as lite-only. TypeScript consumers following either public reference will incorrectly treat the new IETF behavior as unavailable, so update both alongside this API change.

AGENTS.md reference: AGENTS.md:L27-L27

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated in 43fc828: Established.auth, the auth module docs, and doc/lib/js/net.md now describe the moq-transport draft-17+ exchange when both sides negotiate MoQ Auth. The Rust Session::auth docs and doc/concept/moq-lite.md had the same stale claim, so those moved with them.

(Written by Grok 4.7)

The public JS and Rust session docs still said grants exist only on
moq-lite-06. moq-transport draft-17+ presents the same exchange when
both sides negotiate the extension.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
@kixelated
kixelated merged commit 615f843 into quest/m1/auth/README Sep 25, 2026
6 checks passed
@kixelated
kixelated deleted the quest/m1/auth/moq-transport branch September 25, 2026 13:46

Copy link
Copy Markdown
Collaborator Author

Docs follow-up before merge, for the Codex note that public JS auth docs still said grants were lite-only:

  • Established.auth, the auth module docs, and doc/lib/js/net.md now say moq-transport draft-17+ presents the same exchange when both sides negotiate MoQ Auth.
  • Session::auth and doc/concept/moq-lite.md had the same stale claim, so those moved with them.

Squash-merged onto quest/m1/auth/README as 615f843. That base has no required checks, so auto-merge landed immediately instead of holding for the queued Check/Test run.

(Written by Grok 4.7)

@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: 43fc828404

ℹ️ 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".

// Validated before anything is written, so an unrepresentable grant never
// leaves half a message on the wire. Never widen it: refuse, which revokes
// whatever this stream granted before.
if let Err(EncodeError::Unsupported) = ok.encode_msg(&mut Sizer::default(), version) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preflight the complete Rust AUTH_OK message

When a prefix-only grant contains enough entries to exceed the 65,535-byte message-body limit, this preflight succeeds because encode_msg only counts the body and this branch handles only EncodeError::Unsupported; the failing usize-to-u16 conversion occurs later in Message::encode. encode_message then buffers the type ID, returns TooLarge, and serve_issue resets the stream without sending NOT_SUPPORTED, so the presenter receives a cancellation/stream error instead of the documented Error::Unsupported. Unlike the fixed JS path, this Rust path still needs to validate the complete framed message and convert an oversized grant into AUTH_ERROR before touching the writer. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L16-L18

Useful? React with 👍 / 👎.

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