Repository navigation
fix(auth)!: Rust and JS run one token and key-set corpus - #8221
Conversation
Quest-Claim: 479afac7c324b966
`rs/moq-auth/tests/corpus.json` holds tokens, JWKs, key sets, and grants, each accepted with its claims or refused, and both `moq-auth` and `@moq/auth` replay it. It replaces the hand-copied interop fixtures on both sides. The cases it pins: - `exp`, `iat`, `nbf`, and the grant `expires` refuse `null` and anything but whole seconds from the epoch to the clock's last second. Rust read `"exp": null` as never expiring and saturated out-of-range times. - A key set with a duplicate `kid` is refused in both languages. - A JWK's scope root passes `path::validate` on import in both languages. Beside them, in the relay and `moq auth serve`: - The public rules detect a `jwt` query parameter form-decoded, as the server reads it, so `?%6Awt=<token>` is refused rather than falling through. - `Lease::new` runs `Grant::validate`; an expired grant refuses as expired. - Key files are cached and re-read only when their mtime or size changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 5 minutes. View limit details
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. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head af9ce72.
[P3] Keep migration-guide updates in the release cut — js/auth/MIGRATE.md:7–12 (also rs/moq-auth/MIGRATE.md:7–14 and rs/moq-relay/MIGRATE.md:8–13). Root AGENTS.md reserves MIGRATE.md edits for release cuts and requires breaking PRs to put migration instructions in their Impact section. These additions publish unreleased behavior into the package guides. Move the instructions into this PR's Impact section and leave the guide updates to the release cut.
Direction: the shared corpus and stricter validation are coherent. The current source rounds nbf upward and keys the cache by both path and format, addressing the two earlier issues described in the PR. No additional actionable runtime regression found in this static review.
Verification limits: GitHub-only review of the diff, relevant Rust/JS parsing, grant validation, key-cache code and fixtures. I did not run builds, tests or live rotation/integration checks.
There was a problem hiding this comment.
ℹ️ One minor inconsistency between the PR description and the shipped public API.
Reviewed changes
- Shared corpus:
rs/moq-auth/tests/corpus.jsonis replayed byrs/moq-auth/src/corpus.rsandjs/auth/src/corpus.test.ts, replacing the hand-copied Rust-to-JS and JS-to-Rust interop fixtures; the relay contract tests stay ininterop.test.ts. - Strict times: token
exp/iat/nbfand grantexpiresrefusenull, negatives, fractions, and values past18446744073in both languages, withexp/iatwritten truncated andnbfwritten rounded up. - Duplicate
kid:KeySetparsing refuses two keys sharing akidin Rust and JS. - Scope roots: a JWK scope root is validated on import in both languages; the Rust check moved into
Scope::validateand the duplicate checks inwith_scopeand the JS CLI were dropped. - Relay token detection and lease validation:
presents_tokenform-decodes the query, andLease::newrunsGrant::validatewithGrantExpiredmapping toRefused/expired. - Key-file cache:
serve::Policycaches parsed key files by path and format, re-reading only when the modification time or size changes and dropping a file that disappears.
Verified this run: cargo test -p moq-auth --lib (259 passed), cargo test -p moq-relay --lib auth::tests (34 passed), cd js/auth && bun test (166 passed, 55 corpus cases), plus tsc --noEmit and biome check on the changed JS.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
#8088 narrowed it to private; the corpus test signs raw payloads with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No new issues in the delta. The earlier note on
js/auth/src/wire.ts:37remains open and these commits do not touch it.
Reviewed changes
- Base-branch merges: two merges of
origin/mainbring#8088's API narrowing (privateKeyhelpers,peer.rsremoved) into the branch without changing the PR's own hunks. - Corpus signing helper:
Key::to_encoding_keyis restored topub(crate)becausecorpus.rssigns raw payloads with it; it stays crate-internal, so there is no public API change.
Verified this run: cd js/auth && bun test (166 passed, 0 failed, 55 corpus cases). The Rust suite was not re-run here because this checkout has no debug artifacts and a cold --release build exceeds the sandbox cap; CI covers it.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
No commits landed since the prior Pullfrog review: the synchronize event re-ran at the same head d72729e, and git diff d72729e..HEAD is empty. The only change is the author's edit to the PR description.
- Impact wording corrected: the "New export:
UnixSecondsSchema" line was dropped from the Impact section, matching the code.UnixSecondsSchemaremains an internal helper injs/auth/src/wire.ts, imported byclaims.tsandcontract.ts; onlyPatternListSchemais re-exported viaclaims.ts, so the public API surface is unchanged. - Prior thread resolved: the inline note on
js/auth/src/wire.ts:37is resolved and the substantive concern no longer holds.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Summary
Rust (
moq-auth) and JS (@moq/auth) now replay one shared corpus,rs/moq-auth/tests/corpus.json, of tokens, JWKs, key sets and grants. Each case is either accepted, with the claims it verifies to, or refused.rs/moq-auth/src/corpus.rsandjs/auth/src/corpus.test.tsrun it, so a difference between the two parsers fails a test in both languages. The corpus replaces the hand-copied fixtures injs/auth/src/interop.test.ts, which was JS reading Rust-signed tokens, and in thekey.rstests, which was Rust reading JS-signed tokens. Both directions are now corpus cases. The layout follows the existing shared vectors: the Rust crate owns the file undertests/, Rust loads it withinclude_str!, and JS reads it by relative URL.The corpus pins the fixes from the audit:
exp,iat,nbfand the Grant wireexpiresnow refusenull. They also refuse anything that is not a whole number of seconds from 0 to 18446744073, the last second Rust's clock holds. Before this, Rust read"exp": nullas never expiring, and it silently clamped negative or too-large times into range.kid: a key set with two keys sharing akidis refused in both languages. Before, the first match won.path::validateon import in both languages. In Rust the check moved intoScope::validate, so the separate checks inwith_scopeand the JS CLI were removed.moq auth servedoes, so?%6Awt=<token>is refused instead of falling through to the public grant.Lease::new: it now runsGrant::validate. An expired grant maps toRefusedwith reasonexpired; it used to map toUnavailable, which tells a client to retry.moq auth serve: key files are parsed once and cached by path. Each token costs astat; the file is re-read and re-parsed only when its modification time or size changes, so rotating a key still needs no restart. A key file that disappears is dropped from the cache.Impact
moq-auth(wire, breaking): strict times on claims and grants;KeySetrefuses duplicatekids; JWK import refuses a scope root thatpath::validaterefuses. Migration notes are inrs/moq-auth/MIGRATE.md.moq-auth(wire): times are written as whole seconds, rounded down instead of to the nearest second. This keeps the clock's last second readable on the way back in, and an expiry can only move earlier.moq-auth::serve::Keys: the doc now says a key file is re-read when it changes rather than per request. No API change.@moq/auth(breaking):ClaimsSchema,GrantSchema,KeySetSchemaandScopeSchemahave the same refusals. Migration notes are injs/auth/MIGRATE.md.moq-relay(breaking):Lease::newrefuses a grant thatGrant::validaterefuses. Migration notes are inrs/moq-relay/MIGRATE.md.Choices made without asking
Lease::newrefuses an already-expired grant rather than returning a lease that ends immediately. This matches howAuth::admittreats an answered grant.expiresof 18446744074, which the strict reader refuses. The mTLS integration tests caught this.nulls:Key::from_file_asyncis left in place (public API, now unused in this repo). Grant fields other thanexpires(root,revalidate,tier,label) still readnullas absent in Rust, while JS refuses it. Anullin any of those means the same as leaving the field out, so neither language reads a wider grant.Overlap check
moq-dev/moq#8205 changes
moq auth servedefaults forexpandsign. It doesn't touchexpparsing; this PR only changes whichexpvalues parse. The two PRs may have a textual conflict injs/auth/src/cli.tsandserve.rs. moq-dev/moq#8088 narrows themoq-authAPI surface and builds no corpus.Caused by
No single PR. The Rust and JS parsers were written separately and only checked against a few fixtures; the cause was found by the 2026-11-07 invariant audit.
Checks
quest gate nix develop --command just cipasses on d72729e, after merging main. Main's moq-dev/moq#8088 madeKey::to_encoding_keyprivate, which broke the new corpus test in CI. It ispub(crate)again.Suggested follow-ups
Fix the test-order dependency in
js/net:a connection samples the transport only while a reservation reads the estimate(js/net/src/connection/bandwidth.test.ts) fails when bun's directory-listing order runsauth.test.ts,integration.test.tsandsocket.test.tsbefore it. That is the order in one local checkout. A fresh checkout of the same commit, and current main, pass. This PR doesn't touchjs/net.Make Rust refuse
nullon every Grant wire field, matching JS. Recommendation: drop, because anullthere means the same as an absent field.Models
Size: S
Implementer: Claude Code, claude-opus-5-5, medium effort
Implementer: Claude Code, claude-opus-5-5, medium effort (iterate, XS)
(Written by Claude Opus 5.5 with medium effort)
Review
Rewritten by
quest reviewafter each round.01a1b9ae-ce40-7341-8705-7926e7ed8c9e, round 1No actionable regressions found; parsing, caching, and admission changes appear consistent, but builds and tests were not run as instructed.