UNPKG

@salesforce/agents

Version:

Client side APIs for working with Salesforce agents

281 lines 12.1 kB
"use strict"; /* * 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