LCOV - code coverage report
Current view: top level - dashql-app/src/connections/salesforce - salesforce_connection_setup.ts Coverage Total Hit
Test: DashQL 915828b Lines: 95.8 % 142 136
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as connection from '@ankoh/dashql-jsonschema/connection.js';
       2              : 
       3              : import { dateToTimestamp } from '../proto_helper.js';
       4              : 
       5              : import {
       6              :     SETUP_CANCELLED,
       7              :     SETUP_FAILED,
       8              :     SETUP_STARTED,
       9              :     GENERATED_PKCE_CHALLENGE,
      10              :     GENERATING_PKCE_CHALLENGE,
      11              :     OAUTH_NATIVE_LINK_OPENED,
      12              :     OAUTH_WEB_WINDOW_CLOSED,
      13              :     OAUTH_WEB_WINDOW_OPENED,
      14              :     RECEIVED_CORE_AUTH_CODE,
      15              :     RECEIVED_CORE_AUTH_TOKEN,
      16              :     RECEIVED_CORE_USER_INFO,
      17              :     RECEIVED_DATA_CLOUD_ACCESS_TOKEN,
      18              :     REQUESTING_CORE_AUTH_TOKEN,
      19              :     REQUESTING_DATA_CLOUD_ACCESS_TOKEN,
      20              :     SalesforceConnectionStateAction,
      21              :     SF_CHANNEL_READY,
      22              :     SF_CHANNEL_SETUP_STARTED,
      23              : } from './salesforce_connection_state.js';
      24              : import { PlatformType } from '../../platform/platform_type.js';
      25              : import { SalesforceConnectorConfig } from '../connector_configs.js';
      26              : import { collectSalesforceAuthInfo, SalesforceApiClientInterface, SalesforceDatabaseChannel } from './salesforce_api_client.js';
      27              : import { Dispatch } from '../../utils/variant.js';
      28              : import { Logger, stringifyError } from '../../platform/logger/logger.js';
      29              : import { PlatformEventListener } from '../../platform/events/event_listener.js';
      30              : import { RESET_CONNECTION } from '../connection_state.js';
      31              : import { AttachedDatabase, HyperDatabaseChannel, HyperDatabaseClient, HyperDatabaseConnectionContext } from '../hyper/hyperdb_grpc_client.js';
      32              : import { authenticateSalesforce, SalesforceAuthenticationProgress } from './salesforce_authentication.js';
      33              : import { ConnectorType } from '../connector_info.js';
      34              : import { getSidbAttachedDatabase } from './salesforce_catalog_update.js';
      35              : 
      36            1 : const LOG_CTX = "salesforce_setup";
      37              : 
      38              : /// The connectors that share the Salesforce setup
      39              : export type SalesforceConnectorType = ConnectorType.SALESFORCE_DATA_CLOUD | ConnectorType.SIDB;
      40              : 
      41              : // We use the web-server OAuth Flow with or without consumer secret.
      42              : //
      43              : // !! Don't embed an External Client App secret !!
      44              : //
      45              : // For untrusted clients, like this SPA, the web server OAuth flow can be configure to NOT require a consumer secret but
      46              : // still use PKCE. PKCE makes this more preferrable than the alternative user-agent flow for untrusted clients since it
      47              : // ensures that the application that starts the authentication flow is the same one that finishes it.
      48              : // (Salesforce discourages using the user-agent flow, see https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_user_agent_flow.htm&type=5)
      49              : //
      50              : // Make sure this is checked (should be by default):
      51              : //      Setup > App Manager > Your App > "Require Proof Key for Code Exchange (PKCE)"
      52              : // Uncheck this:
      53              : //      Setup > App Manager > Your App > "Require Secret for Web Server Flow"
      54              : // What you'll eventually need as well (not, if you only use the native apps):
      55              : //      Setup > CORS > Enable CORS for OAuth endpoints
      56              : //      Setup > CORS > Allowed Origins List > Add your Origin
      57              : //
      58              : // Docs:
      59              : //  - Web Server Flow: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_web_server_flow.htm&type=5
      60              : //  - User Agent Flow: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_user_agent_flow.htm&type=5
      61              : //  - PKCE: https://oauth.net/2/pkce/
      62              : //
      63              : // PKCE flow:
      64              : //  1. Client creates the code_verifier. (RFC 7636, Section 4.1)
      65              : //  2. Client creates the code_challenge by transforming the code_verifier using S256 encryption. (RFC 7636, Section 4.2)
      66              : //  3. Client sends the code_challenge and code_challenge_method with the initial authorization request. (RFC 7636, Section 4.3)
      67              : //  4. Server responds with an authorization_code. (RFC 7636, Section 4.4)
      68              : //  5. Client sends authorization_code and code_verifier to the token endpoint. (RFC 7636, Section 4.5)
      69              : //  6. Server transforms the code_verifier using the code_challenge_method from the initial authorization request and checks the result against the code_challenge. If the value of both strings match, then the server has verified that the requests came from the same client and will issue an access_token. (RFC 7636, Section 4.6)
      70              : 
      71            1 : export async function setupSalesforceConnection(modifyState: Dispatch<SalesforceConnectionStateAction>, logger: Logger, connector: SalesforceConnectorType, params: connection.SalesforceConnectionParams, config: SalesforceConnectorConfig, platformType: PlatformType, apiClient: SalesforceApiClientInterface, httpClient: HyperDatabaseClient | null, appEvents: PlatformEventListener, forceReLogin: boolean, abortSignal: AbortSignal): Promise<SalesforceDatabaseChannel> {
      72            1 :     params = structuredClone(params);
      73            1 :     let hyperChannel: HyperDatabaseChannel;
      74            1 :     let sfChannel: SalesforceDatabaseChannel;
      75            1 :     try {
      76              :         // Start the authorization process
      77            1 :         modifyState({
      78            1 :             type: SETUP_STARTED,
      79            1 :             value: params,
      80            1 :         });
      81            1 :         abortSignal.throwIfAborted();
      82            1 :         if (!config.auth?.oauthRedirect) {
      83            1 :             throw new Error(`missing oauth redirect url`);
      84              :         }
      85            1 :         const onAuthenticationProgress = (progress: SalesforceAuthenticationProgress) => {
      86            1 :             switch (progress.stage) {
      87            1 :                 case 'GENERATING_PKCE_CHALLENGE':
      88            1 :                     modifyState({ type: GENERATING_PKCE_CHALLENGE, value: null });
      89            1 :                     break;
      90            1 :                 case 'GENERATED_PKCE_CHALLENGE':
      91            1 :                     modifyState({ type: GENERATED_PKCE_CHALLENGE, value: progress.pkceChallenge });
      92            1 :                     break;
      93            0 :                 case 'OAUTH_NATIVE_LINK_OPENED':
      94            0 :                     modifyState({ type: OAUTH_NATIVE_LINK_OPENED, value: null });
      95            0 :                     break;
      96            1 :                 case 'OAUTH_WEB_WINDOW_OPENED':
      97            1 :                     modifyState({ type: OAUTH_WEB_WINDOW_OPENED, value: null });
      98            1 :                     break;
      99            1 :                 case 'OAUTH_WEB_WINDOW_CLOSED':
     100            1 :                     modifyState({ type: OAUTH_WEB_WINDOW_CLOSED, value: null });
     101            1 :                     break;
     102            1 :                 case 'RECEIVED_CORE_AUTH_CODE':
     103            1 :                     modifyState({
     104            1 :                         type: RECEIVED_CORE_AUTH_CODE,
     105            1 :                         value: { token: progress.code, createdAt: dateToTimestamp(new Date())! },
     106            1 :                     });
     107            1 :                     break;
     108            1 :                 case 'REQUESTING_CORE_AUTH_TOKEN':
     109            1 :                     modifyState({ type: REQUESTING_CORE_AUTH_TOKEN, value: null });
     110            1 :                     break;
     111            1 :                 case 'RECEIVED_CORE_AUTH_TOKEN':
     112            1 :                     modifyState({ type: RECEIVED_CORE_AUTH_TOKEN, value: progress.coreAccessToken });
     113            1 :                     break;
     114            1 :                 case 'RECEIVED_CORE_USER_INFO':
     115            1 :                     modifyState({ type: RECEIVED_CORE_USER_INFO, value: progress.coreUserInfo });
     116            1 :                     break;
     117            1 :                 case 'REQUESTING_DATA_CLOUD_ACCESS_TOKEN':
     118            1 :                     modifyState({ type: REQUESTING_DATA_CLOUD_ACCESS_TOKEN, value: null });
     119            1 :                     break;
     120            1 :                 case 'RECEIVED_DATA_CLOUD_ACCESS_TOKEN':
     121            1 :                     modifyState({ type: RECEIVED_DATA_CLOUD_ACCESS_TOKEN, value: progress.dataCloudAccessToken });
     122              :                     break;
     123            1 :             }
     124            1 :         };
     125            1 :         const { coreAccessToken, dataCloudAccessToken: dcToken } = await authenticateSalesforce({
     126            1 :             logger,
     127            1 :             params,
     128            1 :             authConfig: config.auth,
     129            1 :             platformType,
     130            1 :             apiClient,
     131            1 :             appEvents,
     132            1 :             forceReLogin,
     133            1 :             abortSignal,
     134            1 :             onProgress: onAuthenticationProgress,
     135            1 :         });
     136            1 :         abortSignal.throwIfAborted();
     137              : 
     138              :         // Start the channel setup, Salesforce connections speak to Hyper over http
     139            1 :         const connParams: connection.HyperConnectionParams = {
     140            1 :             protocol: 'V3_HTTP',
     141            1 :             endpoint: dcToken.instanceUrl ?? "",
     142            1 :             tls: {
     143            1 :                 clientKeyPath: "",
     144            1 :                 clientCertPath: "",
     145            1 :                 caCertsPath: ""
     146            1 :             },
     147            1 :             attachedDatabases: [],
     148            1 :             metadata: {
     149            1 :                 message: "",
     150            1 :                 details: {}
     151            1 :             } as any,
     152            1 :         };
     153            1 :         modifyState({
     154            1 :             type: SF_CHANNEL_SETUP_STARTED,
     155            1 :             value: connParams,
     156            1 :         });
     157            1 :         abortSignal.throwIfAborted()
     158              : 
     159              :         // Static connection context.
     160              :         // Inject the database name, the audience header and the bearer token
     161            1 :         const authInfo = collectSalesforceAuthInfo(coreAccessToken, dcToken);
     162              :         // SIDB queries the configured Hyper Cloud database
     163            1 :         const attachedDatabases: AttachedDatabase[] = [];
     164            1 :         if (connector === ConnectorType.SIDB) {
     165            1 :             attachedDatabases.push(getSidbAttachedDatabase(params));
     166              :         }
     167            1 :         const connectionContext: HyperDatabaseConnectionContext = {
     168            1 :             getAttachedDatabases(): AttachedDatabase[] {
     169            1 :                 return attachedDatabases;
     170            1 :             },
     171            1 :             async getRequestMetadata(): Promise<Record<string, string>> {
     172            1 :                 return {
     173            1 :                     audience: authInfo?.offcoreTenantId ?? "",
     174            1 :                     authorization: `Bearer ${authInfo?.offcoreRawJwt}`,
     175            1 :                 };
     176            1 :             },
     177            0 :             getQueryParameters(): Record<string, string> {
     178            0 :                 return {};
     179            0 :             }
     180            1 :         };
     181              : 
     182              :         // Create the channel
     183            1 :         if (!httpClient) throw new Error(`No client available for protocol ${connParams.protocol}`);
     184            1 :         hyperChannel = await httpClient.connect(connParams, connectionContext);
     185            1 :         sfChannel = new SalesforceDatabaseChannel(apiClient, coreAccessToken, dcToken, hyperChannel);
     186            1 :         abortSignal.throwIfAborted();
     187              : 
     188              :         // Mark the channel as ready
     189            1 :         modifyState({
     190            1 :             type: SF_CHANNEL_READY,
     191            1 :             value: sfChannel,
     192            1 :         });
     193            1 :         abortSignal.throwIfAborted();
     194              : 
     195            1 :     } catch (error: any) {
     196            1 :         if (error.name === 'AbortError') {
     197            1 :             logger.info("Cancelled OAuth flow", {}, LOG_CTX);
     198            1 :             modifyState({
     199            1 :                 type: SETUP_CANCELLED,
     200            1 :                 value: error,
     201            1 :             });
     202            1 :         } else if (error instanceof Error) {
     203            1 :             logger.warn("Failed OAuth flow", { "error": stringifyError(error) }, LOG_CTX);
     204            1 :             modifyState({
     205            1 :                 type: SETUP_FAILED,
     206            1 :                 value: {
     207            1 :                     message: error.message,
     208            1 :                 },
     209            1 :             });
     210            1 :         }
     211              :         // Rethrow the error
     212            1 :         throw error;
     213            1 :     }
     214              : 
     215            1 :     return sfChannel;
     216            1 : }
     217              : 
     218              : export interface SalesforceSetupApi {
     219              :     setup(dispatch: Dispatch<SalesforceConnectionStateAction>, connector: SalesforceConnectorType, params: connection.SalesforceConnectionParams, abortSignal: AbortSignal): Promise<SalesforceDatabaseChannel>
     220              :     reset(dispatch: Dispatch<SalesforceConnectionStateAction>): Promise<void>
     221              : }
     222              : 
     223            1 : export function createSalesforceSetup(httpClient: HyperDatabaseClient | null, salesforceApi: SalesforceApiClientInterface, platformType: PlatformType, appEvents: PlatformEventListener, config: SalesforceConnectorConfig, forceReLogin: boolean, logger: Logger): (SalesforceSetupApi | null) {
     224            1 :     const setup = async (updateState: Dispatch<SalesforceConnectionStateAction>, connector: SalesforceConnectorType, params: connection.SalesforceConnectionParams, abort: AbortSignal) => {
     225            1 :         return setupSalesforceConnection(updateState, logger, connector, params, config, platformType, salesforceApi, httpClient, appEvents, forceReLogin, abort);
     226            1 :     };
     227            1 :     const reset = async (updateState: Dispatch<SalesforceConnectionStateAction>) => {
     228            1 :         updateState({
     229            1 :             type: RESET_CONNECTION,
     230            1 :             value: null,
     231            1 :         });
     232            1 :     };
     233            1 :     return { setup, reset };
     234            1 : };
        

Generated by: LCOV version 2.0-1