feat(auth): Sign In via BossConsole account in the Share menu#295
Conversation
Adds account sign-in as the 4th item in the Share menus, against BossConsole's shared Supabase backend (one user pool) over raw GoTrue REST — no Supabase SDK, reusing the existing ktor client. Magic-link flow: enter email → GoTrue emails a link → BossConsole's redirect function emits a bossterm://auth/verify deep link → BossTerm verifies the token and persists the session. Hidden in embedded builds (serverName != "bossterm"); the BossConsole backend half is risa-labs-inc/BossConsole#787. New compose-ui/.../auth/ package: - BossAccountManager: StateFlow<AccountState>; sendMagicLink (POST /auth/v1/otp, redirect_to=bossterm://auth/verify), handleAuthDeepLink (POST /auth/v1/verify, token_hash + verbatim type), signOut, refresh-on-startup. - AuthModels / AuthStorage: DTOs + parseAuthDeepLink + error mapping; session in ~/.bossterm/auth.json (atomic write, chmod 600 on POSIX, never logged). - DeepLinkHandler / DeepLinkSocket: new bossterm:// support — macOS setOpenURIHandler (installed before AWT), Windows/Linux argv + single-instance loopback forwarder (bounded read, constant-time secret check), -Dbossterm.debug.deeplink dev hook. - WindowsProtocolRegistrar: HKCU scheme registration on packaged Windows launches. - SignInWindow: email → sent (60s resend cooldown) → verifying → signed-in. Wiring: Main.kt (fun main(args) + DeepLinkHandler.install first thing); macOS CFBundleURLTypes (build.gradle.kts) + .deb .desktop patch (MimeType + %U) + snap x-scheme-handler/bossterm; "Sign In…" at all three Share menu sites; a deferred "Signed in as …" toast. Generated with [Claude Code](https://claude.com/claude-code)
Review:
|
…e Sign In dialog Complete the Share-menu sign-in feature with the affordances surfaced while testing against the live backend: - Paste-link fallback: when the bossterm:// deep link can't open the app (dev builds, unregistered scheme, another device), the Sign In dialog now offers a collapsible "paste the sign-in link" field. parseAuthInput() leniently extracts the token from the clean deep link, the branded email's redirect URL, or the raw confirmation URL (percent-encoded or not), mirroring BossConsole's manual flow; BossAccountManager.verifyPastedLink() redeems it and surfaces a friendly error when no token is found. - Scrollable Sign In dialog: content column scrolls and the window is taller (420 -> 480 dp) so the expanded paste field/Verify button is never clipped. - Account row in the Share menu: once signed in, the 4th Share item shows the account email instead of "Sign In…", with a person glyph drawn as a Java2D Swing Icon. (An emoji in the label corrupted the AWT popup's font metrics and garbled every item — the icon is vector-drawn so it can't.) Adds parseAuthInput unit tests covering the deep link, confirmation URL, the percent-encoded redirect URL, token_hash precedence, and junk -> null. Generated with [Claude Code](https://claude.com/claude-code)
Code Review — feat(auth): Sign In via BossConsole accountThorough, well-documented PR. The pure-logic core (parsing, error mapping, storage) is clean and well-tested, the threading model around the toast/ 🐛 Bugs / behavioral issues1.
Not a crash (graceful degradation), but the doc comment ("the first instance listens") doesn't match the behavior. Consider: only start the listener if no live listener already owns the port file (probe it first), or at least reconcile the comment with "last launch wins." 2. ⚙️ Minor
✅ Strengths
🧪 TestsGood coverage of the pure layer (deep-link/paste parsing incl. the legacy Overall LGTM pending a look at finding #1 (or just aligning the doc with reality). Nice work. Reviewed with Claude Code. |
…d tokens Two review findings on the Sign In flow: 1. Deep-link routing was "last launch wins", and a later launch's exit tore it down. DeepLinkHandler.install() called DeepLinkSocket.startPrimaryListener() on every non-forwarding launch, so each one overwrote ~/.bossterm/deeplink.port with its own port+secret AND registered deleteOnExit — so deep links routed to the LAST instance, and when it exited the port file was deleted even though earlier instances were still running (a subsequent link then found no file and spawned a new window). Now startPrimaryListener probes the advertised port first: if a live listener already owns routing it leaves it be; a refused connect means a stale file (dead owner), which it clears before claiming ownership. The first live instance owns routing for as long as it runs — matching the doc comment. 2. parseAuthInput / paramValue used URLDecoder.decode, whose form-encoding semantics turn '+' into a space — a generic URI query (RFC 3986) treats '+' as literal, and a standard-base64 token_hash can contain '+', so a pasted token could be silently corrupted. Added decodeQueryComponent (pre-escapes literal '+' to %2B, then decodes) and use it at all three query-decode sites (parseAuthInput's whole-string pass, paramValue, and parseAuthDeepLink — same latent bug, just less likely to hit). Tokens never contain spaces, so nothing is lost. Tests: AuthModelsTest gains two regression guards (a literal '+' survives both the OS deep-link and the pasted-URL paths). 19 auth tests green. Generated with [Claude Code](https://claude.com/claude-code)
Review:
|
Two more review findings on the Sign In flow: 1. No HTTP timeout — a stuck request stranded the UI forever. The CIO client was built with only expectSuccess=false, so verify()/sendMagicLink() could await a hung server indefinitely, leaving the dialog on "Signing you in…" / "Check your email" with no way back. Installed HttpTimeout (request 30s / connect 15s / socket 15s), matching DesktopUpdateService's API client; a stuck request now resolves to the existing mapAuthError(0, …) path. 2. paramValue matched token= as a bare substring, so it latched onto the token= inside access_token=/refresh_token=. A pasted implicit-flow callback fragment (#access_token=…&refresh_token=…&token=…) yielded the access token instead of the verify token. Now the name= must sit at a real parameter boundary (start-of-string, or after ?/&/#/whitespace), scanning later occurrences if the first is embedded. Only the lenient paste path was affected — the strict OS deep-link path always parsed by URI. Tests: AuthModelsTest gains two regression guards (access_token/refresh_token are not mistaken for the hash; token is still found when only refresh_token precedes it). 21 auth tests green. Generated with [Claude Code](https://claude.com/claude-code)
|
Review: feat(auth): Sign In via BossConsole account in the Share menu Thorough, well-documented PR — the comments carry the why (RFC 3986 Bugs / correctness
Security
Robustness / minor
Test coverage Pure-logic coverage is genuinely good — deep-link parsing, Style Matches the codebase. Nit: the auth wiring in Overall: solid, security-conscious work with the risky bits clearly flagged. Nothing blocking; the temp-file permission window and the |
Addresses the outstanding code-review concerns on the Sign In flow. Correctness / data race: - verify() no longer recovers the toast email by re-reading the shared state.value after adoptSession() — on the multi-threaded IO scope a concurrent transition could drop or stale it. adoptSession() now returns the adopted email and verify() emits that directly (only when non-blank). restoreSession() still ignores it, so the "interactive-verify-only" emission contract is unchanged. - paramValue()'s value scan now also stops at '?' (a nested '?' never occurs inside a real hex/base64url token, so it can only trim junk like token=abc?foo=bar). Security / storage: - The staging temp file is chmod-600 while still EMPTY (new newOwnerOnlyTempFile), before any token bytes are written. java.io.File.createTempFile honors umask (commonly 0644), so writing first left the token briefly group/world-readable until the restrict ran. The existing test only checked the FINAL file. - save() stages the temp in the target's own directory via file.absoluteFile.parentFile, so a bare-filename File can't fall back to the system temp dir and turn the ATOMIC_MOVE into a failing cross-device move. - DeepLinkSocket's trust model is now documented explicitly: loopback-only + chmod-600 port file means the secret is readable only by this OS user; a same-user process is already inside the trust boundary and a forged verify only redeems a token it must already hold (no escalation). Robustness: - signOut() flips state to SignedOut synchronously (instant UI from the Compose onClick) and moves the AuthStorage.load()/clear() disk IO + server revoke into the launched coroutine, off the UI thread. - DeepLinkSocket gains an explicit stop() teardown (closes the listener socket, drops the port file); the daemon listener remains an app-lifetime singleton in production. Tests: - New DeepLinkSocketTest exercises the single-owner / forwarding state machine over real loopback sockets with an injectable port file: claim-on-empty + forward round-trip, no-file forward, stale-file cleanup, no-usurp of a live listener, wrong-secret rejection, and the non-bossterm scheme filter. - AuthModelsTest gains guards for the '?' value boundary and the empty-then-600 staging temp file. 29 auth tests green. Generated with [Claude Code](https://claude.com/claude-code)
The earlier reviews noted a deep link that LAUNCHES the app verifies in main() before any window subscribes to signInEvents (replay = 0), so the emit was dropped and the cold-start sign-in never toasted (the user was signed in, just no confirmation). BossAccountManager now routes a completed-sign-in email through emitSignIn(): if a collector is live it emits as before; otherwise it stashes the email in an AtomicReference for the first TabbedTerminal collector to drain exactly once on startup. Using replay = 1 instead would re-toast the stale email on every newly opened window; draining once avoids that. Tests: AuthModelsTest guards the stash/drain-once contract and the blank-email no-op. 31 auth tests green. Generated with [Claude Code](https://claude.com/claude-code)
…SCII Two minor review nits: - GoTrueError.errorCode was parsed but never read (text() uses only the human-readable spellings). Removed it; ignoreUnknownKeys still skips the field during decode. - readBoundedLine decodes byte-per-char; documented the ASCII assumption (the wire format is a hex secret + percent-encoded bossterm:// URI, so no multibyte input is expected). Generated with [Claude Code](https://claude.com/claude-code)
Code Review: Sign In via BossConsole account (PR #295)Reviewed the full auth package, deep-link plumbing, menu wiring, packaging changes, and tests. This is a careful, well-documented piece of work — the kdoc on each file explains why, the security model is stated explicitly, and the commit history shows several review findings already chased down (single-owner routing, literal- Strengths
Minor issues / suggestions
Test coverageStrong for what's testable in-process: deep-link parsing (incl. VerdictHigh quality and low risk. Nothing here blocks merge in my view — items 1, 2, and 4 are the most worth a quick follow-up (toast delivery, Windows re-registration cost, and tracking the refresh-on-401 TODO + RPM gap). Review generated with Claude Code. |
Review follow-up — all findings addressedThanks for the thorough pass. Resolved across Bugs / correctness
Security
Robustness
Test coverage
Also closed from the earlier reviews
Intentionally left (with rationale)
|
PR Review: Sign In via BossConsole account in the Share menuThorough, well-documented change. The security reasoning is laid out explicitly in the code, the tricky edge cases (base64 👍 Strengths
🐛 Potential issues1. portFile.writeText("${server.localPort}\n$secret\n")
restrictToOwner(portFile)On a fresh 2. buf.append(b.toChar())The URI is sent as UTF-8 ( 3. Cold-start ⚡ Performance
🎨 Nits
🧪 Test coverageGood for the pure logic and the socket. Acknowledged gaps that are fine to ship with, given they're called out in the description: the network half ( Overall a solid, security-conscious implementation. Addressing #1 (secret-file perms window) is the most worthwhile follow-up, since it undercuts the same protection you took pains to get right elsewhere. Note: I reviewed the diff statically and did not run the build (per CLAUDE.md the app isn't run during review); the PR states the full build and the 🤖 Generated with Claude Code |
|
Review: feat(auth): Sign In via BossConsole account in the Share menu Reviewed all 16 files. This is a high-quality, unusually well-documented PR — the security reasoning (chmod-600 staging temp, constant-time secret compare, RFC-3986 plus-preservation, boundary-anchored token scanning) is sound and backed by targeted tests. The single-instance forwarding state machine and its regression tests are particularly good. Below are findings, mostly minor; nothing blocks merge. Bugs / correctness
Design / follow-ups (partly acknowledged in comments)
Security
Test coverage Strong for the pure/stateful logic: deep-link parsing (token vs token_hash, verbatim type, literal plus, access_token/refresh_token decoy rejection, nested question-mark), error mapping, storage round-trip + corruption + 600-perms, and the full socket claim/forward/stale/no-usurp/wrong-secret/wrong-scheme matrix. The network half ( Style / conventions
Overall: clean, defensive, and well-tested where it matters. My only address-before-relying-on-it item is #4 (refresh-on-401), and that is a future-consumer concern rather than a defect in this PR. Generated with Claude Code |
Adds Sign In as the 4th item in the Share menus, against BossConsole's shared Supabase backend (one user pool) over raw GoTrue REST — no Supabase SDK, reusing the existing ktor client. Magic-link flow: enter email → GoTrue emails a link → BossConsole's redirect function emits
bossterm://auth/verify?token_hash=…&type=…→ BossTerm verifies and persists the session. Hidden in embedded builds (serverName != "bossterm").Backend half: risa-labs-inc/BossConsole#789 (closes #787). Until that ships, the email is BOSS-Console-branded and its link opens BossConsole — the client here is complete and testable via a manually-opened deep link.
New
compose-ui/.../auth/packageStateFlow<AccountState>;sendMagicLink(POST /auth/v1/otp,redirect_to=bossterm://auth/verify),handleAuthDeepLink(POST /auth/v1/verifywithtoken_hash+ verbatimtype),signOut, refresh-on-startup.parseAuthDeepLink+ error mapping; session in~/.bossterm/auth.json(atomic write, chmod 600 on POSIX, never logged).bossterm://support: macOSsetOpenURIHandler(installed before AWT), Windows/Linux argv + single-instance loopback forwarder (bounded read, constant-time secret compare),-Dbossterm.debug.deeplinkdev hook.Wiring
Main.kt(fun main(args)+DeepLinkHandler.installfirst thing); macOSCFBundleURLTypes;.deb.desktoppatch (MimeType +%U); snapx-scheme-handler/bossterm; "Sign In…" at all three Share menu sites; a deferred "Signed in as …" toast.Security (reviewed)
Anon key is byte-identical to BossConsole's already-public fallback (RLS is the gate); tokens chmod-600 + never logged; deeplink params go into a JSON body (no injection); socket secret is 128-bit, owner-only, constant-time-compared.
Tests / verification
:compose-ui:desktopTestauth suite (deeplink parsing incl.token↔token_hash+ verbatimtype, error mapping, storage round-trip/corruption, 600-perms assertion) green; full build green.Manual before release (OS-delivery has no automated coverage): packaged
.appcold-startopen "bossterm://auth/verify?token_hash=…&type=magiclink"; a Linux.deb.desktopcarries the MimeType +%U; Windows HKCU registration. RPM scheme registration is a known gap (jpackage.rpmisn't repacked).Generated with Claude Code