From 5ddf1412b3e447d1db933795bc227a89a4274209 Mon Sep 17 00:00:00 2001 From: zak <36251971+zrm625@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:08:01 -0400 Subject: [PATCH] Clarify desktop Rivet sign-in and retain rejected sessions --- apps/host-selfhost/docs/rivet.md | 4 ++- .../src/rivet-setup-state.test.ts | 8 +++++ apps/host-selfhost/web/rivet-setup-panel.tsx | 19 ++++++++---- apps/host-selfhost/web/rivet-setup-state.ts | 9 +++++- e2e/selfhost/rivet-setup-ui.test.ts | 29 +++++++++++++++++-- 5 files changed, 58 insertions(+), 11 deletions(-) diff --git a/apps/host-selfhost/docs/rivet.md b/apps/host-selfhost/docs/rivet.md index 613e962496..73d9af8c14 100644 --- a/apps/host-selfhost/docs/rivet.md +++ b/apps/host-selfhost/docs/rivet.md @@ -41,6 +41,8 @@ expired; a scope shortfall requires broader grants rather than the same login. Execution errors produce bounded diagnostics without persisting arbitrary error content. +A rejected saved sign-in is retained and requires the service owner to reconnect Google Messages. The panel does not offer replacement pairing while a saved session exists. Temporary connection failures remain retryable. + ## Validate with synthetic fixtures Run scoped Vitest tests in `apps/host-selfhost` for `rivet-setup*.test.ts` and @@ -50,7 +52,7 @@ production build. The recorded browser scenario uses the native Rivet producer fixture. In a disposable Rivet checkout, set `RIVET_SETUP_DEMO_DIR` to a new private directory under `/tmp` -and run `tsx scripts/setup-demo.ts`. Its `ready.json` provides the loopback endpoint. +and run `tsx scripts/setup-native-demo.ts`. Its `ready.json` provides the loopback endpoint. Set `E2E_RIVET_SETUP_DIRECTORY`, `E2E_RIVET_SETUP_URL`, and `EXECUTOR_RIVET_SETUP_CONFIG_FILE` to that fixture's directory, URL, and a new JSON file inside the directory. Trust only the new fixture certificate by starting the diff --git a/apps/host-selfhost/src/rivet-setup-state.test.ts b/apps/host-selfhost/src/rivet-setup-state.test.ts index e1610e8db0..8acc87cc7c 100644 --- a/apps/host-selfhost/src/rivet-setup-state.test.ts +++ b/apps/host-selfhost/src/rivet-setup-state.test.ts @@ -4,6 +4,7 @@ import { setupActive, setupInstruction, localSignInUrl, + googleSignInRejected, } from "../web/rivet-setup-state"; const state = () => @@ -19,6 +20,13 @@ const state = () => google: null, }); describe("Rivet setup state", () => { + it("retains a rejected saved session and directs recovery to its owner", () => { + const rejected = { ...state(), googleSessionPresent: true, error: "GM_REPAIR_REQUIRED" }; + expect(googleSignInRejected(rejected)).toBe(true); + expect(setupInstruction(rejected)).toContain("Ask the service owner to reconnect"); + expect(googleSignInRejected({ ...rejected, error: "GM_CONNECT_FAILED" })).toBe(false); + expect(googleSignInRejected({ ...rejected, googleAuthenticated: true })).toBe(false); + }); it("local Google sign-in stays bound to the separate setup listener and current one-time attempt", () => { const uri = new URL("rivet-pair://google"); const id = "00000000-0000-4000-8000-000000000001"; diff --git a/apps/host-selfhost/web/rivet-setup-panel.tsx b/apps/host-selfhost/web/rivet-setup-panel.tsx index 4d72d17719..79d0a22d72 100644 --- a/apps/host-selfhost/web/rivet-setup-panel.tsx +++ b/apps/host-selfhost/web/rivet-setup-panel.tsx @@ -6,6 +6,7 @@ import { decodeSetupStatus, setupActive, setupInstruction, + googleSignInRejected, localSignInUrl, type SetupStatus, } from "./rivet-setup-state"; @@ -151,6 +152,7 @@ export function RivetSetupPanel(props: { readonly integration: string }) { ) : null; const active = setupActive(status); const handoff = localSignInUrl(status); + const rejected = googleSignInRejected(status); return (
@@ -164,9 +166,11 @@ export function RivetSetupPanel(props: { readonly integration: string }) { {status.googleAuthenticated ? "Authenticated" - : status.googleConfigured - ? "Needs pairing or verification" - : "Not configured"} + : rejected + ? "Sign-in expired" + : status.googleConfigured + ? "Needs pairing or verification" + : "Not configured"} @@ -177,11 +181,14 @@ export function RivetSetupPanel(props: { readonly integration: string }) { className="inline-block rounded-md border border-border px-3 py-2 text-sm" href={handoff} > - Sign in with Google passkey + Open Google sign-in on this computer

- Opens the Rivet helper in a fresh local Chrome session. Keep your phone nearby with - Bluetooth enabled. This one-time link expires in ten minutes. + Requires the Rivet helper and Chromium installed on this computer. Use your phone's + passkey, then confirm the Messages emoji. This one-time link expires in ten minutes. +

+

+ On a phone? Open this Accounts page on your computer to complete Google sign-in.

) : null} diff --git a/apps/host-selfhost/web/rivet-setup-state.ts b/apps/host-selfhost/web/rivet-setup-state.ts index dd8370ae4f..a4cf2c8951 100644 --- a/apps/host-selfhost/web/rivet-setup-state.ts +++ b/apps/host-selfhost/web/rivet-setup-state.ts @@ -36,6 +36,11 @@ export const setupActive = (status: SetupStatus) => status.google !== null && ["starting", "sign_in", "phone_confirmation", "verifying"].includes(status.google.state); +export const googleSignInRejected = (status: SetupStatus) => + status.googleSessionPresent && + !status.googleAuthenticated && + status.error === "GM_REPAIR_REQUIRED"; + export function localSignInUrl(status: SetupStatus): string | null { if ( status.google?.method !== "local-browser" || @@ -84,7 +89,7 @@ export function setupInstruction(status: SetupStatus): string { (status.google?.state === "sign_in" || status.google?.state === "starting") && status.google.method === "local-browser" ) - return "Open Google sign-in on a nearby computer. Use your phone's passkey in Chrome, then confirm the Messages emoji here."; + return "Open Google sign-in on a nearby computer. Use your phone's passkey in Chromium, then confirm the Messages emoji here."; if (status.google?.state === "phone_confirmation") return "Open Google Messages on your phone and confirm the matching emoji."; if (status.google?.state === "sign_in" || status.google?.state === "starting") @@ -93,6 +98,8 @@ export function setupInstruction(status: SetupStatus): string { return "Verifying Google Messages authentication without fetching history."; if (status.google?.state === "failed" && !status.googleSessionPresent) return "Google Messages pairing did not finish. Retry when you are ready."; + if (googleSignInRejected(status)) + return "Google rejected your saved sign-in. Ask the service owner to reconnect Google Messages. Your saved session and history are retained."; if (!status.phoneConnected) return "Connect your phone: scan the pairing QR in Rivet, review the address, then Save and connect."; if (status.googleConfigured && !status.googleAuthenticated) diff --git a/e2e/selfhost/rivet-setup-ui.test.ts b/e2e/selfhost/rivet-setup-ui.test.ts index 529bd2f91d..bef8904aa0 100644 --- a/e2e/selfhost/rivet-setup-ui.test.ts +++ b/e2e/selfhost/rivet-setup-ui.test.ts @@ -1,5 +1,5 @@ import { randomBytes } from "node:crypto"; -import { writeFileSync, rmSync } from "node:fs"; +import { writeFileSync, rmSync, existsSync } from "node:fs"; import { join } from "node:path"; import { expect } from "@effect/vitest"; import { Effect } from "effect"; @@ -42,6 +42,13 @@ scenario( }); yield* Effect.gen(function* () { writeFileSync(join(directory, "owner-id"), session.user.id, { mode: 0o600 }); + yield* Effect.promise(async () => { + const deadline = Date.now() + 10000; + while (!existsSync(join(directory, "native-ready.json"))) { + if (Date.now() > deadline) throw new Error("Native fixture did not become ready"); + await new Promise((resolve) => setTimeout(resolve, 25)); + } + }); writeFileSync( configFile, JSON.stringify({ @@ -85,7 +92,7 @@ scenario( }); await step("Start local Google sign-in and cancel the attempt", async () => { await page.getByRole("button", { name: "Connect Google Messages", exact: true }).click(); - await page.getByRole("link", { name: "Sign in with Google passkey" }).waitFor(); + await page.getByRole("link", { name: "Open Google sign-in on this computer" }).waitFor(); await page.getByRole("button", { name: "Cancel setup", exact: true }).click(); await page .getByRole("button", { name: "Connect Google Messages", exact: true }) @@ -101,7 +108,7 @@ scenario( .getByRole("button", { name: "Connect Google Messages", exact: true }) .click(); - const link = page.getByRole("link", { name: "Sign in with Google passkey" }); + const link = page.getByRole("link", { name: "Open Google sign-in on this computer" }); await link.waitFor(); const uri = new URL((await link.getAttribute("href"))!); expect(uri.protocol).toBe("rivet-pair:"); @@ -151,6 +158,21 @@ scenario( await page.getByRole("button", { name: "Check connections", exact: true }).click(); await page.getByText("Authenticated", { exact: true }).waitFor(); }); + await step("Retain a rejected saved session at phone width", async () => { + await page.setViewportSize({ width: 390, height: 844 }); + writeFileSync(join(directory, "gmessages", "reject.synthetic"), "", { mode: 0o600 }); + await page.getByRole("button", { name: "Check connections", exact: true }).click(); + await page.getByText("Sign-in expired", { exact: true }).waitFor(); + await page.getByText(/Ask the service owner to reconnect/).waitFor(); + expect( + await page + .getByRole("button", { name: "Connect Google Messages", exact: true }) + .count(), + ).toBe(0); + expect( + await page.getByRole("link", { name: "Open Google sign-in on this computer" }).count(), + ).toBe(0); + }); }); }).pipe( Effect.ensuring( @@ -161,6 +183,7 @@ scenario( join(directory, "owner-id"), join(directory, "connect-phone.synthetic"), join(directory, "gmessages", "confirm.synthetic"), + join(directory, "gmessages", "reject.synthetic"), ]) rmSync(file, { force: true }); writeFileSync(join(directory, "setup.token"), "b".repeat(64), { mode: 0o600 });