Skip to content

feat(openapi): spec the MoQ join-token mint surface - #30

Merged
yakimoto merged 1 commit into
mainfrom
feat/moq-openapi-surface
Jul 25, 2026
Merged

feat(openapi): spec the MoQ join-token mint surface#30
yakimoto merged 1 commit into
mainfrom
feat/moq-openapi-surface

Conversation

@yakimoto

@yakimoto yakimoto commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Closes #24.

What

Adds the MoQ tag and the two join-token mint operations to openapi.yaml — the Media over QUIC product is live end-to-end and had no entry in the spec, so no SDK and no CLI command could be generated for it.

Operation Route Scope
mintMoqPublishToken POST /moq/publish/{ns}/{track} moq:write
mintMoqSubscribeToken GET /moq/subscribe/{ns}/{track} moq:read
  • MoqJoinToken response schema: ok, relayWsUrl, joinToken, expiresIn, ns, track, role, scope, optional protocol.
  • MoqNamespaceParam / MoqTrackParam constrained to ^[a-z0-9-]{1,64}$, matching the gateway's segment regex exactly.
  • Optional x-wave-declare-protocol publish header (documented as best-effort: an unrecognized or unauthorized value is ignored and never fails the mint).

Failures are specified, not just the happy path

400 MOQ_JOIN_BAD_RESOURCE, 401, 402 (x402 challenge), 403, 429, and the fail-closed 503 MOQ_JOIN_UNCONFIGURED — plus a reusable PaymentRequired response and new X402PaymentRequired / X402Accepts schemas.

The 402 needed its own schema because it is not the Error envelope: its error member is a plain string ("payment required") and the normalized WAVE error object is nested under error_detail. Every other 402 in this document $refs Error, which does not match what the gateway returns.

Boundary: the wire protocol is not modelled

The MoQ media session is not an HTTP surface, so it is described in prose in the tag description rather than modelled: the direct-to-relay flow, and the two token carriers (?join= query parameter for browser WebSocket clients that cannot set headers; x-wave-moq-join header server-to-server).

The surface corresponds to draft-ietf-moq-transport-18 — what the relay currently speaks (preferred draft-18, negotiating draft-07..draft-18 over ALPN). draft-19 was published 2026-07-06 and is not yet deployed; adoption is tracked separately. The mint contract is transport-version independent — a draft bump changes the wire session, not this HTTP contract.

Receipts

1. Live probes against api.wave.online (unauthenticated only — no credentials were used):

POST /v1/moq/publish/testns/testtrack        -> 402  {"x402Version":1,"error":"payment required",
                                                      "accepts":[{"scheme":"exact","protocol":"x402",
                                                      "network":"base","maxAmountRequired":"1000",
                                                      "resource":"/v1/moq/publish/testns/testtrack", ...}],
                                                      "error_detail":{"code":"PAYMENT_REQUIRED", ...}}
GET  /v1/moq/subscribe/testns/testtrack      -> 402  (same x402 challenge shape)
GET  /v1/nope/nope                           -> 403  {"error":{"code":"ROUTE_NOT_MAPPED", ...}}

Behaviour note: a request with a syntactically-valid-but-unrecognized bearer token also returns 402, not 401, on these routes — the x402 challenge is offered ahead of key rejection. The spec's PaymentRequired description records this so a generated client does not treat 402 as exceptional. 401 remains specified because the shared auth gate can still produce it.

2. Codegen runs clean from this spec alone (sdks/codegen/generate.py --spec <this openapi.yaml>, run in a scratch tree so no SDK output is committed here):

IR: 17 products, 60 operations, 67 schemas, version 0.1.0
go:   23 files -> sdk-go/
rust: 26 files -> sdk-rust/
ruby: 23 files -> sdk-ruby/

Emitted Go client:

// MintPublishToken — Mint a MoQ publish join-token (operationId: mintMoqPublishToken, POST /moq/publish/{ns}/{track}).
func (s *MoqService) MintPublishToken(ctx context.Context, ns string, track string) (*MoqJoinToken, error)
func (s *MoqService) MintSubscribeToken(ctx context.Context, ns string, track string) (*MoqJoinToken, error)

type MoqJoinToken struct {
	Ok         bool    `json:"ok"`
	RelayWsURL string  `json:"relayWsUrl"`
	JoinToken  string  `json:"joinToken"`
	ExpiresIn  int64   `json:"expiresIn"`
	Ns         string  `json:"ns"`
	Track      string  `json:"track"`
	Role       string  `json:"role"`
	Scope      string  `json:"scope"`
	Protocol   *string `json:"protocol,omitempty"`
}

