Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion packages/code-analyzer-apexguru-engine/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
14 changes: 13 additions & 1 deletion packages/code-analyzer-apexguru-engine/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 3 additions & 7 deletions packages/code-analyzer-apexguru-engine/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,65 @@


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 = () => {}) {
this.emitLogEvent = emitLogEvent;
}

/**
* 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<void> {
// Use hardcoded credentials (temporary implementation)
this.emitLogEvent(LogLevel.Warn, '⚠️ Using HARDCODED authentication credentials (for testing)');
async initialize(config: AuthConfig): Promise<void> {
// 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 <org-alias>\n' +
' 2. Pass --target-org flag: sf code-analyzer run --target-org <org-alias> ...\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
}
})
});
}

/**
Expand Down Expand Up @@ -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<string> - The Org JWT token
* @throws Error if minting fails
*/
async mintOrgJwt(featureId: string = 'VibesService'): Promise<string> {
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<string> - The Org JWT token
*/
async getOrMintOrgJwt(): Promise<string> {
if (this.orgJwt) {
return this.orgJwt;
}
return await this.mintOrgJwt();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,23 @@ export class ApexGuruService {
}

/**
* Initialize authentication
* Initialize authentication and mint Org JWT
*/
async initialize(targetOrg?: string): Promise<void> {
// 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}`);
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/code-analyzer-apexguru-engine/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,8 @@ export type ApexGuruFix = {
export type ApexGuruRequestBody = {
classContent: string; // Base64 encoded Apex class
};

export type OrgJwtResponse = {
jwt: string;
message?: string | null;
};
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
import { ApexGuruAuthService } from '../src/services/ApexGuruAuthService';
import { Org, Connection } from '@salesforce/core';

// Mock @salesforce/core
jest.mock('@salesforce/core');

describe('ApexGuruAuthService', () => {
let authService: ApexGuruAuthService;
let mockEmitLogEvent: jest.Mock;
let mockConnection: jest.Mocked<Connection>;

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 throw error when targetOrg authentication fails', async () => {
(Org.create as jest.Mock).mockRejectedValue(new Error('Org not found'));

await expect(authService.initialize({ targetOrg: 'invalid-org' }))
.rejects
.toThrow("Failed to authenticate with org 'invalid-org'");
});

// 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', () => {
Expand Down Expand Up @@ -55,4 +86,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
});
});
Loading
Loading