-
Notifications
You must be signed in to change notification settings - Fork 329
[comp] Production Deploy #3205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
[comp] Production Deploy #3205
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 a20de6d
fix(browserbase): harden automation run reliability
tofikwest 26a4e6b
Merge branch 'main' into tofik/browser-automation-auth-profiles
tofikwest bbb15db
fix(browserbase): close remaining reliability gaps
tofikwest 4138764
fix(browserbase): select active evidence page
tofikwest 818b892
chore: merge release v3.86.5 back to main [skip ci]
github-actions[bot] 0bc328c
fix(browserbase): tighten auth profile reliability
tofikwest 1c98fe7
Merge branch 'main' into tofik/browser-automation-auth-profiles
tofikwest 829a7f8
fix(browserbase): recover stale context setup
tofikwest 35df682
fix(browserbase): remove llm credential prompts
tofikwest a3c4386
Merge pull request #3201 from trycompai/tofik/browser-automation-auth…
tofikwest a67beaf
fix(browserbase): retry context creation failures
tofikwest f5c765e
Merge branch 'main' into tofik/browser-automation-auth-profiles
tofikwest 397c8bb
Merge pull request #3206 from trycompai/tofik/browser-automation-auth…
tofikwest c8ed2e9
fix(browserbase): request identity encoded api responses
tofikwest c17a43b
Merge branch 'main' into tofik/browser-automation-auth-profiles
tofikwest 716710b
Merge pull request #3207 from trycompai/tofik/browser-automation-auth…
tofikwest 10c9c1b
fix(findings): include all enabled frameworks in overview filter drop…
tofikwest ea00eea
fix(browserbase): make task auth flow primary
tofikwest 2d219d1
Merge branch 'main' into tofik/browser-automation-task-auth-flow
tofikwest 43eb666
Merge pull request #3209 from trycompai/tofik/browser-automation-task…
tofikwest 20f4de9
Merge branch 'main' into tofik/bug-unable-to-filter-on
tofikwest f28bcef
Merge pull request #3208 from trycompai/tofik/bug-unable-to-filter-on
tofikwest b3ac466
fix(browserbase): retry session api failures
tofikwest ee394dd
feat(framework-editor): add per-framework requirement sort order (FRA…
tofikwest 0af0c3d
fix(framework-editor): tiebreak requirement order by identifier, not …
tofikwest d7cca3a
fix(wizard): restore step progress indicator on return to saved profile
tofikwest c317b77
fix(people): allow reactivating deactivated members via update endpoint
tofikwest 54d7bc4
Merge pull request #3214 from trycompai/tofik/frame-18-framework-sort…
tofikwest 94e9700
Merge branch 'main' into tofik/round-1-feedback-wizard
tofikwest ed346a6
Merge pull request #3215 from trycompai/tofik/round-1-feedback-wizard
tofikwest e60cff0
Merge branch 'main' into tofik/bug-unable-to-reactivate-user
tofikwest 6928184
Merge pull request #3216 from trycompai/tofik/bug-unable-to-reactivat…
tofikwest aee86ec
fix(evidence-wizard): add confirmation before discarding form on cancel
tofikwest 1683d00
fix(browserbase): preserve non-retryable errors
tofikwest 5ac1a87
Merge pull request #3211 from trycompai/tofik/browserbase-session-api…
tofikwest 2805c24
Merge branch 'main' into tofik/create-a-ticket-in-team
tofikwest 18fd4d0
Merge pull request #3217 from trycompai/tofik/create-a-ticket-in-team
tofikwest a866b3a
fix(integrations): retry transient transport errors in check runtime
tofikwest 5ffb251
Merge branch 'main' into tofik/neon-and-cloudflare-checks-failing
tofikwest d30acac
Merge pull request #3218 from trycompai/tofik/neon-and-cloudflare-che…
tofikwest 0548f7a
fix(members): enable email update in employee details for admins
tofikwest 54720b0
Merge branch 'main' into tofik/update-email-user-addresses
tofikwest 16bcdbc
fix(evidence): display user email fallback when name is empty
tofikwest 8f43b92
fix(compliance): order frameworks consistently to prevent accidental …
tofikwest 7040faa
Merge pull request #3219 from trycompai/tofik/update-email-user-addre…
tofikwest e3bd813
feat(framework-editor): add framework families (FRAME-20)
tofikwest 3021cbf
fix(trust-portal): review access button links to correct page
tofikwest c6ef69f
Merge pull request #3223 from trycompai/tofik/review-access-button-in…
tofikwest ed1dee6
Merge branch 'main' into tofik/employee-name-not-displaying-in
tofikwest 7c894b4
Merge pull request #3220 from trycompai/tofik/employee-name-not-displ…
tofikwest 0a016ef
fix(framework-editor): fix app build + address review on framework fa…
tofikwest b173b43
Merge branch 'main' into tofik/gdpr-framework-showing-as-hipaa
tofikwest e93b9ba
Merge pull request #3221 from trycompai/tofik/gdpr-framework-showing-…
tofikwest d3a399d
fix(framework-editor): family-scoped move + reject null in update DTO…
tofikwest 9d5b2c5
Merge branch 'main' into tofik/frame-20-framework-families
tofikwest 167f1c9
fix(gcp-scc): allow exceptions on public bucket findings by using fal…
tofikwest 116383a
fix(framework-editor): make family delete atomic against concurrent m…
tofikwest fe7f906
fix(bug-todo): address cubic review
tofikwest df82274
Merge pull request #3224 from trycompai/tofik/bug-this-finding-cannot-be
tofikwest 9bc42e7
Merge branch 'main' into tofik/frame-20-framework-families
tofikwest c40d8cb
Merge pull request #3222 from trycompai/tofik/frame-20-framework-fami…
tofikwest 78030f6
fix(browserbase): retry stagehand init to survive premature close
tofikwest 11d61f1
Merge pull request #3225 from trycompai/tofik/browserbase-stagehand-i…
tofikwest 483c368
fix(api): add familyId to update-policy frameworks schema (unblock bu…
tofikwest f7fcc0c
Merge branch 'main' into tofik/fix-update-policy-family-id
tofikwest ac9a041
Merge pull request #3226 from trycompai/tofik/fix-update-policy-famil…
tofikwest File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
apps/api/src/browserbase/browser-auth-profile-context.service.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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( | ||
| '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
191
apps/api/src/browserbase/browser-auth-profile.service.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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__' }, | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
Errorinstead of a Nest timeout exception. Clients get inconsistent status/error semantics for a retryable timeout path.Prompt for AI agents