The earlier integration audit identified the gaps; this is the
how-to-build-them. Split across four files plus an index so each
review / commit stays small:
- 2026-04-26-tier-plans-index.md — top-level pointer + sequence
dependencies + what's deliberately out of scope.
- 2026-04-26-tier1-coding-plan.md — listener counter, presence
aggregation, augmented kind-10312 tags (publishing/onstage),
live chat (kind 1311), reactions (kind 7 / 9735), edit + close
+ scheduled rooms, role parsing + promote/demote, hand-raise
queue, kick (kind 4312). Concrete file-level wiring for each
step + suggested commit order (six independent PRs).
- 2026-04-26-tier2-coding-plan.md — participant grid,
per-avatar context menu (follow / mute / zap / promote / kick),
zap entry points (room + speaker), share-via-naddr.
- 2026-04-26-tier3-coding-plan.md — room theming PARSER ONLY
(graceful fallback for themed rooms; full theming behind a
later phase), background-audio + wake-lock audit checklist.
- 2026-04-26-tier4-coding-plan.md — moq-auth token re-mint on
long sessions and moq-lite Connection.Reload-equivalent
reconnect with backoff. Step 1 is subsumed by Step 2 once the
reconnect path is in.
No code changes — pure docs. Each plan names exact file paths,
new types, reused helpers, strings, tests, and call-out risks so
an implementer (or follow-up agent) can pick up Step N without
re-deriving the surrounding context.
6.0 KiB
Tier 4 — coding plan (token refresh, moq-lite reconnect backoff)
Tier 4 of 2026-04-26-nostrnests-integration-audit.md. Pure
infrastructure hardening — no user-visible UX changes. Covers the
items the audit flagged as "MAYBE GAP" or "worth confirming".
Step 1 — Token re-mint before expiry on long sessions (#16)
moq-auth tokens live 600 s and there's no refresh endpoint. A listener / speaker that stays in a room past the 10-minute mark needs to mint a fresh JWT and reopen the WT session — otherwise the moq-rs relay starts rejecting per-track auth checks.
Audit first
- Walk
MoqLiteSession+MoqLiteNestsListener/MoqLiteNestsSpeakerto confirm the JWT is only consulted at WT CONNECT time (it is — moq-rs's auth check is per-session, not per-OBJECT). That means the only thing that needs re-minting is when the WT session itself dies and we reconnect — see Step 2. - If the user stays in a room for hours without a transport blip,
the relay never re-checks the JWT, so we don't strictly need a
pro-active re-mint. Confirm against moq-rs: does the relay
drop the session when its claims
exppasses? If yes → step 2's reconnect path naturally re-mints.
If a pro-active re-mint IS needed
Implement in MoqLiteNestsListener / MoqLiteNestsSpeaker:
- Parse the JWT's
expclaim from the response — extendOkHttpNestsClient.mintTokento return(token, expiresAt: Long)instead of justtoken. - Schedule a coroutine in
connectNests*that fires(expiresAt - now - 60s)before expiry and calls a privatereauthorize()that mints a fresh token and...- either silently passes it to a (yet-to-exist) moq-lite "AUTH_REFRESH" message — moq-lite doesn't have one today,
- or quietly tears down the current WT session + reconnects (matching the reconnect path in Step 2).
Open question
Does moq-rs honour a re-handshake on the same session? Almost
certainly not — there's no in-band auth message. So the pro-active
path collapses into "trigger a reconnect ~60 s before exp".
Recommendation
Treat this step as subsumed by Step 2 — once the reconnect
backoff handles transport drops, an exp-driven self-disconnect
is just another trigger. Schedule:
val refreshAt = expiresAt - now - REFRESH_LEAD_MS // 60_000
scope.launch {
delay(refreshAt.coerceAtLeast(0))
triggerReconnect(reason = "JWT expiry")
}
in connectNestsListener / connectNestsSpeaker, where
triggerReconnect(...) is the same entry point Step 2 builds.
Step 2 — moq-lite Connection.Reload-equivalent reconnect with backoff (#17)
The JS reference uses
delay: { initial: 1000, multiplier: 2, max: 30000 }
when the WT session drops. Confirm Amethyst's MoqLiteSession
has equivalent backoff before adding it.
Audit
nestsClient/.../moq/lite/MoqLiteSession.kt— does any flow on it actually reconnect on transport failure? Or does the session surfaceClosedand leave it to the caller (theNestsListener/NestsSpeakerconnectors) to retry?nestsClient/.../NestsConnect.kt—connectNestsListener/connectNestsSpeakerwalk the HTTP→WT→moq-lite handshake once and return a Listener/Speaker. No retry today.
The audit will almost certainly conclude: no auto-reconnect exists. Add it.
Design
Add at the orchestration layer (connectNestsListener /
connectNestsSpeaker), not inside MoqLiteSession. The session is
"this connection"; reconnect is "open another connection".
nestsClient/.../NestsReconnectPolicy.kt(NEW) — pure data:data class NestsReconnectPolicy( val initialDelayMs: Long = 1_000, val multiplier: Double = 2.0, val maxDelayMs: Long = 30_000, val maxAttempts: Int = Int.MAX_VALUE, )nestsClient/.../NestsListener.kt— extend the state machine:- new
NestsListenerState.Reconnecting(attempt: Int, delayMs: Long) - on the underlying session emitting
Closed, walk the reconnect flow (mint fresh JWT → open WT → wrap in MoqLiteSession → re-issue every active SubscribeHandle).
- new
nestsClient/.../NestsSpeaker.kt— equivalent on the speaker side. Re-publish the broadcast suffix automatically; transparently resume capture after the new session opens.
Test
- New
nestsClient/src/jvmTest/.../moq/lite/MoqLiteReconnectTest.kt— drive aFakeWebTransportthat closes after the first frame, then assert the listener auto-reconnects withininitialDelayMs + jitter. - Update one of the interop tests (round-trip) to deliberately kill the WT session mid-broadcast and verify the listener resumes receiving frames.
Risk
- Re-issuing every active SubscribeHandle has to preserve the
consumer-side
Flow<MoqObject>so app code doesn't notice. Achievable by buffering the handle's flow upstream of the per-session adapter — likely aMutableSharedFlowper handle that the per-session pump emits into.
Suggested commit order
- Audit + notes: open a small text note documenting whether
the relay drops sessions on JWT
exp(Step 1 dependency) — a ~2-paragraph file innestsClient/plans/. NestsReconnectPolicy+ state machine extension (Step 2, listener side first).- Auto-reconnect tests against
FakeWebTransport. - Speaker-side reconnect (Step 2 second half).
- JWT-expiry-driven reconnect (Step 1, plumbing only — uses the Step 2 entry point).
Each step is independently merge-safe; the listener-only path is useful even if the speaker reconnect ships later.
Out of scope for Tier 4
- WebSocket fallback (
useWebSocket: true) — only matters for browser clients without WebTransport. Android has WT via QUIC, so not a gap. - Multi-track / video / fetch — confirmed not used by nostrnests.
- Bitrate probes — not used by nostrnests.
- Per-IP rate-limit smoothing for
/auth— already handled at the user-action level (one mint per join). No need for client-side throttling.