Skip to content

Commit 1c22dff

Browse files
committed
Merge branch 'main' of https://github.com/trycompai/comp into mariano/cloud-tests-bugged
# Conflicts: # packages/docs/openapi.json
2 parents 6f1405a + 7de133f commit 1c22dff

File tree

6 files changed

+142
-86
lines changed

6 files changed

+142
-86
lines changed

apps/portal/src/app/actions/login.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ export const login = createSafeActionClient({ handleServerError })
5555
email: parsedInput.email,
5656
otp: parsedInput.otp,
5757
},
58-
asResponse: true,
5958
});
6059

6160
return {

packages/device-agent/src/main/auth.ts

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,25 @@ export async function performLogin(deviceInfo: DeviceInfo): Promise<StoredAuth |
120120
authWindow.hide();
121121
}
122122

123-
// Wait for cookies to settle
124-
await new Promise((r) => setTimeout(r, 500));
125-
126123
try {
127-
const authData = await extractAuthAndRegisterAll(deviceInfo);
124+
const authData = await extractAuthAndRegisterAll(portalUrl, deviceInfo);
128125
if (authData) {
129126
setAuth(authData);
130127
log(`Auth complete: ${authData.organizations.length} org(s) registered`);
131128
finish(authData);
132129
} else {
133-
log('Auth extraction returned null, closing window');
130+
// extractAuthAndRegisterAll already showed an error dialog
134131
finish(null);
135132
}
136133
} catch (error) {
137134
log(`Auth extraction failed: ${error}`, 'ERROR');
135+
dialog.showMessageBoxSync({
136+
type: 'error',
137+
title: 'Sign-In Failed',
138+
message: 'An unexpected error occurred during sign-in.',
139+
detail: `${error}`,
140+
buttons: ['OK'],
141+
});
138142
finish(null);
139143
}
140144
};
@@ -157,24 +161,56 @@ export async function performLogin(deviceInfo: DeviceInfo): Promise<StoredAuth |
157161
});
158162
}
159163

