From 72aa34c3446d1562cba9899ab6115d4a9f41bc4a Mon Sep 17 00:00:00 2001 From: Nikhil Mittal Date: Tue, 5 May 2026 15:43:15 +0530 Subject: [PATCH 1/2] apexguru jwt minting --- .../package.json | 2 +- .../src/config.ts | 14 +- .../src/engine.ts | 10 +- .../src/services/ApexGuruAuthService.ts | 150 ++++++++++++++---- .../src/services/ApexGuruService.ts | 15 +- .../src/types/index.ts | 5 + .../test/ApexGuruAuthService.test.ts | 85 +++++++++- 7 files changed, 229 insertions(+), 52 deletions(-) diff --git a/packages/code-analyzer-apexguru-engine/package.json b/packages/code-analyzer-apexguru-engine/package.json index 8f1203da..2f4388f7 100644 --- a/packages/code-analyzer-apexguru-engine/package.json +++ b/packages/code-analyzer-apexguru-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-apexguru-engine", "description": "ApexGuru Engine Package for the Salesforce Code Analyzer", - "version": "0.37.0", + "version": "0.38.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", diff --git a/packages/code-analyzer-apexguru-engine/src/config.ts b/packages/code-analyzer-apexguru-engine/src/config.ts index 2fa7b989..4606a07c 100644 --- a/packages/code-analyzer-apexguru-engine/src/config.ts +++ b/packages/code-analyzer-apexguru-engine/src/config.ts @@ -5,6 +5,12 @@ import { ConfigDescription, ConfigValueExtractor } from '@salesforce/code-analyz * Currently minimal - authentication is handled via SF CLI */ export type ApexGuruEngineConfig = { + /** + * Target Salesforce org username or alias + * If not specified, uses the default SF CLI org + */ + target_org?: string; + /** * Maximum time to wait for ApexGuru API response (in milliseconds) * Default: 120000 (2 minutes) @@ -46,7 +52,7 @@ export const DEFAULT_APEXGURU_ENGINE_CONFIG: ApexGuruEngineConfig = { * Configuration schema description for ApexGuru Engine */ export const APEXGURU_ENGINE_CONFIG_DESCRIPTION: ConfigDescription = { - overview: 'Configuration for ApexGuru Engine. Authentication is handled via Salesforce CLI (sf org login web).', + overview: 'Configuration for ApexGuru Engine. Authentication is handled via Salesforce CLI (sf org login web). Use --target-org flag to specify the org.', fieldDescriptions: { api_timeout_ms: { descriptionText: 'Maximum time to wait for ApexGuru API response (in milliseconds). Default: 120000 (2 minutes)', @@ -85,6 +91,11 @@ export async function validateAndNormalizeConfig( 'api_backoff_multiplier' ]); + // Extract target org from CLI flag only + // - If user passes --target-org: use that org + // - If user doesn't pass --target-org: undefined (auth service uses default SF CLI org) + const targetOrg: string | undefined = process.env.CODE_ANALYZER_TARGET_ORG; + // Extract and validate timeout const apiTimeoutMs: number = configValueExtractor.extractNumber( 'api_timeout_ms', @@ -130,6 +141,7 @@ export async function validateAndNormalizeConfig( } return { + target_org: targetOrg, api_timeout_ms: apiTimeoutMs, api_initial_retry_ms: apiInitialRetryMs, api_max_retry_ms: apiMaxRetryMs, diff --git a/packages/code-analyzer-apexguru-engine/src/engine.ts b/packages/code-analyzer-apexguru-engine/src/engine.ts index 66fca426..98ac3acc 100644 --- a/packages/code-analyzer-apexguru-engine/src/engine.ts +++ b/packages/code-analyzer-apexguru-engine/src/engine.ts @@ -195,13 +195,9 @@ export class ApexGuruEngine extends EngineEventEmitter implements Engine { * Target org can be set via SF_TARGET_ORG environment variable. */ private getTargetOrgFromEnvironment(): string | undefined { - // Check environment variable - if (process.env.SF_TARGET_ORG) { - return process.env.SF_TARGET_ORG; - } - - // Return undefined to use default org from SF CLI - return undefined; + // Return target_org from config (set via CLI --target-org flag or config file) + // If undefined, ApexGuruAuthService will use default SF CLI org + return this.config.target_org; } } diff --git a/packages/code-analyzer-apexguru-engine/src/services/ApexGuruAuthService.ts b/packages/code-analyzer-apexguru-engine/src/services/ApexGuruAuthService.ts index 9b81022d..4aa3b13d 100644 --- a/packages/code-analyzer-apexguru-engine/src/services/ApexGuruAuthService.ts +++ b/packages/code-analyzer-apexguru-engine/src/services/ApexGuruAuthService.ts @@ -1,23 +1,13 @@ - - -import { AuthInfo, Connection } from '@salesforce/core'; +import { Connection, Org } from '@salesforce/core'; import { LogLevel } from '@salesforce/code-analyzer-engine-api'; -import { AuthConfig } from '../types'; - -/** - * TEMPORARY: Hardcoded credentials for testing - * TODO: Implement SF CLI, env vars, and OAuth in future PR - * NEVER commit real credentials - use 'YOUR_ACCESS_TOKEN_HERE' as placeholder - */ -const HARDCODED_ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN_HERE'; // Get from: sf org display --verbose -const HARDCODED_INSTANCE_URL = 'https://yourorg.my.salesforce.com'; // e.g., https://yourorg.my.salesforce.com +import { AuthConfig, OrgJwtResponse } from '../types'; /** - * Handles authentication to Salesforce orgs for ApexGuru API access - * TODO: Currently uses hardcoded credentials only. Implement proper auth in future PR. + * Handles authentication to Salesforce orgs for ApexGuru API access. */ export class ApexGuruAuthService { private connection?: Connection; + private orgJwt?: string; private readonly emitLogEvent: (logLevel: LogLevel, message: string) => void; constructor(emitLogEvent: (logLevel: LogLevel, message: string) => void = () => {}) { @@ -25,31 +15,51 @@ export class ApexGuruAuthService { } /** - * Initialize connection to Salesforce org - * TODO: Implement SF CLI, env vars, and OAuth in future PR - * @param _config - Auth configuration (currently unused, for future implementation) + * Initialize connection to Salesforce org using one of two methods: + * + * Method 1: SF CLI org with --target-org flag + * config.targetOrg = 'myorg' or 'user@example.com' + * + * Method 2: SF CLI default org (fallback) + * No config provided - uses SF CLI default org + * + * @param config - Auth configuration */ - async initialize(_config: AuthConfig): Promise { - // Use hardcoded credentials (temporary implementation) - this.emitLogEvent(LogLevel.Warn, '⚠️ Using HARDCODED authentication credentials (for testing)'); + async initialize(config: AuthConfig): Promise { + // Method 1: SF CLI org (alias or username) via --target-org flag + if (config.targetOrg) { + this.emitLogEvent(LogLevel.Fine, `Authenticating with org: ${config.targetOrg}`); + try { + const org = await Org.create({ aliasOrUsername: config.targetOrg }); + this.connection = org.getConnection(); + this.emitLogEvent(LogLevel.Fine, `Successfully authenticated to org`); + return; + } catch { + this.emitLogEvent(LogLevel.Error, `Failed to authenticate with org: ${config.targetOrg}`); + throw new Error( + `Failed to authenticate with org '${config.targetOrg}'. ` + + 'Please verify the org alias/username and ensure you are authenticated:\n' + + ' sf org list\n' + + ' sf org login web' + ); + } + } - // Validate that credentials were actually set - if (!HARDCODED_ACCESS_TOKEN || HARDCODED_ACCESS_TOKEN.includes('YOUR_ACCESS_TOKEN')) { + // Method 2: SF CLI default org (fallback) + this.emitLogEvent(LogLevel.Fine, 'No target org specified, using default org'); + try { + const org = await Org.create({}); + this.connection = org.getConnection(); + this.emitLogEvent(LogLevel.Fine, 'Successfully authenticated to default org'); + } catch { + this.emitLogEvent(LogLevel.Error, 'Failed to authenticate: No default org found'); throw new Error( - 'Hardcoded credentials not set! Edit ApexGuruAuthService.ts and set:\n' + - ' - HARDCODED_ACCESS_TOKEN (get from: sf org display --verbose)\n' + - ' - HARDCODED_INSTANCE_URL (e.g., https://yourorg.my.salesforce.com)' + 'No default org found. Please either:\n' + + ' 1. Set a default org: sf config set target-org \n' + + ' 2. Pass --target-org flag: sf code-analyzer run --target-org ...\n' + + ' 3. Authenticate to an org: sf org login web' ); } - - this.connection = await Connection.create({ - authInfo: await AuthInfo.create({ - accessTokenOptions: { - accessToken: HARDCODED_ACCESS_TOKEN, - instanceUrl: HARDCODED_INSTANCE_URL - } - }) - }); } /** @@ -86,4 +96,76 @@ export class ApexGuruAuthService { getApiVersion(): string { return this.getConnection().version || '64.0'; } + + /** + * Mint an Org JWT token for SFAP API access + * + * @param featureId - Feature ID for tracking (default: 'VibesService') + * @returns Promise - The Org JWT token + * @throws Error if minting fails + */ + async mintOrgJwt(featureId: string = 'VibesService'): Promise { + const accessToken = this.getAccessToken(); + const instanceUrl = this.getInstanceUrl(); + + const endpoint = `${instanceUrl}/ide/auth`; + this.emitLogEvent(LogLevel.Fine, 'Minting Org JWT for SFAP API access'); + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + 'X-Feature-Id': featureId, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + const errorText = await response.text(); + this.emitLogEvent(LogLevel.Error, `Failed to mint Org JWT: HTTP ${response.status}`); + throw new Error( + `Failed to mint Org JWT: ${response.status} ${response.statusText}. ` + + `Response: ${errorText}` + ); + } + + const data = await response.json() as OrgJwtResponse; + + if (!data.jwt) { + this.emitLogEvent(LogLevel.Error, 'Org JWT response missing jwt field'); + throw new Error('Org JWT response missing jwt field'); + } + + this.orgJwt = data.jwt; + this.emitLogEvent(LogLevel.Fine, 'Successfully minted Org JWT'); + return data.jwt; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.emitLogEvent(LogLevel.Error, 'Org JWT minting failed'); + throw new Error(`Org JWT minting failed: ${errorMessage}`); + } + } + + /** + * Get the cached Org JWT token + * @returns The Org JWT if available, undefined otherwise + */ + getOrgJwt(): string | undefined { + return this.orgJwt; + } + + /** + * Get or mint the Org JWT token + * If already minted, returns the cached token. Otherwise, mints a new one. + * @returns Promise - The Org JWT token + */ + async getOrMintOrgJwt(): Promise { + if (this.orgJwt) { + return this.orgJwt; + } + return await this.mintOrgJwt(); + } } diff --git a/packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts b/packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts index 0260a67f..1c60b626 100644 --- a/packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts +++ b/packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts @@ -41,10 +41,23 @@ export class ApexGuruService { } /** - * Initialize authentication + * Initialize authentication and mint Org JWT */ async initialize(targetOrg?: string): Promise { + // Initialize auth service with SF CLI await this.authService.initialize({ targetOrg }); + + // Mint Org JWT for SFAP API access + const orgJwt = await this.authService.mintOrgJwt(); + + try { + const jwtParts = orgJwt.split('.'); + if (jwtParts.length === 3) { + //const payload = JSON.parse(Buffer.from(jwtParts[1], 'base64').toString()); + } + } catch (error) { + this.emitLogEvent(LogLevel.Warn, `Could not decode JWT payload: ${error}`); + } } /** diff --git a/packages/code-analyzer-apexguru-engine/src/types/index.ts b/packages/code-analyzer-apexguru-engine/src/types/index.ts index 3c0f90da..c827c34b 100644 --- a/packages/code-analyzer-apexguru-engine/src/types/index.ts +++ b/packages/code-analyzer-apexguru-engine/src/types/index.ts @@ -99,3 +99,8 @@ export type ApexGuruFix = { export type ApexGuruRequestBody = { classContent: string; // Base64 encoded Apex class }; + +export type OrgJwtResponse = { + jwt: string; + message?: string | null; +}; diff --git a/packages/code-analyzer-apexguru-engine/test/ApexGuruAuthService.test.ts b/packages/code-analyzer-apexguru-engine/test/ApexGuruAuthService.test.ts index 40ae30e6..c8b21a81 100644 --- a/packages/code-analyzer-apexguru-engine/test/ApexGuruAuthService.test.ts +++ b/packages/code-analyzer-apexguru-engine/test/ApexGuruAuthService.test.ts @@ -1,4 +1,5 @@ import { ApexGuruAuthService } from '../src/services/ApexGuruAuthService'; +import { Org, Connection } from '@salesforce/core'; // Mock @salesforce/core jest.mock('@salesforce/core'); @@ -6,24 +7,60 @@ jest.mock('@salesforce/core'); describe('ApexGuruAuthService', () => { let authService: ApexGuruAuthService; let mockEmitLogEvent: jest.Mock; + let mockConnection: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); mockEmitLogEvent = jest.fn(); authService = new ApexGuruAuthService(mockEmitLogEvent); + + // Mock connection + mockConnection = { + instanceUrl: 'https://test.salesforce.com', + accessToken: 'mock_access_token', + version: '64.0' + } as any; }); describe('initialize', () => { - it('should throw error when hardcoded credentials not set', async () => { - // Placeholder credentials should trigger error - await expect(authService.initialize({})) - .rejects.toThrow('Hardcoded credentials not set'); + it('should initialize with targetOrg using SF CLI', async () => { + const mockOrg = { + getConnection: jest.fn().mockReturnValue(mockConnection) + }; + (Org.create as jest.Mock).mockResolvedValue(mockOrg); + + await authService.initialize({ targetOrg: 'myorg' }); + + expect(Org.create).toHaveBeenCalledWith({ aliasOrUsername: 'myorg' }); + expect(mockOrg.getConnection).toHaveBeenCalled(); + expect(authService.getInstanceUrl()).toBe('https://test.salesforce.com'); + }); + + it('should initialize with direct credentials', async () => { + const mockCreateConnection = jest.fn().mockResolvedValue(mockConnection); + + // Mock Connection.create + (Connection.create as jest.Mock).mockImplementation(mockCreateConnection); + + await authService.initialize({ + accessToken: 'test_token', + instanceUrl: 'https://test.salesforce.com' + }); + + expect(mockCreateConnection).toHaveBeenCalled(); }); - // TODO: Add tests for proper auth methods when implemented - // - SF CLI integration - // - Environment variables - // - OAuth flow + it('should initialize with default org when no config provided', async () => { + const mockOrg = { + getConnection: jest.fn().mockReturnValue(mockConnection) + }; + (Org.create as jest.Mock).mockResolvedValue(mockOrg); + + await authService.initialize({}); + + expect(Org.create).toHaveBeenCalledWith({}); + expect(mockOrg.getConnection).toHaveBeenCalled(); + }); }); describe('getConnection', () => { @@ -55,4 +92,36 @@ describe('ApexGuruAuthService', () => { // TODO: Add test for getApiVersion with mock connection when proper auth is implemented }); + + describe('mintOrgJwt', () => { + it('should throw error if not initialized', async () => { + await expect(authService.mintOrgJwt()) + .rejects.toThrow('Auth service not initialized'); + }); + + // TODO: Add tests for successful JWT minting when proper auth is implemented + // - Should call /dataseed/auth with correct headers + // - Should return JWT from response + // - Should cache JWT in orgJwt property + // - Should handle API errors properly + }); + + describe('getOrgJwt', () => { + it('should return undefined when JWT not minted', () => { + expect(authService.getOrgJwt()).toBeUndefined(); + }); + + // TODO: Add test for returning cached JWT after minting + }); + + describe('getOrMintOrgJwt', () => { + it('should throw error if not initialized', async () => { + await expect(authService.getOrMintOrgJwt()) + .rejects.toThrow('Auth service not initialized'); + }); + + // TODO: Add tests for getOrMintOrgJwt when proper auth is implemented + // - Should mint new JWT if not cached + // - Should return cached JWT if available + }); }); From b0704f0a2264e27ff135109c1c447f0d9b6b3366 Mon Sep 17 00:00:00 2001 From: Nikhil Mittal Date: Tue, 5 May 2026 15:53:17 +0530 Subject: [PATCH 2/2] test case fix --- .../test/ApexGuruAuthService.test.ts | 16 +++++----------- .../test/ApexGuruEngine.test.ts | 10 +++++++--- .../test/ApexGuruService.test.ts | 3 ++- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/code-analyzer-apexguru-engine/test/ApexGuruAuthService.test.ts b/packages/code-analyzer-apexguru-engine/test/ApexGuruAuthService.test.ts index c8b21a81..c8fe8c35 100644 --- a/packages/code-analyzer-apexguru-engine/test/ApexGuruAuthService.test.ts +++ b/packages/code-analyzer-apexguru-engine/test/ApexGuruAuthService.test.ts @@ -36,18 +36,12 @@ describe('ApexGuruAuthService', () => { expect(authService.getInstanceUrl()).toBe('https://test.salesforce.com'); }); - it('should initialize with direct credentials', async () => { - const mockCreateConnection = jest.fn().mockResolvedValue(mockConnection); + it('should throw error when targetOrg authentication fails', async () => { + (Org.create as jest.Mock).mockRejectedValue(new Error('Org not found')); - // Mock Connection.create - (Connection.create as jest.Mock).mockImplementation(mockCreateConnection); - - await authService.initialize({ - accessToken: 'test_token', - instanceUrl: 'https://test.salesforce.com' - }); - - expect(mockCreateConnection).toHaveBeenCalled(); + await expect(authService.initialize({ targetOrg: 'invalid-org' })) + .rejects + .toThrow("Failed to authenticate with org 'invalid-org'"); }); it('should initialize with default org when no config provided', async () => { diff --git a/packages/code-analyzer-apexguru-engine/test/ApexGuruEngine.test.ts b/packages/code-analyzer-apexguru-engine/test/ApexGuruEngine.test.ts index 3d556f35..2b174b6f 100644 --- a/packages/code-analyzer-apexguru-engine/test/ApexGuruEngine.test.ts +++ b/packages/code-analyzer-apexguru-engine/test/ApexGuruEngine.test.ts @@ -369,17 +369,21 @@ describe('ApexGuruEngine', () => { }); it('should extract targetOrg from environment', async () => { - process.env.SF_TARGET_ORG = 'my-org'; + // Set environment variable BEFORE creating the engine + process.env.CODE_ANALYZER_TARGET_ORG = 'my-org'; + + // Create new engine with config that includes target_org from environment + const engineWithConfig = new ApexGuruEngine({ target_org: 'my-org', api_timeout_ms: 120000, api_initial_retry_ms: 2000, api_max_retry_ms: 60000, api_backoff_multiplier: 2 }); mockWorkspace.getTargetedFiles.mockResolvedValue(['/test/Test.cls']); mockApexGuruService.analyzeApexClass.mockResolvedValue([]); (fs.readFile as jest.Mock).mockResolvedValue('public class Test {}'); - await engine.runRules(['SoqlInALoop'], mockRunOptions); + await engineWithConfig.runRules(['SoqlInALoop'], mockRunOptions); expect(mockApexGuruService.initialize).toHaveBeenCalledWith('my-org'); - delete process.env.SF_TARGET_ORG; + delete process.env.CODE_ANALYZER_TARGET_ORG; }); it('should aggregate violations from multiple files', async () => { diff --git a/packages/code-analyzer-apexguru-engine/test/ApexGuruService.test.ts b/packages/code-analyzer-apexguru-engine/test/ApexGuruService.test.ts index 13a19feb..98f05acf 100644 --- a/packages/code-analyzer-apexguru-engine/test/ApexGuruService.test.ts +++ b/packages/code-analyzer-apexguru-engine/test/ApexGuruService.test.ts @@ -28,7 +28,8 @@ describe('ApexGuruService', () => { getConnection: jest.fn().mockReturnValue(mockConnection), getAccessToken: jest.fn().mockReturnValue('test-token'), getInstanceUrl: jest.fn().mockReturnValue('https://test.salesforce.com'), - getApiVersion: jest.fn().mockReturnValue('64.0') + getApiVersion: jest.fn().mockReturnValue('64.0'), + mintOrgJwt: jest.fn().mockResolvedValue('mock-jwt-token') } as any; (ApexGuruAuthService as jest.Mock).mockImplementation(() => mockAuthService);