Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
3b40979
feat(browserbase): add per-site auth profiles
tofikwest Jun 19, 2026
a20de6d
fix(browserbase): harden automation run reliability
tofikwest Jun 19, 2026
26a4e6b
Merge branch 'main' into tofik/browser-automation-auth-profiles
tofikwest Jun 19, 2026
bbb15db
fix(browserbase): close remaining reliability gaps
tofikwest Jun 19, 2026
4138764
fix(browserbase): select active evidence page
tofikwest Jun 19, 2026
818b892
chore: merge release v3.86.5 back to main [skip ci]
github-actions[bot] Jun 19, 2026
0bc328c
fix(browserbase): tighten auth profile reliability
tofikwest Jun 19, 2026
1c98fe7
Merge branch 'main' into tofik/browser-automation-auth-profiles
tofikwest Jun 19, 2026
829a7f8
fix(browserbase): recover stale context setup
tofikwest Jun 19, 2026
35df682
fix(browserbase): remove llm credential prompts
tofikwest Jun 19, 2026
a3c4386
Merge pull request #3201 from trycompai/tofik/browser-automation-auth…
tofikwest Jun 19, 2026
a67beaf
fix(browserbase): retry context creation failures
tofikwest Jun 19, 2026
f5c765e
Merge branch 'main' into tofik/browser-automation-auth-profiles
tofikwest Jun 19, 2026
397c8bb
Merge pull request #3206 from trycompai/tofik/browser-automation-auth…
tofikwest Jun 19, 2026
c8ed2e9
fix(browserbase): request identity encoded api responses
tofikwest Jun 19, 2026
c17a43b
Merge branch 'main' into tofik/browser-automation-auth-profiles
tofikwest Jun 19, 2026
716710b
Merge pull request #3207 from trycompai/tofik/browser-automation-auth…
tofikwest Jun 19, 2026
10c9c1b
fix(findings): include all enabled frameworks in overview filter drop…
tofikwest Jun 19, 2026
ea00eea
fix(browserbase): make task auth flow primary
tofikwest Jun 19, 2026
2d219d1
Merge branch 'main' into tofik/browser-automation-task-auth-flow
tofikwest Jun 19, 2026
43eb666
Merge pull request #3209 from trycompai/tofik/browser-automation-task…
tofikwest Jun 19, 2026
20f4de9
Merge branch 'main' into tofik/bug-unable-to-filter-on
tofikwest Jun 19, 2026
f28bcef
Merge pull request #3208 from trycompai/tofik/bug-unable-to-filter-on
tofikwest Jun 19, 2026
b3ac466
fix(browserbase): retry session api failures
tofikwest Jun 19, 2026
ee394dd
feat(framework-editor): add per-framework requirement sort order (FRA…
tofikwest Jun 22, 2026
0af0c3d
fix(framework-editor): tiebreak requirement order by identifier, not …
tofikwest Jun 22, 2026
d7cca3a
fix(wizard): restore step progress indicator on return to saved profile
tofikwest Jun 22, 2026
c317b77
fix(people): allow reactivating deactivated members via update endpoint
tofikwest Jun 22, 2026
54d7bc4
Merge pull request #3214 from trycompai/tofik/frame-18-framework-sort…
tofikwest Jun 22, 2026
94e9700
Merge branch 'main' into tofik/round-1-feedback-wizard
tofikwest Jun 22, 2026
ed346a6
Merge pull request #3215 from trycompai/tofik/round-1-feedback-wizard
tofikwest Jun 22, 2026
e60cff0
Merge branch 'main' into tofik/bug-unable-to-reactivate-user
tofikwest Jun 22, 2026
6928184
Merge pull request #3216 from trycompai/tofik/bug-unable-to-reactivat…
tofikwest Jun 22, 2026
aee86ec
fix(evidence-wizard): add confirmation before discarding form on cancel
tofikwest Jun 22, 2026
1683d00
fix(browserbase): preserve non-retryable errors
tofikwest Jun 22, 2026
5ac1a87
Merge pull request #3211 from trycompai/tofik/browserbase-session-api…
tofikwest Jun 22, 2026
2805c24
Merge branch 'main' into tofik/create-a-ticket-in-team
tofikwest Jun 22, 2026
18fd4d0
Merge pull request #3217 from trycompai/tofik/create-a-ticket-in-team
tofikwest Jun 22, 2026
a866b3a
fix(integrations): retry transient transport errors in check runtime
tofikwest Jun 22, 2026
5ffb251
Merge branch 'main' into tofik/neon-and-cloudflare-checks-failing
tofikwest Jun 22, 2026
d30acac
Merge pull request #3218 from trycompai/tofik/neon-and-cloudflare-che…
tofikwest Jun 22, 2026
0548f7a
fix(members): enable email update in employee details for admins
tofikwest Jun 22, 2026
54720b0
Merge branch 'main' into tofik/update-email-user-addresses
tofikwest Jun 22, 2026
16bcdbc
fix(evidence): display user email fallback when name is empty
tofikwest Jun 22, 2026
8f43b92
fix(compliance): order frameworks consistently to prevent accidental …
tofikwest Jun 22, 2026
7040faa
Merge pull request #3219 from trycompai/tofik/update-email-user-addre…
tofikwest Jun 22, 2026
e3bd813
feat(framework-editor): add framework families (FRAME-20)
tofikwest Jun 22, 2026
3021cbf
fix(trust-portal): review access button links to correct page
tofikwest Jun 22, 2026
c6ef69f
Merge pull request #3223 from trycompai/tofik/review-access-button-in…
tofikwest Jun 22, 2026
ed1dee6
Merge branch 'main' into tofik/employee-name-not-displaying-in
tofikwest Jun 22, 2026
7c894b4
Merge pull request #3220 from trycompai/tofik/employee-name-not-displ…
tofikwest Jun 22, 2026
0a016ef
fix(framework-editor): fix app build + address review on framework fa…
tofikwest Jun 22, 2026
b173b43
Merge branch 'main' into tofik/gdpr-framework-showing-as-hipaa
tofikwest Jun 22, 2026
e93b9ba
Merge pull request #3221 from trycompai/tofik/gdpr-framework-showing-…
tofikwest Jun 22, 2026
d3a399d
fix(framework-editor): family-scoped move + reject null in update DTO…
tofikwest Jun 22, 2026
9d5b2c5
Merge branch 'main' into tofik/frame-20-framework-families
tofikwest Jun 22, 2026
167f1c9
fix(gcp-scc): allow exceptions on public bucket findings by using fal…
tofikwest Jun 22, 2026
116383a
fix(framework-editor): make family delete atomic against concurrent m…
tofikwest Jun 22, 2026
fe7f906
fix(bug-todo): address cubic review
tofikwest Jun 22, 2026
df82274
Merge pull request #3224 from trycompai/tofik/bug-this-finding-cannot-be
tofikwest Jun 22, 2026
9bc42e7
Merge branch 'main' into tofik/frame-20-framework-families
tofikwest Jun 22, 2026
c40d8cb
Merge pull request #3222 from trycompai/tofik/frame-20-framework-fami…
tofikwest Jun 22, 2026
78030f6
fix(browserbase): retry stagehand init to survive premature close
tofikwest Jun 22, 2026
11d61f1
Merge pull request #3225 from trycompai/tofik/browserbase-stagehand-i…
tofikwest Jun 22, 2026
483c368
fix(api): add familyId to update-policy frameworks schema (unblock bu…
tofikwest Jun 22, 2026
f7fcc0c
Merge branch 'main' into tofik/fix-update-policy-family-id
tofikwest Jun 22, 2026
ac9a041
Merge pull request #3226 from trycompai/tofik/fix-update-policy-famil…
tofikwest Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ContextModule } from './context/context.module';
import { TrustPortalModule } from './trust-portal/trust-portal.module';
import { ControlTemplateModule } from './framework-editor/control-template/control-template.module';
import { IsmsDocumentTemplateModule } from './framework-editor/isms-document-template/isms-document-template.module';
import { FrameworkFamilyModule } from './framework-editor/framework-family/framework-family.module';
import { FrameworkEditorFrameworkModule } from './framework-editor/framework/framework.module';
import { PolicyTemplateModule } from './framework-editor/policy-template/policy-template.module';
import { RequirementModule } from './framework-editor/requirement/requirement.module';
Expand Down Expand Up @@ -101,6 +102,7 @@ import { OffboardingChecklistModule } from './offboarding-checklist/offboarding-
FrameworkEditorFrameworkModule,
PolicyTemplateModule,
RequirementModule,
FrameworkFamilyModule,
TaskTemplateModule,
FindingTemplateModule,
FindingsModule,
Expand Down
94 changes: 94 additions & 0 deletions apps/api/src/browserbase/browser-auth-profile-context.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { db, type BrowserAuthProfile } from '@db';
import { BrowserbaseSessionService } from './browserbase-session.service';
import { BrowserbaseOrgContextService } from './browserbase-org-context.service';
import { PENDING_CONTEXT_ID } from './browserbase-org-context.service';

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

@Injectable()
export class BrowserAuthProfileContextService {
private readonly logger = new Logger(BrowserAuthProfileContextService.name);

constructor(
private readonly sessions: BrowserbaseSessionService = new BrowserbaseSessionService(),
private readonly orgContexts: BrowserbaseOrgContextService = new BrowserbaseOrgContextService(
sessions,
),
) {}

async initialize(input: {
profileId: string;
organizationId: string;
}): Promise<BrowserAuthProfile> {
try {
const contextId = await this.resolveInitialContextId(
input.organizationId,
);
return await db.browserAuthProfile.update({
where: { id: input.profileId },
data: { contextId },
});
} catch (error) {
await this.deletePendingProfile(input.profileId);
throw error;
}
}

async ready(profile: BrowserAuthProfile): Promise<BrowserAuthProfile> {
if (profile.contextId !== PENDING_CONTEXT_ID) return profile;
return this.waitForProfileContext(profile.id);
}

private async resolveInitialContextId(
organizationId: string,
): Promise<string> {
const legacy = await this.orgContexts.getOrgContext(organizationId);
if (legacy) return legacy.contextId;
return this.sessions.createBrowserbaseContext();
}

private async waitForProfileContext(
profileId: string,
): Promise<BrowserAuthProfile> {
const maxWaitMs = 10_000;
const pollMs = 200;
const startedAt = Date.now();

while (Date.now() - startedAt < maxWaitMs) {
const current = await db.browserAuthProfile.findUnique({
where: { id: profileId },
});

if (current && current.contextId !== PENDING_CONTEXT_ID) {
return current;
}

if (!current) {
throw new NotFoundException('Browser auth profile not found');
}

await delay(pollMs);
}

this.logger.warn(
`Timed out waiting for Browser auth profile context ${profileId}`,
);
throw new Error(

@cubic-dev-ai cubic-dev-ai Bot Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Timeout handling throws a generic Error instead of a Nest timeout exception. Clients get inconsistent status/error semantics for a retryable timeout path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/browserbase/browser-auth-profile-context.service.ts, line 77:

<comment>Timeout handling throws a generic `Error` instead of a Nest timeout exception. Clients get inconsistent status/error semantics for a retryable timeout path.</comment>

<file context>
@@ -0,0 +1,94 @@
+    this.logger.warn(
+      `Timed out waiting for Browser auth profile context ${profileId}`,
+    );
+    throw new Error(
+      'Browser profile initialization is taking too long. Please retry.',
+    );
</file context>
Fix with cubic

'Browser profile initialization is taking too long. Please retry.',
);
}

private async deletePendingProfile(profileId: string): Promise<void> {
try {
await db.browserAuthProfile.deleteMany({
where: { id: profileId, contextId: PENDING_CONTEXT_ID },
});
} catch (error) {
this.logger.warn('Failed to clear pending browser auth profile', {
profileId,
error: error instanceof Error ? error.message : String(error),
});
}
}
}
191 changes: 191 additions & 0 deletions apps/api/src/browserbase/browser-auth-profile.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { BrowserbaseSessionService } from './browserbase-session.service';
import { BrowserAuthProfileService } from './browser-auth-profile.service';

jest.mock('@db', () => ({
db: {
browserAuthProfile: {
findMany: jest.fn(),
findUnique: jest.fn(),
findUniqueOrThrow: jest.fn(),
findFirst: jest.fn(),
create: jest.fn(),
update: jest.fn(),
deleteMany: jest.fn(),
},
browserbaseContext: {
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
updateMany: jest.fn(),
deleteMany: jest.fn(),
},
},
}));

import { db } from '@db';

describe('BrowserAuthProfileService', () => {
let sessions: BrowserbaseSessionService;
let service: BrowserAuthProfileService;

beforeEach(() => {
jest.clearAllMocks();
sessions = new BrowserbaseSessionService();
jest
.spyOn(sessions, 'createBrowserbaseContext')
.mockResolvedValue('ctx_new');
service = new BrowserAuthProfileService(sessions);
});

it('normalizes hostname and login identity when creating a profile', async () => {
(db.browserAuthProfile.findUnique as jest.Mock).mockResolvedValue(null);
(db.browserbaseContext.findUnique as jest.Mock).mockResolvedValue(null);
(db.browserAuthProfile.create as jest.Mock).mockResolvedValue({
id: 'bap_1',
organizationId: 'org_1',
hostname: 'github.com',
loginIdentity: 'svc@example.com',
contextId: '__PENDING__',
});
(db.browserAuthProfile.update as jest.Mock).mockResolvedValue({
id: 'bap_1',
organizationId: 'org_1',
hostname: 'github.com',
loginIdentity: 'svc@example.com',
contextId: 'ctx_new',
});

await service.getOrCreateProfileFromUrl({
organizationId: 'org_1',
url: 'https://GitHub.com/acme/repo',
loginIdentity: ' SVC@EXAMPLE.COM ',
});

expect(db.browserAuthProfile.create).toHaveBeenCalledWith({
data: expect.objectContaining({
organizationId: 'org_1',
hostname: 'github.com',
loginIdentity: 'svc@example.com',
contextId: '__PENDING__',
}),
});
expect(db.browserAuthProfile.update).toHaveBeenCalledWith({
where: { id: 'bap_1' },
data: { contextId: 'ctx_new' },
});
});

it('does not create an orphan context when another request creates the profile', async () => {
(db.browserAuthProfile.findUnique as jest.Mock).mockResolvedValue(null);
(db.browserAuthProfile.create as jest.Mock).mockRejectedValue({
code: 'P2002',
});
(db.browserAuthProfile.findUniqueOrThrow as jest.Mock).mockResolvedValue({
id: 'bap_existing',
organizationId: 'org_1',
hostname: 'github.com',
loginIdentity: '',
contextId: 'ctx_existing',
});

const result = await service.getOrCreateProfileFromUrl({
organizationId: 'org_1',
url: 'https://github.com/acme/repo',
});

expect(result.profile.id).toBe('bap_existing');
expect(sessions.createBrowserbaseContext).not.toHaveBeenCalled();
});

it('prioritizes a verified profile for the target hostname', async () => {
(db.browserAuthProfile.findMany as jest.Mock).mockResolvedValue([
{ id: 'bap_old', status: 'needs_reauth', hostname: 'github.com' },
{ id: 'bap_verified', status: 'verified', hostname: 'github.com' },
]);

const profile = await service.resolveProfileForTarget({
organizationId: 'org_1',
targetUrl: 'https://github.com/acme/repo',
});

expect(profile.id).toBe('bap_verified');
expect(db.browserAuthProfile.findMany).toHaveBeenCalledWith({
where: { organizationId: 'org_1', hostname: 'github.com' },
orderBy: { updatedAt: 'desc' },
});
});

it('rejects profile verification for a different hostname', async () => {
jest
.spyOn(sessions, 'checkLoginStatus')
.mockResolvedValue({ isLoggedIn: true });
(db.browserAuthProfile.findFirst as jest.Mock).mockResolvedValue({
id: 'bap_1',
organizationId: 'org_1',
hostname: 'github.com',
lastVerifiedAt: null,
});

await expect(
service.verifyProfileSession({
organizationId: 'org_1',
profileId: 'bap_1',
sessionId: 'sess_1',
url: 'https://gitlab.com/acme/repo',
}),
).rejects.toThrow('Verification URL must match');
expect(sessions.checkLoginStatus).not.toHaveBeenCalled();
});

it('rejects profile verification when the session uses another context', async () => {
jest.spyOn(sessions, 'getSessionContextId').mockResolvedValue('ctx_other');
jest
.spyOn(sessions, 'checkLoginStatus')
.mockResolvedValue({ isLoggedIn: true });
(db.browserAuthProfile.findFirst as jest.Mock).mockResolvedValue({
id: 'bap_1',
organizationId: 'org_1',
hostname: 'github.com',
contextId: 'ctx_profile',
lastVerifiedAt: null,
});

await expect(
service.verifyProfileSession({
organizationId: 'org_1',
profileId: 'bap_1',
sessionId: 'sess_wrong',
url: 'https://github.com/acme/repo',
}),
).rejects.toThrow('does not belong to this auth profile');
expect(sessions.checkLoginStatus).not.toHaveBeenCalled();
expect(db.browserAuthProfile.update).not.toHaveBeenCalled();
});

it('treats a pending legacy org context as unavailable', async () => {
(db.browserbaseContext.findUnique as jest.Mock).mockResolvedValue({
organizationId: 'org_1',
contextId: '__PENDING__',
});

await expect(service.getOrgContext('org_1')).resolves.toBeNull();
});

it('clears pending org context when Browserbase context creation fails', async () => {
(db.browserbaseContext.findUnique as jest.Mock).mockResolvedValue(null);
(db.browserbaseContext.create as jest.Mock).mockResolvedValue({
organizationId: 'org_1',
contextId: '__PENDING__',
});
jest
.spyOn(sessions, 'createBrowserbaseContext')
.mockRejectedValue(new Error('Browserbase unavailable'));

await expect(service.getOrCreateOrgContext('org_1')).rejects.toThrow(
'Browserbase unavailable',
);
expect(db.browserbaseContext.deleteMany).toHaveBeenCalledWith({
where: { organizationId: 'org_1', contextId: '__PENDING__' },
});
});
});
Loading
Loading