2b86d71c1da6d4b31250c40de52539c6ab37e1a7
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2b86d71c1d |
test(audio-rooms): one listener's unsubscribe doesn't tear down others
One speaker, two listeners A and B. Push 3 frames (both receive), A unsubscribes, push 3 more frames, B's stream completes with all 6. If A's unsubscribe accidentally tore down the speaker's broadcast or B's subscribe (e.g. an over-eager session-wide cleanup in a future refactor), B would time out waiting for the second batch — and the test names that exact failure mode in the message. |
||
|
|
dcbe31d4c8 |
test(audio-rooms): late-joining listener doesn't replay history
Pin moq-lite-03's "from latest" subscribe semantics: a listener that joins after the speaker has been broadcasting for a while sees only new frames, not the pre-subscribe history. Phase 1: speaker pushes 5 early frames (bytes 0..4) with no listener attached. The relay does not buffer these in moq-lite-03. Phase 2: late listener subscribes. Phase 3: speaker pushes 5 more frames (bytes 100..104). The listener takes the first 5 frames it sees and we assert ALL of them carry bytes >= 100 — any contamination from phase 1 fails loudly with the offending bytes named. This pins the no-replay guarantee against a future regression to "buffer-and-replay" behaviour, which would change recovery latency characteristics for users joining mid-stream. |
||
|
|
d1b00dd346 |
test(audio-rooms): pin mute/unmute end-to-end
Verifies the broadcaster's mute path through a real moq-relay: - push two unmuted frames -> listener receives them - setMuted(true), push two muted frames -> listener never sees them - setMuted(false), push two more unmuted frames -> listener resumes Asserts the listener's flow contains exactly [0, 1, 2, 3] in order (the muted 50, 51 frames must be absent, not silently filled with zeros). This pins `if (muted) continue` in AudioRoomMoqLiteBroadcaster against an accidental "send a silent placeholder while muted" regression. Also factors the previously-private DriverCapture / StubEncoder helpers into a shared InteropFrameDriver.kt so the new test can reuse them without copy-pasting per file. |
||
|
|
8b7ad2ebe1 |
test(audio-rooms): pin moq-lite-03 not-found contract for early subscribe
The previous test `listener_subscribed_before_announce_receives_late_frames` validated a contract from older moq-rs versions: the relay would HOLD a SUBSCRIBE issued before any publisher announced, then resolve it once a publisher arrived. moq-lite-03 dropped that behaviour — the relay now rejects immediately with `subscribed error err=not found` and FINs the bidi without writing a SubscribeDrop body, which our session reader surfaces as `MoqProtocolException: subscribe stream FIN before reply`. Rename the test to `subscribe_before_announce_fails_with_not_found` and flip the assertion to pin the new contract. Acceptance message check is permissive: matches the current "FIN before reply" form AND a hypothetical future "not found" SubscribeDrop, so a relay change to explicitly Drop wouldn't silently regress this test. The class-level coverage doc loses the obsolete "subscribe-before-announce holds" claim and gains a note on why the test now flips. Verified end-to-end against moq-relay 0.10.25 + moq-auth running bare-metal (-DnestsInteropExternal=true): all five interop test classes (Auth, AuthFailure, AuthEndpoints, RoundTrip, MultiPeer) now pass — 13/13 cases green. |
||
|
|
da1c4d3968 |
fix(audio-rooms): advertise moq-lite-03 in WT CONNECT sub-protocols
Without `wt-available-protocols`, moq-relay (`web-transport-quinn`) falls back to the legacy in-band SETUP exchange (moq-lite-02) instead of selecting the moq-lite-03 sub-protocol from the ALPN-style negotiation header. Then the relay tries to decode our first post-CONNECT bytes as a SETUP_CLIENT message, hits an unknown control type, and closes the QUIC connection with `connection closed err=invalid value` — surfaced client-side as a stuck SUBSCRIBE that ends with `subscribe stream FIN before reply for id=0` (the bidi gets FIN'd because the whole connection is being torn down). Pass `wt-available-protocols: "moq-lite-03"` on the Extended CONNECT request, encoded as an RFC 8941 Structured Field List of strings (the header format mandated by draft-ietf-webtrans-http3-14 §3.3). With this, moq-relay logs `negotiated version=moq-lite-03 transport="quic"` and the SUBSCRIBE makes it into the relay's actual moq-lite session pump. Mechanism: web-transport-proto's `ConnectRequest::encode` reads `self.protocols` and writes them as a comma-separated list of bare strings under `wt-available-protocols`. The server side (web-transport- quinn) reads the same header into `request.protocols`, and moq-native's `QuinnRequest::ok()` picks the first match against its supported ALPN list (`moq-lite-04`, `moq-lite-03`, `moq-00`, `moqt-15`, etc.). On match, version selection happens via the WT sub-protocol response and the in-band SETUP is skipped — which is what moq-lite-03 expects. Default the factory list to `["moq-lite-03"]`. Callers that want a different version (or to disable sub-protocol negotiation entirely to talk to a SETUP-based server) override the constructor parameter. Bare-metal harness: NostrNestsHarness.startExternal() now skips the TCP probe of the moq-relay port. moq-relay binds UDP only; the Docker forwarder happens to also open TCP, but a directly-launched binary doesn't, so the previous `Socket(host, 4443)` probe failed with ConnectException. The QUIC handshake from the test surfaces a real transport problem if any. |
||
|
|
d00e406587 |
test(audio-rooms): -DnestsInteropExternal bypasses Docker
Adds a "bring your own stack" path to NostrNestsHarness so the interop
tests can run without a Docker daemon. With `-DnestsInteropExternal=true`:
- Skip the `docker compose up` that builds + boots
moq-auth + moq-relay
- Port-probe + /health-check the same 8090 / 4443 endpoints the
Docker path uses
- close() is a no-op — the caller owns the lifecycle
Useful in two situations:
1. Sandboxes / restricted CI without a Docker daemon (just run
`cargo install moq-relay` + `node moq-auth/dist/index.js`
yourself, then run gradle with the flag)
2. Fast iteration — the Docker path takes ~30 s to compile
moq-relay on first run; with this, you keep both processes
alive across many test invocations
Forward `nestsInteropExternal` (and `nestsInteropDebug`,
`nestsInteropMoqRev`) through the Test task so the property reaches
test workers; without that, the gate stays off in the worker JVM.
Also: `assertSpeakerReached` / `assertListenerReached` now log a "✘"
checkpoint with the rich state description before calling fail().
JUnit captures the assertion message in a separate section, but the
"standard output" tab is what most people read first when scanning
for a cause — so the chained-cause string now lands in both places.
|
||
|
|
5f90588fe4 |
test(audio-rooms): InteropDebug step logger + harness diagnostics
When an interop test fails today the report shows a generic
AssertionError pointing at a `runBlocking {` line — useless for
narrowing down which sub-step (mintToken, WT connect, ANNOUNCE,
SUBSCRIBE_OK, frame round-trip) actually exploded.
This adds a small InteropDebug helper that:
- prints a labelled "▶ start" / "✔ ok" / "✘ fail — Class: msg ⟵ Cause: msg"
trail per step (output gated on `-DnestsInterop=true` so the
default test run stays silent)
- walks chained `cause` so the surface message reveals the real
network / protocol error instead of the wrapping string
- pretty-prints NestsSpeakerState / NestsListenerState (Failed in
particular unwraps `cause` for the report)
Each test body in NostrNestsRoundTripInteropTest +
NostrNestsMultiPeerInteropTest now wraps connect / startBroadcasting /
subscribeSpeaker / await-frames in `InteropDebug.stepSuspending(...)`
so a failure points at the exact sub-step rather than the test method.
Harness diagnostics: when `start()` fails, capture
`docker compose ps` + recent logs for moq-auth / moq-relay / strfry
into the IllegalStateException message before tearing the stack down.
Without this, the test report only shows "exited with code 1" —
operators have to re-run by hand to find out which container
crashed.
Output is hidden by default; only surfaces when
`-DnestsInterop=true` (or the explicit `-DnestsInteropDebug=true`)
is set.
|
||
|
|
74d5e77a83 |
fix(audio-rooms): retry mintToken on transport hiccup + harness /health warmup
The remaining interop failures all root in the same window: a stale
keep-alive pool entry from one test class is reused on the FIRST POST
of the next test class, the connection RSTs as the request body
writes, and OkHttp's built-in retryOnConnectionFailure won't retry a
POST after any byte of the body has gone out. Same situation hits a
phone client whose Wi-Fi hands off mid-mint.
Two fixes, both production-shaped:
- OkHttpNestsClient.mintToken now wraps execute() in
executeWithTransportRetry(): one retry on SocketException /
EOFException / generic IOException. Request builders are
immutable, so the second pass opens a fresh connection cleanly.
HTTP error status codes (4xx / 5xx) and malformed responses are
NOT retried — they go to the caller as before.
- NostrNestsHarness now polls GET /health until it returns 200
after the port-probe succeeds. moq-auth's Node runtime opens its
listen socket before the request handlers are wired, so a POST
that arrives in that window can RST. Waiting for /health proves
the request pipeline is live, eliminating the SocketException
that hit the first test of every test class run after
AuthEndpoints.
Symptoms fixed:
- NostrNestsAuthFailureInteropTest.missing_authorization_header_is_rejected_401
-> SocketException
- NostrNestsRoundTripInteropTest.production_speaker_broadcasts_to_production_listener_via_real_relay
-> "Failed to reach http://127.0.0.1:8090/auth"
- NostrNestsMultiPeerInteropTest.* (3 tests, same root cause)
|
||
|
|
7e67c4655f |
fix(audio-rooms): share Docker harness across all interop test classes
Each interop test class was running its own NostrNestsHarness.start() in @BeforeClass and harnessOrNull?.close() in @AfterClass — meaning the Docker stack tore down + spun back up between every class. That sequence was both slow (~30 s Cargo build for moq-relay each time) and unreliable: leftover network state from the prior `down -v` was racing the next `up -d`, leaving moq-auth either unreachable (SocketException) or producing truncated 401 responses (EOFException on body.string()) for tests that ran after the first. Symptoms before this fix (first class wins; everything else fails): - NostrNestsAuthEndpointsInteropTest ✅ - NostrNestsAuthInteropTest → SocketException ❌ - NostrNestsAuthFailureInteropTest → EOFException ❌ - NostrNestsRoundTripInteropTest → "Failed to reach" ❌ - NostrNestsMultiPeerInteropTest → "Failed to reach" ❌ Fix: NostrNestsHarness.shared() returns a process-singleton. First caller does the docker compose up + port-probe; every subsequent caller reuses the same containers. Teardown is registered once via Runtime.addShutdownHook so the stack lives for the JVM's lifetime and dies cleanly at test process exit. All five test classes now call shared() in @BeforeClass; the @AfterClass blocks no longer call close() on the singleton (clearing the local reference is enough — the shutdown hook handles the real teardown). The original start() entry point is preserved for callers that want a per-call harness. |
||
|
|
13e84f276d |
fix(audio-rooms): clone kixelated/moq + run generate-certs in harness
The previous --recurse-submodules fix turned out to be moot: nostrnests's docker-compose-moq.yml references `./moq` as a build context, but the moq directory is NOT in the nostrnests repo and is NOT a submodule — each developer is expected to clone kixelated/moq into ./moq themselves before running compose. Confirmed against the upstream README + repo listing (nostrnests/nests has moq-auth/, nests-relay/, NestsUI-v2/ but no moq/ directory). Two new harness steps before `docker compose up -d`: - ensureMoqSource — clone https://github.com/kixelated/moq.git into <nests-cache>/moq on first run; fetch + checkout DEFAULT_MOQ_REVISION on every run so the build is reproducible. Override the pin via -DnestsInteropMoqRev=<sha-or-branch>. - ensureDevCerts — run dev-config/generate-certs.sh to produce the self-signed TLS chain moq-relay mounts read-only at /certs. The script is idempotent (bails when fullchain.pem exists) so we always invoke it; a chmod +x defensively handles Windows / odd CI checkouts. Reverted the spurious --recurse-submodules + submodule update path since the repo doesn't have submodules at all — the original `git clone` was correct, just incomplete. |
||
|
|
64859546b3 |
fix(audio-rooms): clone nostrnests with submodules so docker compose finds moq/
The harness was running plain `git clone`, but docker-compose-moq.yml references `./moq` (kixelated/moq-rs) and `./moq-auth` as build contexts via git submodules. Without --recurse-submodules the directories don't exist and `docker compose up -d` fails with: unable to prepare context: path '<cache>/nests/moq' not found Fix: clone with --recurse-submodules on first run, and sync submodules after every fetch+checkout so the working tree matches whatever revision pin the checked-out commit declares. |
||
|
|
bc43168032 |
chore(audio-rooms): post-moq-lite cleanup + KDoc + plan refresh
Tidy items now that the moq-lite swap is complete on both sides:
- Drop the no-op `supportedMoqVersions: List<Long>` parameter from
`connectNestsListener` / `connectNestsSpeaker`. moq-lite negotiates
via ALPN, no caller passed a value, and the project rule forbids
no-op back-compat shims.
- `NostrNestsRoundTripInteropTest` KDoc + comments now describe the
moq-lite framing path (one Subscribe bidi for `audio/data`, group
uni streams with `DataType=0` + GroupHeader + size-prefixed frames)
instead of the stale IETF "OBJECT_DATAGRAMs / SETUP" framing.
- `DefaultNestsListener` / `DefaultNestsSpeaker` KDoc now flags them
as IETF MoQ-transport reference impls — production uses
`MoqLiteNests*`. They stay around for the IETF unit-test suite.
- `audio-rooms-completion.md` Phase M5 / M6 / M7 marked **DONE** —
the plan was written before the moq-lite gap was discovered, so
it described the work as IETF-MoQ-publisher additions; the
moq-lite path lands the same outcome via a different protocol.
|
||
|
|
1887bd1fa7 |
test/refactor(audio-rooms): nostrnests wire-shape fixes + interop expansion (phase 4)
Wire-shape corrections discovered while scoping the interop test suite
against the real moq-rs relay:
1. WebTransport CONNECT path is now /<moqNamespace> (matches the
relay's claims.root prefix check). Previously hardcoded "/anon".
2. JWT travels in the ?jwt=<token> query parameter, not the
Authorization header — moq-rs only reads the query param. The
bearer-token path on QuicWebTransportFactory is now unused for
nests; left in place for non-nests WebTransport servers.
3. Harness `moqEndpoint` is the relay base URL only; the connect
helpers append /<namespace>?jwt=<token> themselves.
Interop test additions (all -DnestsInterop=true gated, default-skipped):
- NostrNestsAuthFailureInteropTest — locks in the moq-auth sidecar's
rejection paths (missing/wrong-scheme Authorization, NIP-98 signed
for the wrong URL, malformed namespace per the strict regex,
publish=true grant for any caller — sidecar does NOT gate by NIP-53
hostlist).
- NostrNestsAuthEndpointsInteropTest — /health, /.well-known/jwks.json
shape (must contain ES256/P-256), 404 on unknown route.
- NostrNestsMultiPeerInteropTest — multi-listener fan-out, multi-
speaker isolation, subscribe-before-announce. Code is wired through
production connectNestsSpeaker / connectNestsListener; will pass
once the moq-lite gap (below) is resolved.
Major finding documented in nestsClient/plans/2026-04-26-moq-lite-gap.md:
nostrnests's stack uses moq-lite (kixelated's variant), NOT IETF
draft-ietf-moq-transport which `:nestsClient` currently implements. The
two are wire-incompatible — single-string broadcast/track names vs. byte
tuples, different ANNOUNCE/SUBSCRIBE framing. The wire-shape fixes here
make the WebTransport CONNECT itself succeed, but the post-CONNECT MoQ
framing layer still needs a moq-lite codec before round-trip / multi-peer
tests can pass against real nests. Pursued as a separate phase.
|
||
|
|
0ac8c0f791 |
test(audio-rooms): production round-trip via real MoQ relay (phase 3/3)
Drives connectNestsSpeaker + connectNestsListener end-to-end against the
real nostrnests Docker stack. Speaker announces a track, listener
subscribes by pubkey, speaker pushes deterministic frames through
AudioRoomBroadcaster → MoQ → relay → listener.objects flow, and the
test asserts payload integrity + monotonic object ids.
Validates the wire shapes the Phase-2 refactor committed to:
- QuicWebTransportFactory + PermissiveCertificateValidator can
handshake against the relay's self-signed dev cert
- JWT minting + WebTransport CONNECT + MoQ SETUP all succeed
- The single-segment TrackNamespace `nests/<kind>:<host>:<room>`
matches the relay's `root` JWT claim
Single-keypair design sidesteps host-vs-audience auth policy so the
test stays focused on transport + protocol; a future dual-keypair
test can layer permissions on top.
Skipped by default — set -DnestsInterop=true to enable.
|
||
|
|
beec8204e5 |
refactor(audio-rooms): NestsClient API matches nostrnests reality (phase 2/3)
The Phase-1 interop harness exposed a substantial mismatch between our
production HTTP client and what the nostrnests reference server actually
exposes. This commit refactors `:nestsClient` and the wiring above it so
the production code path can talk to a real moq-auth + moq-relay.
| Aspect | Before | After (matches nostrnests/moq-auth/src/index.ts) |
|-----------|-------------------------------------------|--------------------------------------------------|
| Method | GET | POST |
| URL | `<base>/<roomId>` | `<base>/auth` |
| Body | none | `{"namespace":"nests/<kind>:<host>:<roomId>","publish":bool}` |
| Response | `{endpoint, token, codec, sample_rate}` | `{token}` only |
| Endpoint | from response | from event's `endpoint` tag (passed via `NestsRoomConfig.endpoint`) |
| NIP-98 | bound to GET URL | bound to POST URL + body hash |
Type changes:
- New `NestsRoomConfig` data class bundling (authBaseUrl, endpoint,
hostPubkey, roomId, kind). Built by the caller (UI / VM) from the
NIP-53 kind 30312 event before invoking connectNests*.
- `NestsRoomConfig.moqNamespace()` produces the exact format
moq-auth's NAMESPACE_REGEX expects: `nests/<kind>:<hex64>:<roomId>`.
- `NestsRoomInfo` deleted; replaced with a tiny `NestsTokenResponse(token)`
matching the real response shape.
- `NestsClient.resolveRoom(serviceBase, roomId, signer): NestsRoomInfo`
→ `NestsClient.mintToken(room, publish, signer): String`. The
publish flag drives the JWT claims (`get` for listeners, `put`
for speakers).
Wire path:
- `OkHttpNestsClient` now POSTs `<authBase>/auth` with a JSON body
and a NIP-98 Authorization header bound to (POST, url, body-hash).
- `connectNestsListener` / `connectNestsSpeaker` take `room:
NestsRoomConfig` instead of split (serviceBase, roomId), pass
`publish=false` / `publish=true` respectively, and use the room's
`endpoint` (not a server-returned one) for the WebTransport
connect. The minted JWT is the bearer token.
- `NestsListenerState.Connected` / `NestsSpeakerState.Connected` /
`Broadcasting` carry the `room: NestsRoomConfig` instead of the old
`roomInfo: NestsRoomInfo`.
- MoQ TrackNamespace for the room is now a single segment whose
bytes are `room.moqNamespace()` — the simplest mapping to the
relay's JWT claim check (`root: "<namespace>"`); Phase-3 round-trip
test will confirm and adjust if the relay expects a multi-segment
tuple.
Wiring above:
- `AudioRoomViewModel` constructor: replaces `(serviceBase, roomId)`
with `(room: NestsRoomConfig)`. Connector seam interfaces
(NestsListenerConnector, NestsSpeakerConnector) follow the same
shape.
- `AudioRoomViewModelFactory` (Android) takes `room: NestsRoomConfig`.
- `AudioRoomActivity` adds `EXTRA_AUTH_BASE_URL`, `EXTRA_ENDPOINT`,
`EXTRA_HOST_PUBKEY`, `EXTRA_KIND` Intent extras (was just service
+ roomId) and reconstructs `NestsRoomConfig` in onCreate. Drops
`EXTRA_SERVICE_BASE`.
- `AudioRoomJoinCard` reads `event.endpoint()` + `event.pubKey` +
`event.kind` in addition to `event.service()`; rooms missing any
of those are silently un-joinable (the event author didn't host
on a nests-compatible relay).
- `AudioRoomActivityContent` takes `room: NestsRoomConfig` in place
of (serviceBase, roomId) and threads it down.
Phase-1 ping test rewired to use the production `OkHttpNestsClient`
end-to-end against the real `/auth`, asserting we get back a
3-segment JWT.
Existing in-process tests updated for the new types: NestsConnectTest,
NestsSpeakerTest, AudioRoomViewModelTest. NestsRoomInfoTest renamed to
NestsRoomConfigTest with new cases for the namespace formatter and the
auth-URL helper. All 80 in-process tests still green.
Phase 3 (next) will add the full round-trip interop test that runs
production `connectNestsListener` + `connectNestsSpeaker` through the
real moq-relay — that's where MoQ wire-format assumptions (draft
revision, OBJECT_DATAGRAM layout, namespace tuple shape) get verified
or get followup audit findings.
|
||
|
|
3283d302fa |
test(audio-rooms): nostrnests interop harness + /auth ping (phase 1/3)
Brings up the nostrnests reference server (https://github.com/nostrnests/nests) locally via Docker Compose so we can drive `:nestsClient`'s production code against the real MoQ relay + NIP-98 auth sidecar. Mirrors the `:quic` `InteropRunner` pattern (aioquic Docker, opt-in via `-DinteropHost=…`): - Set `-DnestsInterop=true` to enable; default `:nestsClient:jvmTest` runs skip via JUnit `Assume.assumeTrue` (shown as <skipped>, not <failure>). - Repo cloned + cached at `~/.cache/amethyst-nests-interop/nests/`, pinned to the `DEFAULT_REVISION` (currently `main`; override via `-DnestsInteropRev=<sha>` to lock in for reproducibility). - `docker compose -f docker-compose-moq.yml up -d` brings up moq-relay (host 4443 TCP+UDP), moq-auth (host 8090), strfry (7777). Port-probes 4443 + 8090 with a 90 s timeout. - `close()` runs `docker compose down -v --remove-orphans`. Tests use `@BeforeClass`/`@AfterClass` to amortise the ~30-60 s spin-up across all cases in one class. Phase-1 ping test (NostrNestsAuthInteropTest): - Generates an ephemeral KeyPair / NostrSignerInternal via Quartz. - POSTs `<authBase>/auth` with `{"namespace":"nests/30312:<pubkey>:<roomId>", "publish":true}`, NIP-98 Authorization header signed for that exact (url, method, payload) tuple. - Asserts 200 + a `"token":"…"` JWT in the response body. Doesn't yet route through `OkHttpNestsClient` because the production client's wire shape (GET `<base>/<roomId>` returning `{endpoint, token, codec, sample_rate}`) does not match nostrnests' actual API (POST `<base>/auth` with `{namespace, publish}` body, returning just `{token}` — endpoint comes from the NIP-53 event's `endpoint` tag instead of the HTTP response). Phase 2 of this audit refactors production to match; this test documents the divergence on the wire so the refactor has a clear target. Verified: harness compiles clean; default `:nestsClient:jvmTest` shows the test as <skipped> (not <failure>) when `nestsInterop` property is unset. Files: - nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsHarness.kt - nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsAuthInteropTest.kt |
||
|
|
46742636c7 |
feat(quic): Phase L — wire QuicWebTransportFactory into nestsClient
Replace the Kwik stub in :nestsClient with a pure-Kotlin QUIC + WebTransport
adapter built on top of :quic.
- QuicWebTransportSessionState (in :quic) bundles the QuicConnection +
QuicConnectionDriver + the CONNECT bidi stream id and exposes
open-bidi-stream / open-uni-stream / send-datagram / poll-incoming-* /
close primitives. Stream-type prefix bytes (0x41 / 0x54 + quarter session id)
are pushed onto each new stream automatically. close() emits a
WT_CLOSE_SESSION capsule before tearing down the QUIC connection.
- QuicWebTransportFactory (in :nestsClient/jvmAndroid, replacing
KwikWebTransportFactory) drives the full open sequence:
1. UDP connect + QuicConnection.start
2. wait until handshake completes (Status.CONNECTED)
3. open H3 control uni-stream with stream-type 0x00 + the
SETTINGS frame (ENABLE_CONNECT_PROTOCOL=1, H3_DATAGRAM=1,
ENABLE_WEBTRANSPORT=1)
4. open the Extended CONNECT bidi: HEADERS frame carrying
:method=CONNECT, :protocol=webtransport, :scheme=https,
:authority, :path, optional Authorization: Bearer
5. wrap the connection + driver + connect stream id in a
WebTransportSession adapter that the existing nestsClient MoQ +
audio pipeline already targets.
- The KwikWebTransportFactory stub + its test are removed; nothing else in
:amethyst, :commons, or the audio pipeline changes — the moment connect()
returns a session, the rest of PR #2494's stack runs end-to-end.
- spotless / ktlint compliance: file rename to match the QuicWebTransportSession
class name, comment-style cleanup in TlsConstants and LongHeaderPacket.
Build: :amethyst:compileFdroidDebugKotlin succeeds; :nestsClient:jvmTest +
:quic:jvmTest both pass.
https://claude.ai/code/session_01EC1tfXfap8k8GyKvrxkxZx
|
||
|
|
4ead4ccd5c |
feat(nestsClient): WebTransport abstraction + fake + Kwik stub
Phase 3b-1 of the Clubhouse/nests integration. Lands the [WebTransportSession] abstraction the MoQ layer (Phase 3c) will code against, so MoQ framing + tests can develop in parallel with the real Kwik-based transport integration (deferred to Phase 3b-2). commonMain: - `WebTransportSession` — bidi/uni stream access, datagrams, close. - `WebTransportBidiStream` / `WebTransportReadStream` / `WebTransportWriteStream` — minimal read/write surface. - `WebTransportFactory.connect(authority, path, bearerToken)` for opening sessions. - `WebTransportException(kind, ...)` with four canonical failure modes (HandshakeFailed, ConnectRejected, PeerClosed, NotImplemented) so UI code doesn't need to know about library-specific exceptions. - `FakeWebTransport.pair()` — in-memory, fully-wired client/server pair for unit-testing MoQ framing without touching a real QUIC stack. jvmAndroid: - `KwikWebTransportFactory` stub that throws `WebTransportException(NotImplemented)` at `connect()`. Doc spells out the handshake sequence (QUIC dial → H3 SETTINGS → `:method=CONNECT :protocol=webtransport` Extended CONNECT → 2xx) so Phase 3b-2 can drop the real implementation in without touching callers. Tests: - `FakeWebTransportTest` — datagram round-trip both directions, bidi-stream write visible on peer side, close() flips isOpen. - `WebTransportExceptionTest` — kind + message + cause preserved. - `KwikWebTransportFactoryTest` — `connect()` currently fails with the NotImplemented sentinel, so Phase 3b-2 can replace this test when the real handshake lands. https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D |
||
|
|
933b522273 |
feat(nestsClient): NIP-98 auth + nests room-info client
Phase 3a of the Clubhouse/nests integration. Adds a new KMP module `nestsClient` (Android + JVM targets) that owns the HTTP control plane for talking to a nests audio-room backend. No transport/audio yet — that arrives in 3b with WebTransport + MoQ. Surface: - `NestsAuth.header(signer, url, method)` signs a kind 27235 event via Quartz's existing HTTPAuthorizationEvent and returns a ready-to-use `Authorization: Nostr <base64>` header value. - `NestsRoomInfo` data class + tolerant JSON parser (ignores unknown fields so newer nests server revisions don't break older clients). - `NestsClient.resolveRoom(serviceBase, roomId, signer)` calls `<serviceBase>/<roomId>` with the signed NIP-98 header and returns the MoQ endpoint + token the audio layer will need. - `OkHttpNestsClient` (jvmAndroid) is the default implementation shared between Android and desktop JVM. Tests: - `NestsRoomInfoTest` (commonTest) covers full/minimal payloads, unknown-field tolerance, missing-endpoint rejection, and URL construction edge cases. - `NestsAuthTest` (jvmTest) signs a real 27235 event, decodes the base64 back through Quartz's JacksonMapper, and asserts the signature verifies and the url+method tags bind correctly. https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D |