Skip to content

Commit d09fa83

Browse files
NEW @W-22178016@ Implement Org JWT Minting (#462)
1 parent e2e6970 commit d09fa83

9 files changed

Lines changed: 256 additions & 57 deletions

File tree

packages/code-analyzer-apexguru-engine/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@salesforce/code-analyzer-apexguru-engine",
33
"description": "ApexGuru Engine Package for the Salesforce Code Analyzer",
4-
"version": "0.37.0",
4+
"version": "0.38.0-SNAPSHOT",
55
"author": "The Salesforce Code Analyzer Team",
66
"license": "BSD-3-Clause",
77
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",

packages/code-analyzer-apexguru-engine/src/config.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import { ConfigDescription, ConfigValueExtractor } from '@salesforce/code-analyz
55
* Currently minimal - authentication is handled via SF CLI
66
*/
77
export type ApexGuruEngineConfig = {
8+
/**
9+
* Target Salesforce org username or alias
10+
* If not specified, uses the default SF CLI org
11+
*/
12+
target_org?: string;
13+
814
/**
915
* Maximum time to wait for ApexGuru API response (in milliseconds)
1016
* Default: 120000 (2 minutes)
@@ -46,7 +52,7 @@ export const DEFAULT_APEXGURU_ENGINE_CONFIG: ApexGuruEngineConfig = {
4652
* Configuration schema description for ApexGuru Engine
4753
*/
4854
export const APEXGURU_ENGINE_CONFIG_DESCRIPTION: ConfigDescription = {
49-
overview: 'Configuration for ApexGuru Engine. Authentication is handled via Salesforce CLI (sf org login web).',
55+
overview: 'Configuration for ApexGuru Engine. Authentication is handled via Salesforce CLI (sf org login web). Use --target-org flag to specify the org.',
5056
fieldDescriptions: {
5157
api_timeout_ms: {
5258
descriptionText: 'Maximum time to wait for ApexGuru API response (in milliseconds). Default: 120000 (2 minutes)',
@@ -85,6 +91,11 @@ export async function validateAndNormalizeConfig(
8591
'api_backoff_multiplier'
8692
]);
8793

94+
// Extract target org from CLI flag only
95+
// - If user passes --target-org: use that org
96+
// - If user doesn't pass --target-org: undefined (auth service uses default SF CLI org)
97+
const targetOrg: string | undefined = process.env.CODE_ANALYZER_TARGET_ORG;
98+
8899
// Extract and validate timeout
89100
const apiTimeoutMs: number = configValueExtractor.extractNumber(
90101
'api_timeout_ms',
@@ -130,6 +141,7 @@ export async function validateAndNormalizeConfig(
130141
}
131142

132143
return {
144+
target_org: targetOrg,
133145
api_timeout_ms: apiTimeoutMs,
134146
api_initial_retry_ms: apiInitialRetryMs,
135147
api_max_retry_ms: apiMaxRetryMs,

packages/code-analyzer-apexguru-engine/src/engine.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,9 @@ export class ApexGuruEngine extends EngineEventEmitter implements Engine {
195195
* Target org can be set via SF_TARGET_ORG environment variable.
196196
*/
197197
private getTargetOrgFromEnvironment(): string | undefined {
198-
// Check environment variable
199-
if (process.env.SF_TARGET_ORG) {
200-
return process.env.SF_TARGET_ORG;
201-
}
202-
203-
// Return undefined to use default org from SF CLI
204-
return undefined;
198+
// Return target_org from config (set via CLI --target-org flag or config file)
199+
// If undefined, ApexGuruAuthService will use default SF CLI org
200+
return this.config.target_org;
205201
}
206202

207203
}
Lines changed: 139 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,69 @@
1-
2-
3-
import { AuthInfo, Connection } from '@salesforce/core';
1+
import { Connection, Org } from '@salesforce/core';
42
import { LogLevel } from '@salesforce/code-analyzer-engine-api';
5-
import { AuthConfig } from '../types';
6-
7-
/**
8-
* TEMPORARY: Hardcoded credentials for testing
9-
* TODO: Implement SF CLI, env vars, and OAuth in future PR
10-
* NEVER commit real credentials - use 'YOUR_ACCESS_TOKEN_HERE' as placeholder
11-
*/
12-
const HARDCODED_ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN_HERE'; // Get from: sf org display --verbose
13-
const HARDCODED_INSTANCE_URL = 'https://yourorg.my.salesforce.com'; // e.g., https://yourorg.my.salesforce.com
3+
import { AuthConfig, OrgJwtResponse } from '../types';
144

155
/**
16-
* Handles authentication to Salesforce orgs for ApexGuru API access
17-
* TODO: Currently uses hardcoded credentials only. Implement proper auth in future PR.
6+
* Handles authentication to Salesforce orgs for ApexGuru API access.
187
*/
198
export class ApexGuruAuthService {
9+
// Configuration constants
10+
private static readonly DEFAULT_FEATURE_ID = 'CodeAnalyzer';
11+
private static readonly ORG_JWT_ENDPOINT_PATH = '/ide/auth';
12+
2013
private connection?: Connection;
14+
private orgJwt?: string;
2115
private readonly emitLogEvent: (logLevel: LogLevel, message: string) => void;
2216

2317
constructor(emitLogEvent: (logLevel: LogLevel, message: string) => void = () => {}) {
2418
this.emitLogEvent = emitLogEvent;
2519
}
2620

2721
/**
28-
* Initialize connection to Salesforce org
29-
* TODO: Implement SF CLI, env vars, and OAuth in future PR
30-
* @param _config - Auth configuration (currently unused, for future implementation)
22+
* Initialize connection to Salesforce org using one of two methods:
23+
*
24+
* Method 1: SF CLI org with --target-org flag
25+
* config.targetOrg = 'myorg' or 'user@example.com'
26+
*
27+
* Method 2: SF CLI default org (fallback)
28+
* No config provided - uses SF CLI default org
29+
*
30+
* @param config - Auth configuration
3131
*/
32-
async initialize(_config: AuthConfig): Promise<void> {
33-
// Use hardcoded credentials (temporary implementation)
34-
this.emitLogEvent(LogLevel.Warn, '⚠️ Using HARDCODED authentication credentials (for testing)');
32+
async initialize(config: AuthConfig): Promise<void> {
33+
// Method 1: SF CLI org (alias or username) via --target-org flag
34+
if (config.targetOrg) {
35+
this.emitLogEvent(LogLevel.Fine, `Authenticating with org: ${config.targetOrg}`);
36+
try {
37+
const org = await Org.create({ aliasOrUsername: config.targetOrg });
38+
this.connection = org.getConnection();
39+
this.emitLogEvent(LogLevel.Fine, `Successfully authenticated to org`);
40+
return;
41+
} catch {
42+
this.emitLogEvent(LogLevel.Error, `Failed to authenticate with org: ${config.targetOrg}`);
43+
throw new Error(
44+
`Failed to authenticate with org '${config.targetOrg}'. ` +
45+
'Please verify the org alias/username and ensure you are authenticated:\n' +
46+
' sf org list\n' +
47+
' sf org login web'
48+
);
49+
}
50+
}
3551

36-
// Validate that credentials were actually set
37-
if (!HARDCODED_ACCESS_TOKEN || HARDCODED_ACCESS_TOKEN.includes('YOUR_ACCESS_TOKEN')) {
52+
// Method 2: SF CLI default org (fallback)
53+
this.emitLogEvent(LogLevel.Fine, 'No target org specified, using default org');
54+
try {
55+
const org = await Org.create({});
56+
this.connection = org.getConnection();
57+
this.emitLogEvent(LogLevel.Fine, 'Successfully authenticated to default org');
58+
} catch {
59+
this.emitLogEvent(LogLevel.Error, 'Failed to authenticate: No default org found');
3860
throw new Error(
39-
'Hardcoded credentials not set! Edit ApexGuruAuthService.ts and set:\n' +
40-
' - HARDCODED_ACCESS_TOKEN (get from: sf org display --verbose)\n' +
41-
' - HARDCODED_INSTANCE_URL (e.g., https://yourorg.my.salesforce.com)'
61+
'No default org found. Please either:\n' +
62+
' 1. Set a default org: sf config set target-org <org-alias>\n' +
63+
' 2. Pass --target-org flag: sf code-analyzer run --target-org <org-alias> ...\n' +
64+
' 3. Authenticate to an org: sf org login web'
4265
);
4366
}
44-
45-
this.connection = await Connection.create({
46-
authInfo: await AuthInfo.create({
47-
accessTokenOptions: {
48-
accessToken: HARDCODED_ACCESS_TOKEN,
49-
instanceUrl: HARDCODED_INSTANCE_URL
50-
}
51-
})
52-
});
5367
}
5468

5569
/**
@@ -86,4 +100,95 @@ export class ApexGuruAuthService {
86100
getApiVersion(): string {
87101
return this.getConnection().version || '64.0';
88102
}
103+
104+
/**
105+
* Helper method to perform fetch with logging
106+
* @param endpoint - The endpoint URL
107+
* @param logMessage - Log message to emit before fetch
108+
* @param options - Fetch options
109+
* @returns Promise<Response> - The fetch response
110+
*/
111+
private async fetchWithLogging(
112+
endpoint: string,
113+
logMessage: string,
114+
options: RequestInit
115+
): Promise<Response> {
116+
this.emitLogEvent(LogLevel.Fine, logMessage);
117+
return await fetch(endpoint, options);
118+
}
119+
120+
/**
121+
* Mint an Org JWT token for SFAP API access
122+
*
123+
* @param featureId - Feature ID for tracking (default: CodeAnalyzer)
124+
* @returns Promise<string> - The Org JWT token
125+
* @throws Error if minting fails
126+
*/
127+
async mintOrgJwt(featureId: string = ApexGuruAuthService.DEFAULT_FEATURE_ID): Promise<string> {
128+
const accessToken = this.getAccessToken();
129+
const instanceUrl = this.getInstanceUrl();
130+
const endpoint = `${instanceUrl}${ApexGuruAuthService.ORG_JWT_ENDPOINT_PATH}`;
131+
132+
const response = await this.fetchWithLogging(
133+
endpoint,
134+
'Minting Org JWT for SFAP API access',
135+
{
136+
method: 'POST',
137+
headers: {
138+
'Accept': 'application/json',
139+
'Authorization': `Bearer ${accessToken}`,
140+
'X-Feature-Id': featureId,
141+
'Content-Type': 'application/json'
142+
}
143+
}
144+
);
145+
146+
try {
147+
148+
if (!response.ok) {
149+
const errorText = await response.text();
150+
this.emitLogEvent(LogLevel.Error, `Failed to mint Org JWT: HTTP ${response.status}`);
151+
throw new Error(
152+
`Failed to mint Org JWT: ${response.status} ${response.statusText}. ` +
153+
`Response: ${errorText}`
154+
);
155+
}
156+
157+
const data = await response.json() as OrgJwtResponse;
158+
159+
if (!data.jwt) {
160+
this.emitLogEvent(LogLevel.Error, 'Org JWT response missing jwt field');
161+
throw new Error('Org JWT response missing jwt field');
162+
}
163+
164+
this.orgJwt = data.jwt;
165+
this.emitLogEvent(LogLevel.Fine, 'Successfully minted Org JWT');
166+
return data.jwt;
167+
168+
} catch (error) {
169+
const errorMessage = error instanceof Error ? error.message : String(error);
170+
this.emitLogEvent(LogLevel.Error, 'Org JWT minting failed');
171+
throw new Error(`Org JWT minting failed: ${errorMessage}`);
172+
}
173+
}
174+
175+
/**
176+
* Get the cached Org JWT token
177+
* @returns The Org JWT if available, undefined otherwise
178+
*/
179+
getOrgJwt(): string | undefined {
180+
return this.orgJwt;
181+
}
182+
183+
/**
184+
* Get or mint the Org JWT token
185+
* If already minted, returns the cached token. Otherwise, mints a new one.
186+
* @returns Promise<string> - The Org JWT token
187+
*/
188+
async getOrMintOrgJwt(): Promise<string> {
189+
if (this.orgJwt) {
190+
return this.orgJwt;
191+
}
192+
return await this.mintOrgJwt();
193+
}
89194
}

packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,23 @@ export class ApexGuruService {
4141
}
4242

4343
/**
44-
* Initialize authentication
44+
* Initialize authentication and mint Org JWT
4545
*/
4646
async initialize(targetOrg?: string): Promise<void> {
47+
// Initialize auth service with SF CLI
4748
await this.authService.initialize({ targetOrg });
49+
50+
// Mint Org JWT for SFAP API access
51+
const orgJwt = await this.authService.mintOrgJwt();
52+
53+
try {
54+
const jwtParts = orgJwt.split('.');
55+
if (jwtParts.length === 3) {
56+
//const payload = JSON.parse(Buffer.from(jwtParts[1], 'base64').toString());
57+
}
58+
} catch (error) {
59+
this.emitLogEvent(LogLevel.Warn, `Could not decode JWT payload: ${error}`);
60+
}
4861
}
4962

5063
/**

packages/code-analyzer-apexguru-engine/src/types/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,8 @@ export type ApexGuruFix = {
9999
export type ApexGuruRequestBody = {
100100
classContent: string; // Base64 encoded Apex class
101101
};
102+
103+
export type OrgJwtResponse = {
104+
jwt: string;
105+
message?: string | null;
106+
};

0 commit comments

Comments
 (0)