164+
/**
165+
* Waits for the session cookie to appear in the Electron session.
166+
* Retries up to maxAttempts times with a delay between attempts.
167+
*/
168+
async function waitForSessionCookie(
169+
portalUrl: string,
170+
maxAttempts = 10,
171+
delayMs = 500,
172+
): Promise<Electron.Cookie | null> {
173+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
174+
await new Promise((r) => setTimeout(r, delayMs));
175+
176+
const cookies = await session.defaultSession.cookies.get({ url: portalUrl });
177+
const sessionCookie = cookies.find(
178+
(c) =>
179+
c.name === 'better-auth.session_token' || c.name === '__Secure-better-auth.session_token',
180+
);
181+
182+
if (sessionCookie) {
183+
log(`Session cookie found on attempt ${attempt}/${maxAttempts}`);
184+
return sessionCookie;
185+
}
186+
187+
log(`Session cookie not found yet (attempt ${attempt}/${maxAttempts})`, 'WARN');
188+
}
189+
190+
return null;
191+
}
192+
160193
/**
161194
* After login, fetches the user's orgs and registers the device for all of them.
162-
* Shows an error dialog if the user has no organizations.
195+
* Shows error dialogs on failure so the user knows what went wrong.
163196
*/
164197
async function extractAuthAndRegisterAll(
198+
portalUrl: string,
165199
deviceInfo: DeviceInfo,
166200
): Promise<StoredAuth | null> {
167-
const portalUrl = getPortalUrl();
168-
169-
// 1. Get session cookie
170-
const cookies = await session.defaultSession.cookies.get({ url: portalUrl });
171-
const sessionCookie = cookies.find(
172-
(c) =>
173-
c.name === 'better-auth.session_token' || c.name === '__Secure-better-auth.session_token',
174-
);
201+
// 1. Wait for session cookie with retries
202+
const sessionCookie = await waitForSessionCookie(portalUrl);
175203

176204
if (!sessionCookie) {
177-
log('No session cookie found after login', 'WARN');
205+
log('No session cookie found after login (exhausted retries)', 'ERROR');
206+
dialog.showMessageBoxSync({
207+
type: 'error',
208+
title: 'Sign-In Failed',
209+
message: 'Could not complete sign-in.',
210+
detail:
211+
'The session cookie was not set after authentication. Please try again. If the problem persists, restart the app.',
212+
buttons: ['OK'],
213+
});
178214
return null;
179215
}
180216

@@ -188,14 +224,28 @@ async function extractAuthAndRegisterAll(
188224

189225
if (!sessionResponse.ok) {
190226
log(`Session fetch failed: ${sessionResponse.status}`, 'ERROR');
227+
dialog.showMessageBoxSync({
228+
type: 'error',
229+
title: 'Sign-In Failed',
230+
message: 'Could not verify your session.',
231+
detail: `The server returned status ${sessionResponse.status}. Please try signing in again.`,
232+
buttons: ['OK'],
233+
});
191234
return null;
192235
}
193236

194237
const sessionData = await sessionResponse.json();
195238
const userId = sessionData?.user?.id;
196239

197240
if (!userId) {
198-
log('No userId in session', 'ERROR');
241+
log('No userId in session response', 'ERROR');
242+
dialog.showMessageBoxSync({
243+
type: 'error',
244+
title: 'Sign-In Failed',
245+
message: 'Could not retrieve your user information.',
246+
detail: 'The session is missing user data. Please try signing in again.',
247+
buttons: ['OK'],
248+
});
199249
return null;
200250
}
201251

@@ -208,6 +258,13 @@ async function extractAuthAndRegisterAll(
208258

209259
if (!orgsResponse.ok) {
210260
log(`Failed to fetch organizations: ${orgsResponse.status}`, 'ERROR');
261+
dialog.showMessageBoxSync({
262+
type: 'error',
263+
title: 'Sign-In Failed',
264+
message: 'Could not fetch your organizations.',
265+
detail: `The server returned status ${orgsResponse.status}. Please try signing in again.`,
266+
buttons: ['OK'],
267+
});
211268
return null;
212269
}
213270

packages/device-agent/src/main/index.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import { performLogin, performLogout } from './auth';
77
import { initAutoLaunch } from './auto-launch';
88
import { getDeviceInfo } from './device-info';
99
import { log } from './logger';
10-
import { runChecksNow, setSessionExpiredHandler, startScheduler, stopScheduler } from './scheduler';
10+
import {
11+
runChecksNow,
12+
setDevicesNotFoundHandler,
13+
setSessionExpiredHandler,
14+
startScheduler,
15+
stopScheduler,
16+
} from './scheduler';
1117
import { clearAuth, getAuth, getLastCheckResults } from './store';
1218
import {
1319
createTray,
@@ -49,8 +55,11 @@ if (process.platform === 'darwin') {
4955

5056
let currentStatus: TrayStatus = 'unauthenticated';
5157
let currentResults: CheckResult[] = [];
58+
let isSigningIn = false;
5259

53-
// Handle session expiry: clear auth, update UI, and re-prompt login
60+
// Handle session expiry: clear auth, update UI, and re-prompt login.
61+
// stopScheduler() is called BEFORE triggerSignIn() so no periodic checks
62+
// can re-trigger the expired handler during the sign-in flow.
5463
setSessionExpiredHandler(async () => {
5564
log('Session expired — clearing auth and prompting re-login');
5665
stopScheduler();
@@ -59,23 +68,46 @@ setSessionExpiredHandler(async () => {
5968
currentResults = [];
6069
setStatus('unauthenticated');
6170
notifyRenderer(IPC_CHANNELS.AUTH_STATE_CHANGED, false);
62-
// Auto-open sign-in so the user can re-authenticate immediately
71+
triggerSignIn();
72+
});
73+
74+
// Handle stale device IDs: all orgs returned 404 for the stored device IDs.
75+
// This happens when devices are deleted from the portal. Re-login to re-register.
76+
setDevicesNotFoundHandler(async () => {
77+
log('All devices returned 404 — clearing auth and re-registering');
78+
stopScheduler();
79+
await performLogout();
80+
clearAuth();
81+
currentResults = [];
82+
setStatus('unauthenticated');
83+
notifyRenderer(IPC_CHANNELS.AUTH_STATE_CHANGED, false);
6384
triggerSignIn();
6485
});
6586

6687
async function triggerSignIn(): Promise<void> {
88+
if (isSigningIn) {
89+
log('Sign-in already in progress, skipping duplicate request');
90+
return;
91+
}
92+
93+
isSigningIn = true;
6794
log('Sign-in flow triggered');
68-
const deviceInfo = getDeviceInfo();
69-
const auth = await performLogin(deviceInfo);
7095

71-
if (auth) {
72-
const orgNames = auth.organizations.map((o) => o.organizationName).join(', ');
73-
log(`Login successful: ${auth.organizations.length} org(s) — ${orgNames}`);
74-
notifyRenderer(IPC_CHANNELS.AUTH_STATE_CHANGED, true);
75-
setStatus('checking');
76-
startScheduler(handleCheckComplete);
77-
} else {
78-
log('Login cancelled or failed');
96+
try {
97+
const deviceInfo = getDeviceInfo();
98+
const auth = await performLogin(deviceInfo);
99+
100+
if (auth) {
101+
const orgNames = auth.organizations.map((o) => o.organizationName).join(', ');
102+
log(`Login successful: ${auth.organizations.length} org(s) — ${orgNames}`);
103+
notifyRenderer(IPC_CHANNELS.AUTH_STATE_CHANGED, true);
104+
setStatus('checking');
105+
startScheduler(handleCheckComplete);
106+
} else {
107+
log('Login cancelled or failed');
108+
}
109+
} finally {
110+
isSigningIn = false;
79111
}
80112
}
81113

packages/device-agent/src/main/reporter.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface ReportResult {
88
isCompliant: boolean;
99
/** True if ANY org returned 401 — session has expired */
1010
sessionExpired: boolean;
11+
/** True if ALL orgs returned 404 — stored device IDs are stale */
12+
allDevicesNotFound: boolean;
1113
}
1214

1315
/**
@@ -18,7 +20,7 @@ export async function reportCheckResults(checks: CheckResult[]): Promise<ReportR
1820
const auth = getAuth();
1921
if (!auth) {
2022
log('Cannot report check results: not authenticated', 'ERROR');
21-
return { allSucceeded: false, isCompliant: false, sessionExpired: false };
23+
return { allSucceeded: false, isCompliant: false, sessionExpired: false, allDevicesNotFound: false };
2224
}
2325

2426
const portalUrl = getPortalUrl();
@@ -28,6 +30,7 @@ export async function reportCheckResults(checks: CheckResult[]): Promise<ReportR
2830
let allSucceeded = true;
2931
let anyNonCompliant = false;
3032
let sessionExpired = false;
33+
let notFoundCount = 0;
3134

3235
for (const org of auth.organizations) {
3336
const payload: CheckInRequest = {
@@ -59,6 +62,9 @@ export async function reportCheckResults(checks: CheckResult[]): Promise<ReportR
5962
sessionExpired = true;
6063
break; // No point trying other orgs with the same expired token
6164
}
65+
if (response.status === 404) {
66+
notFoundCount++;
67+
}
6268
continue;
6369
}
6470

@@ -80,5 +86,6 @@ export async function reportCheckResults(checks: CheckResult[]): Promise<ReportR
8086
allSucceeded,
8187
isCompliant: !anyNonCompliant && allSucceeded,
8288
sessionExpired,
89+
allDevicesNotFound: notFoundCount > 0 && notFoundCount === auth.organizations.length,
8390
};
8491
}

packages/device-agent/src/main/scheduler.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ let isRunning = false;
1010

1111
type CheckCallback = (results: CheckResult[], isCompliant: boolean) => void;
1212
type SessionExpiredCallback = () => void;
13+
type DevicesNotFoundCallback = () => void;
1314

1415
let onSessionExpired: SessionExpiredCallback | null = null;
16+
let onDevicesNotFound: DevicesNotFoundCallback | null = null;
1517

1618
/**
1719
* Registers a callback for when the session token is rejected (401).
@@ -20,6 +22,13 @@ export function setSessionExpiredHandler(handler: SessionExpiredCallback): void
2022
onSessionExpired = handler;
2123
}
2224

25+
/**
26+
* Registers a callback for when all device IDs return 404 (stale registrations).
27+
*/
28+
export function setDevicesNotFoundHandler(handler: DevicesNotFoundCallback): void {
29+
onDevicesNotFound = handler;
30+
}
31+
2332
/**
2433
* Starts the periodic compliance check scheduler.
2534
* Runs an initial check immediately, then repeats on the configured interval.
@@ -82,14 +91,20 @@ async function runChecksAndReport(onCheckComplete: CheckCallback): Promise<void>
8291
setLastCheckResults(results);
8392

8493
// Report to all organizations
85-
const { isCompliant, sessionExpired } = await reportCheckResults(results);
94+
const { isCompliant, sessionExpired, allDevicesNotFound } = await reportCheckResults(results);
8695

8796
if (sessionExpired) {
8897
log('Session expired during check-in, triggering re-authentication');
8998
onSessionExpired?.();
9099
return;
91100
}
92101

102+
if (allDevicesNotFound) {
103+
log('All device IDs returned 404, triggering re-registration');
104+
onDevicesNotFound?.();
105+
return;
106+
}
107+
93108
log(`Check complete: ${isCompliant ? 'COMPLIANT' : 'NON-COMPLIANT'}`);
94109
onCheckComplete(results, isCompliant);
95110
} catch (error) {

packages/docs/openapi.json

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14390,60 +14390,6 @@
1439014390
]
1439114391
}
1439214392
},
14393-
"/v1/cloud-security/trigger/{connectionId}": {
14394-
"post": {
14395-
"operationId": "CloudSecurityController_triggerScan_v1",
14396-
"parameters": [
14397-
{
14398-
"name": "connectionId",
14399-
"required": true,
14400-
"in": "path",
14401-
"schema": {
14402-
"type": "string"
14403-
}
14404-
}
14405-
],
14406-
"responses": {
14407-
"201": {
14408-
"description": ""
14409-
}
14410-
},
14411-
"tags": [
14412-
"CloudSecurity"
14413-
]
14414-
}
14415-
},
14416-
"/v1/cloud-security/runs/{runId}": {
14417-
"get": {
14418-
"operationId": "CloudSecurityController_getRunStatus_v1",
14419-
"parameters": [
14420-
{
14421-
"name": "runId",
14422-
"required": true,
14423-
"in": "path",
14424-
"schema": {
14425-
"type": "string"
14426-
}
14427-
},
14428-
{
14429-
"name": "connectionId",
14430-
"required": true,
14431-
"in": "query",
14432-
"schema": {
14433-
"type": "string"
14434-
}
14435-
}
14436-
],
14437-
"responses": {
14438-
"200": {
14439-
"description": ""
14440-
}
14441-
},
14442-
"tags": [
14443-
"CloudSecurity"
14444-
]
14445-
}
14446-
},
1444714393
"/v1/browserbase/org-context": {
1444814394
"post": {
1444914395
"description": "Gets the existing browser context for the org or creates a new one",

0 commit comments

Comments
 (0)