Ruby (ruby -c sdk-ruby/lib/wave/moq.rbSyntax OK) and Rust (pub fn moq(&self) -> moq::Moq<'_>) register the product on the umbrella client identically.

3. The generated client reaches the real route. Driving the generated Ruby SDK against production with no credentials:

$ ruby -e 'require "wave"; Wave::Client.new(api_key: "…unauthenticated probe…").moq.mint_publish_token("demo-ns","cam-1")'
Wave::Error: HTTP 402

That is the correct outcome for an unauthenticated caller and proves the generated method builds and calls the right path.

What is NOT verified

  • A generated client has not minted a real token. That needs a moq:write-scoped API key, and no credentials were used for this change. The mint path is proven from the gateway side, but the receipt for "generated client → real joinToken" is still outstanding.
  • go build / cargo check on the generated trees were not run — both wanted to fetch modules from the network in the sandbox. CI runs these gates.
  • Codegen needs a companion one-line fix in sdks before it will run on main's spec at all — see below. That is a pre-existing break, not caused by this PR.

Pre-existing codegen break (separate PR)

generate.py currently dies on main's spec with TypeError: cannot use 'list' as a dict keyAttestation.sig / WaveAttestation.sig use the OpenAPI 3.1 nullable form type: [string, null], which parse_spec._field_type passes through as a Python list into every renderer's type map. Reproduced on the unmodified origin/main spec, so it predates this change. Fixed in a companion sdks PR; the codegen output above was produced with that fix applied.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Note

Low Risk
Spec-only changes to openapi.yaml and CHANGELOG.md; no gateway or relay runtime code. Contract risk is limited to generated clients and integrators relying on the new 402/x402 and MoQ mint shapes.

Overview
Adds the MoQ OpenAPI product so SDK/CLI codegen can target live join-token mint routes that were previously undocumented.

New HTTP operations: POST /moq/publish/{ns}/{track} (mintMoqPublishToken, moq:write) and GET /moq/subscribe/{ns}/{track} (mintMoqSubscribeToken, moq:read). Path params use MoqNamespaceParam / MoqTrackParam with ^[a-z0-9-]{1,64}$. Publish mint documents optional x-wave-declare-protocol for billing (ignored when unrecognized/unauthorized; never fails the mint).

Response and errors: MoqJoinToken (relayWsUrl, joinToken, expiresIn, bound ns/track, role, scope, optional protocol) with Cache-Control: no-store. Failures include 400 MOQ_JOIN_BAD_RESOURCE, standard auth/quota responses, reusable PaymentRequired (402) pointing at X402PaymentRequired / X402Accepts (402 body is not the normal Error envelope), and 503 MOQ_JOIN_UNCONFIGURED via MoqJoinUnconfigured.

The MoQ tag prose clarifies direct-to-relay media (not API gateway), token carriers (join query vs x-wave-moq-join), and pins wire transport to draft-ietf-moq-transport-18 while keeping this HTTP mint contract transport-independent. CHANGELOG records the same under Unreleased.

Reviewed by Cursor Bugbot for commit 8c8cbb2. Configure here.


Summary by cubic

Adds the MoQ join-token mint surface to openapi.yaml so SDKs and the CLI can mint publish/subscribe tokens for Media over QUIC. Defines endpoints, response schema, and the x402 payment challenge.

  • New Features

    • New MoQ tag with mintMoqPublishToken (POST /moq/publish/{ns}/{track}, moq:write) and mintMoqSubscribeToken (GET /moq/subscribe/{ns}/{track}, moq:read).
    • MoqJoinToken response (relayWsUrl, joinToken, expiresIn, ns, track, role, scope, optional protocol); publish accepts optional x-wave-declare-protocol.
    • Path params ns/track constrained to ^[a-z0-9-]{1,64}$; explicit 400/401/402/403/429/503 responses; reusable PaymentRequired using X402PaymentRequired/X402Accepts (not the Error envelope).
    • Wire session is not modeled; tag documents direct-to-relay flow and alignment with draft-ietf-moq-transport-18.
  • Migration

    • Handle 402 as the normal pay-per-use path and parse X402PaymentRequired (not Error); missing or unrecognized API keys may return 402 on these routes.
    • Never cache or log joinToken (Cache-Control: no-store); mint per session and carry it via the join query param or x-wave-moq-join header when connecting to relayWsUrl.

Written for commit 8c8cbb2. Summary will update on new commits.

Review in cubic

MoQ is live end-to-end but had no entry in openapi.yaml, so no SDK and no CLI
command could be generated for it. Adds the MoQ tag and both mint operations
(POST /moq/publish/{ns}/{track} -> moq:write, GET /moq/subscribe/{ns}/{track}
-> moq:read), the MoqJoinToken response schema, and ns/track path parameters
constrained to ^[a-z0-9-]{1,64}$ to match the gateway regex.

Specs the failures alongside the happy path: 400 MOQ_JOIN_BAD_RESOURCE, 401,
the 402 x402 challenge, 403, 429, and the fail-closed 503
MOQ_JOIN_UNCONFIGURED. Adds X402PaymentRequired/X402Accepts and a reusable
PaymentRequired response, because the 402 body is not the Error envelope --
its error member is a string and the normalized error object is nested under
error_detail. Verified against api.wave.online: both a missing key and an
unrecognized key return 402 on these routes, not 401.

The MoQ media session is deliberately not modelled -- it is not an HTTP
surface. The tag description covers the direct-to-relay flow, the join query
parameter / x-wave-moq-join header token carriers, and pins the surface to
draft-ietf-moq-transport-18 (draft-19, published 2026-07-06, not yet
deployed).

Closes #24

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8c8cbb2

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0d122155-8867-4a9d-a261-2da8672e5051)

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a0d735a9-90b4-4d7d-bdc5-f71647025b6d

📥 Commits

Reviewing files that changed from the base of the PR and between b08f786 and 8c8cbb2.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • openapi.yaml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/moq-openapi-surface
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/moq-openapi-surface

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

@yakimoto
yakimoto merged commit 781a933 into main Jul 25, 2026
14 checks passed
@yakimoto
yakimoto deleted the feat/moq-openapi-surface branch July 25, 2026 15:33
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.

Spec the MoQ surface in openapi.yaml — the only proven-live product has no spec

1 participant