From a114ce5c774944ee9af261628a274bf863d3d6d9 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sat, 21 Jun 2025 18:21:22 -0400 Subject: [PATCH 1/6] Store the organization id in credentials --- packages/cloud/src/AuthService.ts | 37 +++++++++++++++++- packages/cloud/src/CloudService.ts | 18 ++++++++- .../cloud/src/__tests__/AuthService.spec.ts | 2 +- .../cloud/src/__tests__/CloudService.test.ts | 38 ++++++++++++++++++- src/activate/handleUri.ts | 8 +++- src/services/mdm/MdmService.ts | 26 +++++++++++-- 6 files changed, 118 insertions(+), 11 deletions(-) diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index f64007f0d20..8f7e7f2257f 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -21,6 +21,7 @@ export interface AuthServiceEvents { const authCredentialsSchema = z.object({ clientToken: z.string().min(1, "Client token cannot be empty"), sessionId: z.string().min(1, "Session ID cannot be empty"), + organizationId: z.string().nullable().optional(), }) type AuthCredentials = z.infer @@ -220,7 +221,16 @@ export class AuthService extends EventEmitter { try { const parsedJson = JSON.parse(credentialsJson) - return authCredentialsSchema.parse(parsedJson) + const credentials = authCredentialsSchema.parse(parsedJson) + + // Migration: If no organizationId but we have userInfo, add it + if (credentials.organizationId === undefined && this.userInfo?.organizationId) { + credentials.organizationId = this.userInfo.organizationId + await this.storeCredentials(credentials) + this.log("[auth] Migrated credentials with organizationId") + } + + return credentials } catch (error) { if (error instanceof z.ZodError) { this.log("[auth] Invalid credentials format:", error.errors) @@ -269,8 +279,13 @@ export class AuthService extends EventEmitter { * * @param code The authorization code from the callback * @param state The state parameter from the callback + * @param organizationId The organization ID from the callback (null for personal accounts) */ - public async handleCallback(code: string | null, state: string | null): Promise { + public async handleCallback( + code: string | null, + state: string | null, + organizationId?: string | null, + ): Promise { if (!code || !state) { vscode.window.showInformationMessage("Invalid Roo Code Cloud sign in url") return @@ -287,6 +302,9 @@ export class AuthService extends EventEmitter { const credentials = await this.clerkSignIn(code) + // Set organizationId (null for personal accounts) + credentials.organizationId = organizationId || null + await this.storeCredentials(credentials) vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud") @@ -417,6 +435,15 @@ export class AuthService extends EventEmitter { return this.userInfo } + /** + * Get the stored organization ID from credentials + * + * @returns The stored organization ID, null for personal accounts or if no credentials exist + */ + public getStoredOrganizationId(): string | null { + return this.credentials?.organizationId || null + } + private async clerkSignIn(ticket: string): Promise { const formData = new URLSearchParams() formData.append("strategy", "ticket") @@ -454,6 +481,12 @@ export class AuthService extends EventEmitter { const formData = new URLSearchParams() formData.append("_is_native", "1") + // Only add organization_id if not null (personal accounts) + const organizationId = this.getStoredOrganizationId() + if (organizationId !== null) { + formData.append("organization_id", organizationId) + } + const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, { method: "POST", headers: { diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index c53538f1bc3..031827a6a53 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -118,14 +118,28 @@ export class CloudService { return userInfo?.organizationRole || null } + public hasStoredOrganizationId(): boolean { + this.ensureInitialized() + return this.authService!.getStoredOrganizationId() !== null + } + + public getStoredOrganizationId(): string | null { + this.ensureInitialized() + return this.authService!.getStoredOrganizationId() + } + public getAuthState(): string { this.ensureInitialized() return this.authService!.getState() } - public async handleAuthCallback(code: string | null, state: string | null): Promise { + public async handleAuthCallback( + code: string | null, + state: string | null, + organizationId?: string | null, + ): Promise { this.ensureInitialized() - return this.authService!.handleCallback(code, state) + return this.authService!.handleCallback(code, state, organizationId) } // SettingsService diff --git a/packages/cloud/src/__tests__/AuthService.spec.ts b/packages/cloud/src/__tests__/AuthService.spec.ts index f4d4e98510a..658bdeb3e85 100644 --- a/packages/cloud/src/__tests__/AuthService.spec.ts +++ b/packages/cloud/src/__tests__/AuthService.spec.ts @@ -328,7 +328,7 @@ describe("AuthService", () => { expect(mockContext.secrets.store).toHaveBeenCalledWith( "clerk-auth-credentials", - JSON.stringify({ clientToken: "Bearer token-123", sessionId: "session-123" }), + JSON.stringify({ clientToken: "Bearer token-123", sessionId: "session-123", organizationId: null }), ) expect(mockShowInfo).toHaveBeenCalledWith("Successfully authenticated with Roo Code Cloud") }) diff --git a/packages/cloud/src/__tests__/CloudService.test.ts b/packages/cloud/src/__tests__/CloudService.test.ts index c7dc07fcae5..f4ed8f86ee8 100644 --- a/packages/cloud/src/__tests__/CloudService.test.ts +++ b/packages/cloud/src/__tests__/CloudService.test.ts @@ -40,6 +40,7 @@ describe("CloudService", () => { getState: ReturnType getSessionToken: ReturnType handleCallback: ReturnType + getStoredOrganizationId: ReturnType on: ReturnType off: ReturnType once: ReturnType @@ -88,6 +89,7 @@ describe("CloudService", () => { getState: vi.fn().mockReturnValue("logged-out"), getSessionToken: vi.fn(), handleCallback: vi.fn(), + getStoredOrganizationId: vi.fn().mockReturnValue(null), on: vi.fn(), off: vi.fn(), once: vi.fn(), @@ -255,7 +257,41 @@ describe("CloudService", () => { it("should delegate handleAuthCallback to AuthService", async () => { await cloudService.handleAuthCallback("code", "state") - expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state") + expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", undefined) + }) + + it("should delegate handleAuthCallback with organizationId to AuthService", async () => { + await cloudService.handleAuthCallback("code", "state", "org_123") + expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state", "org_123") + }) + + it("should return stored organization ID from AuthService", () => { + mockAuthService.getStoredOrganizationId.mockReturnValue("org_456") + + const result = cloudService.getStoredOrganizationId() + expect(mockAuthService.getStoredOrganizationId).toHaveBeenCalled() + expect(result).toBe("org_456") + }) + + it("should return null when no stored organization ID available", () => { + mockAuthService.getStoredOrganizationId.mockReturnValue(null) + + const result = cloudService.getStoredOrganizationId() + expect(result).toBe(null) + }) + + it("should return true when stored organization ID exists", () => { + mockAuthService.getStoredOrganizationId.mockReturnValue("org_789") + + const result = cloudService.hasStoredOrganizationId() + expect(result).toBe(true) + }) + + it("should return false when no stored organization ID exists", () => { + mockAuthService.getStoredOrganizationId.mockReturnValue(null) + + const result = cloudService.hasStoredOrganizationId() + expect(result).toBe(false) }) }) diff --git a/src/activate/handleUri.ts b/src/activate/handleUri.ts index 106bcdb3118..7f0b4c64cc1 100644 --- a/src/activate/handleUri.ts +++ b/src/activate/handleUri.ts @@ -38,7 +38,13 @@ export const handleUri = async (uri: vscode.Uri) => { case "/auth/clerk/callback": { const code = query.get("code") const state = query.get("state") - await CloudService.instance.handleAuthCallback(code, state) + const organizationId = query.get("organizationId") + + await CloudService.instance.handleAuthCallback( + code, + state, + organizationId === "null" ? null : organizationId, + ) break } default: diff --git a/src/services/mdm/MdmService.ts b/src/services/mdm/MdmService.ts index e88601dfb3c..d0a7bc92eea 100644 --- a/src/services/mdm/MdmService.ts +++ b/src/services/mdm/MdmService.ts @@ -6,6 +6,7 @@ import { z } from "zod" import { CloudService, getClerkBaseUrl, PRODUCTION_CLERK_BASE_URL } from "@roo-code/cloud" import { Package } from "../../shared/package" +import { t } from "../../i18n" // MDM Configuration Schema const mdmConfigSchema = z.object({ @@ -89,7 +90,7 @@ export class MdmService { if (!CloudService.hasInstance() || !CloudService.instance.hasOrIsAcquiringActiveSession()) { return { compliant: false, - reason: "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", + reason: t("mdm.errors.cloud_auth_required"), } } @@ -97,18 +98,35 @@ export class MdmService { const requiredOrgId = this.getRequiredOrganizationId() if (requiredOrgId) { try { - const currentOrgId = CloudService.instance.getOrganizationId() + // First try to get from active session + let currentOrgId = CloudService.instance.getOrganizationId() + + // If no active session, check stored credentials + if (!currentOrgId) { + const storedOrgId = CloudService.instance.getStoredOrganizationId() + + // null means personal account, which is not compliant for org requirements + if (storedOrgId === null || storedOrgId !== requiredOrgId) { + return { + compliant: false, + reason: t("mdm.errors.organization_mismatch"), + } + } + + currentOrgId = storedOrgId + } + if (currentOrgId !== requiredOrgId) { return { compliant: false, - reason: "You must be authenticated with your organization's Roo Code Cloud account.", + reason: t("mdm.errors.organization_mismatch"), } } } catch (error) { this.log("[MDM] Error checking organization ID:", error) return { compliant: false, - reason: "Unable to verify organization authentication.", + reason: t("mdm.errors.verification_failed"), } } } From 22be838160a89cf39904caf6a7962b20e075153c Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sat, 21 Jun 2025 22:59:11 -0400 Subject: [PATCH 2/6] Better organization logic --- packages/cloud/src/AuthService.ts | 40 ++++++++----- .../cloud/src/__tests__/AuthService.spec.ts | 56 +++++++++++++++++-- 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index 8f7e7f2257f..2b58a2a45fc 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -538,23 +538,37 @@ export class AuthService extends EventEmitter { userInfo.picture = userData.image_url - // Fetch organization memberships separately + // Fetch organization info separately - but only populate if user is in organization context try { - const orgMemberships = await this.clerkGetOrganizationMemberships() - if (orgMemberships && orgMemberships.length > 0) { - // Get the first (or active) organization membership - const primaryOrgMembership = orgMemberships[0] - const organization = primaryOrgMembership?.organization - - if (organization) { - userInfo.organizationId = organization.id - userInfo.organizationName = organization.name - userInfo.organizationRole = primaryOrgMembership.role - userInfo.organizationImageUrl = organization.image_url + const storedOrgId = this.getStoredOrganizationId() + + if (storedOrgId !== null) { + // User is in organization context - fetch user's memberships and filter + const orgMemberships = await this.clerkGetOrganizationMemberships() + + // Find the user's membership in this organization + const userMembership = orgMemberships?.find( + (membership: CloudOrganizationMembership) => membership.organization.id === storedOrgId, + ) + + if (userMembership) { + userInfo.organizationId = userMembership.organization.id + userInfo.organizationName = userMembership.organization.name + userInfo.organizationRole = userMembership.role + userInfo.organizationImageUrl = userMembership.organization.image_url + this.log("[auth] User in organization context:", { + id: userMembership.organization.id, + name: userMembership.organization.name, + role: userMembership.role, + }) + } else { + this.log("[auth] Warning: User not found in stored organization:", storedOrgId) } + } else { + this.log("[auth] User in personal account context - not setting organization info") } } catch (error) { - this.log("[auth] Failed to fetch organization memberships:", error) + this.log("[auth] Failed to fetch organization info:", error) // Don't throw - organization info is optional } diff --git a/packages/cloud/src/__tests__/AuthService.spec.ts b/packages/cloud/src/__tests__/AuthService.spec.ts index 658bdeb3e85..944bcd2b243 100644 --- a/packages/cloud/src/__tests__/AuthService.spec.ts +++ b/packages/cloud/src/__tests__/AuthService.spec.ts @@ -633,9 +633,55 @@ describe("AuthService", () => { expect(authService.getUserInfo()).toBeNull() }) - it("should parse user info correctly", async () => { - // Set up with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } + it("should parse user info correctly for personal accounts", async () => { + // Set up with credentials for personal account (no organizationId) + const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null } + mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) + await authService.initialize() + + // Clear previous mock calls + mockFetch.mockClear() + + // Mock successful responses + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ jwt: "jwt-token" }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + response: { + first_name: "Jane", + last_name: "Smith", + image_url: "https://example.com/jane.jpg", + primary_email_address_id: "email-2", + email_addresses: [ + { id: "email-1", email_address: "jane.old@example.com" }, + { id: "email-2", email_address: "jane@example.com" }, + ], + }, + }), + }) + + const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback + await timerCallback() + + // Wait for async operations to complete + await new Promise((resolve) => setTimeout(resolve, 0)) + + const userInfo = authService.getUserInfo() + expect(userInfo).toEqual({ + name: "Jane Smith", + email: "jane@example.com", + picture: "https://example.com/jane.jpg", + }) + }) + + it("should parse user info correctly for organization accounts", async () => { + // Set up with credentials for organization account + const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: "org_1" } mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) await authService.initialize() @@ -699,8 +745,8 @@ describe("AuthService", () => { }) it("should handle missing user info fields", async () => { - // Set up with credentials - const credentials = { clientToken: "test-token", sessionId: "test-session" } + // Set up with credentials for personal account (no organizationId) + const credentials = { clientToken: "test-token", sessionId: "test-session", organizationId: null } mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials)) await authService.initialize() From e9308f3fc110986ba512563492df0d4a82beaa3d Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sat, 21 Jun 2025 23:14:34 -0400 Subject: [PATCH 3/6] Fix tests --- src/services/mdm/__tests__/MdmService.spec.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/services/mdm/__tests__/MdmService.spec.ts b/src/services/mdm/__tests__/MdmService.spec.ts index a7a48a5eb3e..a69e74a9b5c 100644 --- a/src/services/mdm/__tests__/MdmService.spec.ts +++ b/src/services/mdm/__tests__/MdmService.spec.ts @@ -43,6 +43,19 @@ vi.mock("../../../shared/package", () => ({ }, })) +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string) => { + const translations: Record = { + "mdm.errors.cloud_auth_required": + "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", + "mdm.errors.organization_mismatch": + "You must be authenticated with your organization's Roo Code Cloud account.", + "mdm.errors.verification_failed": "Unable to verify organization authentication.", + } + return translations[key] || key + }), +})) + import * as fs from "fs" import * as os from "os" import * as vscode from "vscode" @@ -265,7 +278,7 @@ describe("MdmService", () => { expect(compliance.compliant).toBe(false) if (!compliance.compliant) { - expect(compliance.reason).toContain("requires Roo Code Cloud authentication") + expect(compliance.reason).toContain("Your organization requires Roo Code Cloud authentication") } }) @@ -287,7 +300,9 @@ describe("MdmService", () => { expect(compliance.compliant).toBe(false) if (!compliance.compliant) { - expect(compliance.reason).toContain("organization's Roo Code Cloud account") + expect(compliance.reason).toContain( + "You must be authenticated with your organization's Roo Code Cloud account", + ) } }) From 9eba5b4c035c401141c8b677e6a30ff51f4b00ea Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 22 Jun 2025 00:29:53 -0400 Subject: [PATCH 4/6] Update cloud settings defaults --- packages/types/src/cloud.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index dc5ef439dc3..75ff0b08b97 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -125,6 +125,7 @@ export const ORGANIZATION_ALLOW_ALL: OrganizationAllowList = { export const ORGANIZATION_DEFAULT: OrganizationSettings = { version: 0, cloudSettings: { + recordTaskMessages: true, enableTaskSharing: true, taskShareExpirationDays: 30, }, From d7b5485859d01243557612c5e1d1b57a2ee8b669 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 22 Jun 2025 21:39:16 -0400 Subject: [PATCH 5/6] Fix organization_id handling in Clerk API calls Address review feedback by properly handling 3 cases for organization_id: 1. Have an org id: send organization_id=THE_ORG_ID 2. Have a personal account: send organization_id= (empty string) 3. Don't know if you have an org id (old credentials): don't send organization_id param at all Changes: - Updated clerkCreateSessionToken() to check credentials.organizationId !== undefined - Updated fetchUserInfo() to handle all 3 cases consistently - Added fallback logic for old credentials without organization context - Improved logging for better debugging of organization context --- packages/cloud/src/AuthService.ts | 80 +++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index 2b58a2a45fc..b38a2f87ca6 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -481,11 +481,16 @@ export class AuthService extends EventEmitter { const formData = new URLSearchParams() formData.append("_is_native", "1") - // Only add organization_id if not null (personal accounts) + // Handle 3 cases for organization_id: + // 1. Have an org id: organization_id=THE_ORG_ID + // 2. Have a personal account: organization_id= (empty string) + // 3. Don't know if you have an org id (old style credentials): don't send organization_id param at all const organizationId = this.getStoredOrganizationId() - if (organizationId !== null) { - formData.append("organization_id", organizationId) + if (this.credentials?.organizationId !== undefined) { + // We have organization context info (either org id or personal account) + formData.append("organization_id", organizationId || "") } + // If organizationId is undefined, don't send the param at all (old credentials) const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, { method: "POST", @@ -538,34 +543,59 @@ export class AuthService extends EventEmitter { userInfo.picture = userData.image_url - // Fetch organization info separately - but only populate if user is in organization context + // Fetch organization info if user is in organization context try { const storedOrgId = this.getStoredOrganizationId() - if (storedOrgId !== null) { - // User is in organization context - fetch user's memberships and filter - const orgMemberships = await this.clerkGetOrganizationMemberships() - - // Find the user's membership in this organization - const userMembership = orgMemberships?.find( - (membership: CloudOrganizationMembership) => membership.organization.id === storedOrgId, - ) - - if (userMembership) { - userInfo.organizationId = userMembership.organization.id - userInfo.organizationName = userMembership.organization.name - userInfo.organizationRole = userMembership.role - userInfo.organizationImageUrl = userMembership.organization.image_url - this.log("[auth] User in organization context:", { - id: userMembership.organization.id, - name: userMembership.organization.name, - role: userMembership.role, - }) + if (this.credentials?.organizationId !== undefined) { + // We have organization context info + if (storedOrgId !== null) { + // User is in organization context - fetch user's memberships and filter + const orgMemberships = await this.clerkGetOrganizationMemberships() + + // Find the user's membership in this organization + const userMembership = orgMemberships?.find( + (membership: CloudOrganizationMembership) => membership.organization.id === storedOrgId, + ) + + if (userMembership) { + userInfo.organizationId = userMembership.organization.id + userInfo.organizationName = userMembership.organization.name + userInfo.organizationRole = userMembership.role + userInfo.organizationImageUrl = userMembership.organization.image_url + this.log("[auth] User in organization context:", { + id: userMembership.organization.id, + name: userMembership.organization.name, + role: userMembership.role, + }) + } else { + this.log("[auth] Warning: User not found in stored organization:", storedOrgId) + } } else { - this.log("[auth] Warning: User not found in stored organization:", storedOrgId) + this.log("[auth] User in personal account context - not setting organization info") } } else { - this.log("[auth] User in personal account context - not setting organization info") + // Old credentials without organization context - fetch organization info to determine context + const orgMemberships = await this.clerkGetOrganizationMemberships() + if (orgMemberships && orgMemberships.length > 0) { + // Get the first (or active) organization membership + const primaryOrgMembership = orgMemberships[0] + const organization = primaryOrgMembership?.organization + + if (organization) { + userInfo.organizationId = organization.id + userInfo.organizationName = organization.name + userInfo.organizationRole = primaryOrgMembership.role + userInfo.organizationImageUrl = organization.image_url + this.log("[auth] Legacy credentials: Found organization membership:", { + id: organization.id, + name: organization.name, + role: primaryOrgMembership.role, + }) + } + } else { + this.log("[auth] Legacy credentials: No organization memberships found") + } } } catch (error) { this.log("[auth] Failed to fetch organization info:", error) From b2542e338e6e3464e6b546701af909f21dcbf7c5 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 22 Jun 2025 21:47:56 -0400 Subject: [PATCH 6/6] DRY up organization loading code in AuthService Extract common organization membership processing logic into reusable helper methods: - findOrganizationMembership(): Find specific org membership by ID - findPrimaryOrganizationMembership(): Get first/primary org membership - setUserOrganizationInfo(): Set organization info on user object This eliminates duplication between the two clerkGetOrganizationMemberships() call sites that were doing very similar organization data processing. --- packages/cloud/src/AuthService.ts | 56 +++++++++++++++++-------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index b38a2f87ca6..a02bfcc2983 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -552,17 +552,10 @@ export class AuthService extends EventEmitter { if (storedOrgId !== null) { // User is in organization context - fetch user's memberships and filter const orgMemberships = await this.clerkGetOrganizationMemberships() - - // Find the user's membership in this organization - const userMembership = orgMemberships?.find( - (membership: CloudOrganizationMembership) => membership.organization.id === storedOrgId, - ) + const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId) if (userMembership) { - userInfo.organizationId = userMembership.organization.id - userInfo.organizationName = userMembership.organization.name - userInfo.organizationRole = userMembership.role - userInfo.organizationImageUrl = userMembership.organization.image_url + this.setUserOrganizationInfo(userInfo, userMembership) this.log("[auth] User in organization context:", { id: userMembership.organization.id, name: userMembership.organization.name, @@ -577,22 +570,15 @@ export class AuthService extends EventEmitter { } else { // Old credentials without organization context - fetch organization info to determine context const orgMemberships = await this.clerkGetOrganizationMemberships() - if (orgMemberships && orgMemberships.length > 0) { - // Get the first (or active) organization membership - const primaryOrgMembership = orgMemberships[0] - const organization = primaryOrgMembership?.organization - - if (organization) { - userInfo.organizationId = organization.id - userInfo.organizationName = organization.name - userInfo.organizationRole = primaryOrgMembership.role - userInfo.organizationImageUrl = organization.image_url - this.log("[auth] Legacy credentials: Found organization membership:", { - id: organization.id, - name: organization.name, - role: primaryOrgMembership.role, - }) - } + const primaryOrgMembership = this.findPrimaryOrganizationMembership(orgMemberships) + + if (primaryOrgMembership) { + this.setUserOrganizationInfo(userInfo, primaryOrgMembership) + this.log("[auth] Legacy credentials: Found organization membership:", { + id: primaryOrgMembership.organization.id, + name: primaryOrgMembership.organization.name, + role: primaryOrgMembership.role, + }) } else { this.log("[auth] Legacy credentials: No organization memberships found") } @@ -605,6 +591,26 @@ export class AuthService extends EventEmitter { return userInfo } + private findOrganizationMembership( + memberships: CloudOrganizationMembership[], + organizationId: string, + ): CloudOrganizationMembership | undefined { + return memberships?.find((membership) => membership.organization.id === organizationId) + } + + private findPrimaryOrganizationMembership( + memberships: CloudOrganizationMembership[], + ): CloudOrganizationMembership | undefined { + return memberships && memberships.length > 0 ? memberships[0] : undefined + } + + private setUserOrganizationInfo(userInfo: CloudUserInfo, membership: CloudOrganizationMembership): void { + userInfo.organizationId = membership.organization.id + userInfo.organizationName = membership.organization.name + userInfo.organizationRole = membership.role + userInfo.organizationImageUrl = membership.organization.image_url + } + private async clerkGetOrganizationMemberships(): Promise { const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, { headers: {