[eric] signin: a not-ready install refuses locally instead of landing on the cloud's install_id error

This commit is contained in:
ciregenz
2026-08-12 15:32:22 -07:00
parent 9a1dffe811
commit f4bb5f566b
5 changed files with 86 additions and 13 deletions
@@ -11,6 +11,7 @@ import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
import SignInDialog from '@/app/components/overlays/SignInDialog';
import OnboardingLogo from '../OnboardingLogo';
import BeatShell from './BeatShell';
import { googleStartUrl } from '@/shared/googleStartUrl';
// The account gate: users sign in (Google or email) before anything else, so the free trial and their
// setup are tied to a real account. Google hands off through the external browser and lands out-of-band
@@ -27,6 +28,7 @@ const BeatSignIn: React.FC<{
const proxyUrl = useAppSelector((s) => s.settings.data.openswarm_proxy_url || OPENSWARM_DEFAULT_PROXY_URL);
const installId = useAppSelector((s) => s.settings.data.installation_id ?? '');
const [waitingGoogle, setWaitingGoogle] = useState(false);
const [notReady, setNotReady] = useState(false);
const [emailOpen, setEmailOpen] = useState(false);
const signedIn = !!userId;
@@ -40,8 +42,11 @@ const BeatSignIn: React.FC<{
if (signedIn) return;
report('signin', 'google_clicked');
const localPort = (window as unknown as { __OPENSWARM_PORT__?: number }).__OPENSWARM_PORT__ || 8324;
const params = new URLSearchParams({ install_id: installId, local_port: String(localPort) });
const startUrl = `${proxyUrl.replace(/\/$/, '')}/api/auth/google/start?${params.toString()}`;
const startUrl = googleStartUrl(proxyUrl, installId, localPort);
// First run is exactly when settings may not have landed yet, so this beat is the likeliest
// place to catch a not-ready install. Say so on the row instead of opening a broken page.
if (!startUrl) { setNotReady(true); return; }
setNotReady(false);
const api = (window as unknown as { openswarm?: { openExternal?: (u: string) => void } }).openswarm;
if (api?.openExternal) api.openExternal(startUrl);
else window.open(startUrl, '_blank');
@@ -49,7 +54,7 @@ const BeatSignIn: React.FC<{
};
const rows: Array<{ id: string; name: string; icon: React.ReactNode; onClick: () => void; hint?: string }> = [
{ id: 'google', name: 'Continue with Google', icon: <GoogleIcon sx={{ fontSize: 20, color: '#4285F4' }} />, onClick: onGoogle, hint: waitingGoogle && !signedIn ? 'Waiting for your browser...' : undefined },
{ id: 'google', name: 'Continue with Google', icon: <GoogleIcon sx={{ fontSize: 20, color: '#4285F4' }} />, onClick: onGoogle, hint: notReady ? 'Still starting up, try again in a second' : (waitingGoogle && !signedIn ? 'Waiting for your browser...' : undefined) },
{ id: 'email', name: 'Continue with email', icon: <EmailIcon sx={{ fontSize: 20, color: '#6f6e6a' }} />, onClick: () => { if (!signedIn) setEmailOpen(true); } },
];
@@ -19,6 +19,7 @@ import { activateSignin, fetchSettings } from '@/shared/state/settingsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { OPENSWARM_DEFAULT_PROXY_URL } from '@/shared/config';
import { report } from '@/shared/serviceClient';
import { googleStartUrl } from '@/shared/googleStartUrl';
type Stage = 'choose' | 'email_form' | 'code_form';
@@ -50,11 +51,11 @@ export default function SignInDialog({ onClose, initialStage = 'choose', mandato
const onGoogle = () => {
report('signin', 'google_clicked');
const localPort = (window as any).__OPENSWARM_PORT__ || 8324;
const params = new URLSearchParams({
install_id: installId,
local_port: String(localPort),
});
const startUrl = `${cloudBase}/api/auth/google/start?${params.toString()}`;
const startUrl = googleStartUrl(cloudBase, installId, localPort);
if (!startUrl) {
setErrMsg('Still finishing startup. Give it a second and try again.');
return;
}
const api = (window as any).openswarm;
if (api?.openExternal) {
api.openExternal(startUrl);
@@ -8,6 +8,7 @@ import { signOut } from '@/shared/state/settingsSlice';
import { OPENSWARM_DEFAULT_PROXY_URL } from '@/shared/config';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import SignInDialog from '@/app/components/overlays/SignInDialog';
import { googleStartUrl } from '@/shared/googleStartUrl';
/** Account card at top of General tab; three states: signed in, paid-but-unlinked, or not signed in. */
const AccountCard: React.FC = () => {
@@ -46,11 +47,10 @@ const AccountCard: React.FC = () => {
const onSignIn = () => {
// Pass local_port so the bearer-handoff page POSTs to the right backend (Electron binds in 8324..8424).
const localPort = (window as any).__OPENSWARM_PORT__ || 8324;
const params = new URLSearchParams({
install_id: installId,
local_port: String(localPort),
});
const startUrl = proxyUrl.replace(/\/$/, '') + '/api/auth/google/start?' + params.toString();
const startUrl = googleStartUrl(proxyUrl, installId, localPort);
// No error surface on this card, so the honest move is to do nothing visible rather than send
// them to a black cloud error page. Settings land within a second and the click then works.
if (!startUrl) return;
const api = (window as any).openswarm;
if (api?.openExternal) api.openExternal(startUrl);
else window.open(startUrl, '_blank');
@@ -0,0 +1,48 @@
// Run: node --test (via frontend/scripts/run-tests.mjs)
//
// The install id arrives with settings, so every sign-in button can be clicked before it exists.
// When that happened the app sent the user to the cloud with `install_id=` and they landed on a bare
// black page reading "install_id must be 8-128 chars" (seen live). A URL that cannot work should
// never be built in the first place.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { googleStartUrl, isUsableInstallId } from './googleStartUrl.ts';
const PROXY = 'https://api.openswarm.com';
const REAL_ID = 'faec918d-6bda-42d5-9de9-f274eb49a8bc';
test('a real install id builds the URL the cloud expects', () => {
const u = googleStartUrl(PROXY, REAL_ID, 8324);
assert.equal(u, `https://api.openswarm.com/api/auth/google/start?install_id=${REAL_ID}&local_port=8324`);
});
test('settings not loaded yet refuses instead of building a doomed URL', () => {
assert.equal(googleStartUrl(PROXY, '', 8324), null);
});
test('an id shorter than the cloud accepts is refused on this side of the network', () => {
assert.equal(googleStartUrl(PROXY, '1234567', 8324), null, '7 chars is below the cloud minimum of 8');
assert.notEqual(googleStartUrl(PROXY, '12345678', 8324), null, '8 chars is exactly the minimum and must pass');
});
test('an absurdly long id is refused too, matching the cloud bound', () => {
assert.notEqual(googleStartUrl(PROXY, 'x'.repeat(128), 8324), null);
assert.equal(googleStartUrl(PROXY, 'x'.repeat(129), 8324), null);
});
test('a trailing slash on the proxy does not produce a double slash', () => {
const u = googleStartUrl('https://api.openswarm.com/', REAL_ID, 8324);
assert.ok(u && !u.includes('.com//'), `double slash in ${u}`);
});
test('the port actually travels, since the bearer handoff POSTs back to it', () => {
const u = googleStartUrl(PROXY, REAL_ID, 8411);
assert.ok(u && u.includes('local_port=8411'));
});
test('the predicate agrees with the builder in both directions', () => {
assert.equal(isUsableInstallId(undefined), false);
assert.equal(isUsableInstallId(null), false);
assert.equal(isUsableInstallId(''), false);
assert.equal(isUsableInstallId(REAL_ID), true);
});
+19
View File
@@ -0,0 +1,19 @@
// The install id lives in settings, so it is empty until `GET /api/settings` has answered. Clicking
// "Continue with Google" before that sent the user to the cloud with `install_id=`, and the cloud
// answers with a bare black page reading "install_id must be 8-128 chars". Three call sites built
// this URL by hand and all three could do it. Refusing to build an unusable URL is the whole fix.
// Mirrors the cloud's own validation, so we fail on this side of the network instead of over there.
const INSTALL_ID_MIN = 8;
const INSTALL_ID_MAX = 128;
export function isUsableInstallId(id: string | undefined | null): boolean {
return !!id && id.length >= INSTALL_ID_MIN && id.length <= INSTALL_ID_MAX;
}
/** The Google sign-in start URL, or null when this install cannot form a valid one yet. */
export function googleStartUrl(proxyUrl: string, installId: string, localPort: number): string | null {
if (!isUsableInstallId(installId)) return null;
const params = new URLSearchParams({ install_id: installId, local_port: String(localPort) });
return `${proxyUrl.replace(/\/$/, '')}/api/auth/google/start?${params.toString()}`;
}