feat(terminal): server-resolved short codes for live session invites - #7
Merged
Conversation
…-guest revoke proof
… edge cases - Grant no longer carries id/kind: nothing read them, and both were only ever written, tripping clippy dead_code. - SessionState.invite_token: its last reader was removed with legacy-token grants; dead in-memory state must not linger next to the real source of truth in terminal_session_grants. - create_code: fetch_optional the visibility lookup so a session ending between the host check and this query 404s instead of 500ing. - comment fixes: correct the redeem-route-ordering rationale (matchit prefers static segments regardless of order), note why the guest grant has no expiry.
require_active_session_host catches ended sessions before create_code's fetch_optional visibility lookup runs; only the row-disappears-mid-request race reaches that branch, and that isn't worth simulating.
… reconcile legacy grants - resolve_join_grant excludes kind=short_code so a spoken code can't skip /redeem, bypassing the guest-grant audit trail and the redeem limiter - rotate_short_code takes a FOR UPDATE lock on the session row first, serializing concurrent regenerate calls against idx_tsg_one_live_code - server boot reconciles orphaned invite_token rows left by a rollback/roll-forward cycle past migration 037's one-shot backfill; ON CONFLICT DO NOTHING makes it safe under a rolling deploy - rate_limit::check_user_budget is the one implementation of the warn-and-429 shape, used by both session_codes handlers and the existing per-user middleware - redeemed guest grants are attributed to the session host, not the redeeming guest, so host-facing tooling can find them - delete a vacuous migration test, tighten a FORBIDDEN assertion, add get_my_session_key status-code regression coverage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Server half of VoltiusApp/voltius#65: a server-resolved, short-lived code that a guest can read aloud or type by hand to join a live terminal session.
Why
The invite artifact carries a session-lifetime
invite_token, so wherever it lands — chat scrollback, clipboard managers, OS handler logs — it stays valid until the session ends. A short-TTL code leaves a dead credential in those places instead. Secondarily, a 69-charactersessionId:tokencannot be read down a phone line;K7M2-P9QX-3Bcan.What changed
Join credentials move from the single
terminal_sessions.invite_tokencolumn into aterminal_session_grantstable (migration 037), with three kinds:legacy_token— backfilled from the existing column, never expires. This is what already-deployed clients present.short_code— 10 symbols of Crockford base32 (50 bits, grouped4-4-2), 10-minute TTL, one live per session enforced by a partial unique index.guest— minted per redemption, never expires, individually revocable.Two endpoints:
POST /v1/terminal-sessions/:id/codeinvite_linksessions only{ code, expires_at }once.POST /v1/terminal-sessions/redeemguestgrant, returns{ session_id, invite_token }.The redeemed secret keeps the existing 32-hex shape, so
my-key?invite_token=and the WebSocket upgrade are untouched. A singleresolve_join_grantreplaces the token check that was previously written out separately inget_my_session_keyandis_authorized_participant.Rate limits: 30 mints/hour per host, 20 redemptions/hour per user. Unknown, malformed, expired and revoked codes all return 404, so no response distinguishes a real code from a wrong one, and the submitted code is never logged.
Backward compatibility
terminal_sessions.invite_tokenis still written, just never read, so rolling back to the previous binary works. Migration 037 backfills every live invite-link session, andsrc/db.rsre-runs that backfill idempotently at startup so a rollback followed by a roll-forward cannot strand a session. BaresessionId:tokenandvoltius://join?s=…&t=…both keep working.Notable fixes found during review
my-keyand skip redemption — no guest grant written, nothing to revoke, no record of who joined, and the redeem limiter bypassed. Now excluded explicitly, with tests at both call sites.SELECT … FOR UPDATEon the session row first.Verification
cargo clippy -- -D warningsclean crate-wide.invite_tokenjoin still works;Deployment
Unlike #69, this is not live until the
voltius-serverimage is rebuilt and redeployed. Redeploy needs--env-file .env.dockhandor secrets come out blank. Verify against the dockervoltius-dbcontainer, not Neon:Known gap
Nothing writes
revoked_atexcept code rotation, so there is no API to revoke one guest. The table makes per-guest revoke possible, but a guest admitted inside the 10-minute window keeps access until the session ends. The TTL bounds who can acquire access, not who keeps it.Three non-blocking minors for later: the startup reconciliation panics the boot on a transient DB error where a warn-and-continue would be safer; its anti-join full-scans
terminal_sessionson every boot; and one comment cites host-facing tooling that does not exist yet.The client half of #65 is not in this PR.