@salesforce/agents
Version:
Client side APIs for working with Salesforce agents
281 lines • 12.1 kB
JavaScript
;
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.setManagerForTesting = exports.managerFor = exports.ConnectionManager = void 0;
const core_1 = require("@salesforce/core");
const utils_1 = require("./utils");
/**
* Manages JWT and standard connections for agent operations.
*
* This class provides:
* - Automatic JWT creation and validation
* - Separation of JWT connection (for SFAP) and standard connection (for org operations)
* - Guard installation to prevent JWT token clobbering
* - JWT validation utilities for debugging
*
* @example
* ```typescript
* const manager = await ConnectionManager.create(connection);
*
* // Get JWT connection for SFAP calls
* const jwtConn = manager.getJwtConnection();
* await jwtConn.request({ method: 'POST', url: '/authoring/scripts', ... });
*
* // Get standard connection for org queries
* const standardConn = manager.getStandardConnection();
* await standardConn.query('SELECT Id FROM User LIMIT 1');
* ```
*/
class ConnectionManager {
jwtConnection;
standardConnection;
/**
* Private constructor. Use ConnectionManager.create() instead.
*/
constructor(jwtConnection, standardConnection) {
this.jwtConnection = jwtConnection;
this.standardConnection = standardConnection;
}
/**
* Creates a new ConnectionManager instance.
*
* Builds two separate Connection objects derived from the username on the supplied
* connection: a standard connection for org-instance operations (SOQL, tooling,
* metadata) and a JWT-upgraded connection for SFAP API calls. The supplied
* connection is read-only — it is never mutated.
*
* @param connection - The connection whose username is used to derive the new connections
* @returns A new ConnectionManager instance
* @throws {SfError} If JWT creation or validation fails, or if the connection has no username
*/
static async create(connection) {
const logger = core_1.Logger.childFromRoot('ConnectionManager');
const username = connection.getUsername();
if (!username) {
throw core_1.SfError.create({
name: 'MissingUsername',
message: 'Cannot create ConnectionManager: username not found on connection.',
});
}
logger.debug(`Creating ConnectionManager for user: ${username}`);
// Build two fresh, independent connections — one for org operations, one for SFAP JWT.
// Building from username (not from the caller's connection object) guarantees the
// caller's connection is not mutated by the JWT upgrade.
const standardConn = await this.createConnectionFromUsername(username);
const jwtSeed = await this.createConnectionFromUsername(username);
logger.debug('Standard and JWT seed connections created');
const jwtConn = await this.createAndValidateJwtConnection(jwtSeed, logger);
logger.debug('JWT connection created and validated');
return new ConnectionManager(jwtConn, standardConn);
}
/**
* Creates a fresh Connection from a username. Used for both the standard and JWT
* connections so the caller's original Connection object is never mutated.
*/
static async createConnectionFromUsername(username) {
const authInfo = await core_1.AuthInfo.create({ username });
return core_1.Connection.create({ authInfo });
}
/**
* Upgrades a connection to a JWT connection for SFAP operations and validates the result.
* The connection passed in is mutated (its accessToken is replaced with the JWT) — callers
* must pass a fresh, isolated Connection rather than a connection they care about.
*/
static async createAndValidateJwtConnection(connection, logger) {
const upgraded = await (0, utils_1.useNamedUserJwt)(connection);
logger.debug('Connection upgraded to JWT');
const validation = this.validateJwt(upgraded.accessToken ?? undefined);
if (!validation.isValid) {
logger.error('JWT validation failed:', validation);
const actions = ['Ensure your Connected App has the correct scopes: chatbot_api, sfap_api, web'];
if (validation.missingFields.length > 0) {
actions.push(`JWT missing required fields: ${validation.missingFields.join(', ')}`);
}
if (validation.isExpired) {
actions.push('JWT is expired - ensure system time is correct');
}
throw core_1.SfError.create({
name: 'InvalidJwtToken',
message: 'Failed to create valid JWT for SFAP access',
data: {
validation: {
...validation,
expiresAt: validation.expiresAt?.toISOString(),
issuedAt: validation.issuedAt?.toISOString(),
},
},
actions,
});
}
if (!validation.hasRequiredFields) {
logger.warn('JWT missing some expected fields:', validation.missingFields);
}
logger.debug('JWT validation passed', {
hasRequiredFields: validation.hasRequiredFields,
expiresAt: validation.expiresAt,
scopes: validation.scopes,
});
return upgraded;
}
/**
* Validates that a token is a proper org JWT with required fields.
*
* @param token - The JWT token to validate
* @returns Validation result with diagnostic information
*/
static validateJwt(token) {
if (!token) {
return {
isValid: false,
hasRequiredFields: false,
missingFields: ['token'],
isExpired: false,
};
}
try {
const parts = token.split('.');
if (parts.length !== 3) {
return {
isValid: false,
hasRequiredFields: false,
missingFields: ['invalid JWT format - expected 3 parts'],
isExpired: false,
};
}
// Decode payload (middle part)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
// Check for expected SFAP JWT fields
// Common JWT fields: sub (subject), iss (issuer), aud (audience), exp (expiration), iat (issued at)
// SFAP-specific: sfdc_app_id, scope
const requiredFields = ['sub', 'iss'];
const optionalButExpected = ['sfdc_app_id', 'scope', 'exp', 'iat'];
const missingFields = [
...requiredFields.filter((field) => !payload[field]),
...optionalButExpected.filter((field) => !payload[field]),
];
const expiresAt = payload.exp ? new Date(payload.exp * 1000) : undefined;
const issuedAt = payload.iat ? new Date(payload.iat * 1000) : undefined;
const isExpired = expiresAt ? expiresAt < new Date() : false;
const hasRequiredFields = requiredFields.every((field) => payload[field]);
return {
isValid: parts.length === 3 && hasRequiredFields && !isExpired,
hasRequiredFields,
missingFields,
expiresAt,
issuedAt,
isExpired,
subject: payload.sub,
issuer: payload.iss,
appId: payload.sfdc_app_id,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
scopes: payload.scope ? payload.scope.split(' ') : undefined,
};
}
catch (error) {
return {
isValid: false,
hasRequiredFields: false,
missingFields: ['JWT parse error'],
isExpired: false,
};
}
}
/**
* Gets the standard (non-JWT) connection for org-instance operations.
* Use this for SOQL queries, metadata operations, tooling API, etc.
*
* @returns The standard connection
*/
getStandardConnection() {
return this.standardConnection;
}
/**
* Gets the JWT connection for SFAP API calls.
* Use this for all requests to api.salesforce.com/einstein/ai-agent endpoints.
*
* @returns The JWT connection
*/
getJwtConnection() {
return this.jwtConnection;
}
/**
* Inspects the current JWT and provides diagnostic information.
* Useful for debugging and troubleshooting JWT-related issues.
*
* @returns JWT validation result with detailed diagnostic information
*/
inspectJwt() {
return ConnectionManager.validateJwt(this.jwtConnection.accessToken ?? undefined);
}
/**
* Refreshes the standard connection by clearing the access token and requesting a new one.
* This is useful after agent operations to ensure subsequent org operations work correctly.
*
* @throws {SfError} If the refresh fails
*/
async refreshStandardConnection() {
delete this.standardConnection.accessToken;
await this.standardConnection.refreshAuth();
}
}
exports.ConnectionManager = ConnectionManager;
// Per-Connection cache of ConnectionManager instances. Keyed by Connection object identity
// so callers never have to pass a manager around — any code that holds the caller's
// Connection can resolve its associated manager via managerFor(connection).
//
// Caching the *promise* (not the resolved manager) deduplicates concurrent first-time
// callers to a single JWT bootstrap. Using a WeakMap means entries are reclaimed when
// the caller drops the Connection, so long-running processes (extensions, language
// servers) don't accumulate stale managers.
const managerCache = new WeakMap();
/**
* Returns the ConnectionManager associated with the supplied Connection, creating one on
* the first call. Subsequent calls with the same Connection return the cached manager.
*
* Different Connection objects — even for the same username — get distinct managers,
* which is the desired behavior: the caller's connection identity is the unit of trust.
*
* @param connection The caller-supplied Connection used as the cache key
* @returns A promise resolving to the manager for this connection
*/
const managerFor = async (connection) => {
let entry = managerCache.get(connection);
if (!entry) {
entry = ConnectionManager.create(connection).catch((err) => {
// Evict the failed bootstrap so a subsequent call can retry rather than re-throwing
// the cached error indefinitely.
managerCache.delete(connection);
throw err;
});
managerCache.set(connection, entry);
}
return entry;
};
exports.managerFor = managerFor;
/**
* Test-only helper: pre-populate the cache so tests can substitute a fake manager
* without exercising the real JWT bootstrap. Callers in production code should use
* managerFor() instead.
*
* @internal
*/
const setManagerForTesting = (connection, manager) => {
managerCache.set(connection, Promise.resolve(manager));
};
exports.setManagerForTesting = setManagerForTesting;
//# sourceMappingURL=connectionManager.js.map