Line data Source code
1 : import * as connection from '@ankoh/dashql-jsonschema/connection.js';
2 : import * as auth from '@ankoh/dashql-jsonschema/auth.js';
3 :
4 : import type { DetailedError } from '../../utils/error.js';
5 :
6 : import { VariantKind } from '../../utils/variant.js';
7 : import { SalesforceDatabaseChannel } from './salesforce_api_client.js';
8 : import { SALESFORCE_DATA_CLOUD_CONNECTOR, SIDB_CONNECTOR } from '../connector_info.js';
9 : import type { SalesforceDetailsType } from '../connection_state_details.js';
10 : import {
11 : ConnectionHealth,
12 : ConnectionState,
13 : ConnectionStatus,
14 : DELETE_CONNECTION,
15 : RESET_CONNECTION,
16 : } from '../connection_state.js';
17 : import { dateToTimestamp } from "../proto_helper.js";
18 :
19 : /// The Salesforce connection state
20 : export interface SalesforceConnectionStateDetails {
21 : /// The connection details
22 : proto: connection.SalesforceConnectionDetails;
23 : /// The Hyper connection
24 : channel: SalesforceDatabaseChannel | null;
25 : }
26 :
27 : /// Create the connection state details
28 1 : export function createSalesforceConnectionStateDetails(params: connection.SalesforceConnectionParams): SalesforceConnectionStateDetails {
29 1 : return {
30 1 : proto: {
31 1 : setupTimings: {},
32 1 : setupParams: params
33 1 : },
34 1 : channel: null
35 1 : };
36 1 : }
37 :
38 : /// Unpack the connection state from the variant
39 1 : export function getSalesforceConnectionDetails(state: ConnectionState | null): SalesforceConnectionStateDetails | null {
40 1 : if (state == null) return null;
41 1 : switch (state.details.type) {
42 1 : case SALESFORCE_DATA_CLOUD_CONNECTOR:
43 1 : case SIDB_CONNECTOR:
44 1 : return state.details.value;
45 1 : default: return null;
46 1 : }
47 1 : }
48 :
49 1 : export const SETUP_CANCELLED = Symbol('AUTH_CANCELLED');
50 1 : export const SETUP_FAILED = Symbol('AUTH_FAILED');
51 1 : export const SETUP_STARTED = Symbol('AUTH_STARTED');
52 :
53 1 : export const SF_CHANNEL_SETUP_CANCELLED = Symbol('SF_CHANNEL_SETUP_CANCELLED');
54 1 : export const SF_CHANNEL_SETUP_FAILED = Symbol('SF_CHANNEL_SETUP_FAILED');
55 1 : export const SF_CHANNEL_SETUP_STARTED = Symbol('SF_CHANNEL_SETUP_STARTED');
56 1 : export const SF_CHANNEL_READY = Symbol('SF_CHANNEL_READY');
57 :
58 1 : export const GENERATING_PKCE_CHALLENGE = Symbol('GENERATING_PKCE_CHALLENGE');
59 1 : export const GENERATED_PKCE_CHALLENGE = Symbol('GENERATED_PKCE_CHALLENGE');
60 1 : export const OAUTH_NATIVE_LINK_OPENED = Symbol('OAUTH_NATIVE_LINK_OPENED');
61 1 : export const OAUTH_WEB_WINDOW_CLOSED = Symbol('OAUTH_WEB_WINDOW_CLOSED');
62 1 : export const OAUTH_WEB_WINDOW_OPENED = Symbol('OAUTH_WEB_WINDOW_OPENED');
63 1 : export const RECEIVED_CORE_AUTH_CODE = Symbol('RECEIVED_AUTH_CODE');
64 1 : export const REQUESTING_CORE_AUTH_TOKEN = Symbol('REQUESTING_CORE_AUTH_TOKEN');
65 1 : export const RECEIVED_CORE_AUTH_TOKEN = Symbol('RECEIVED_CORE_ACCESS_TOKEN');
66 1 : export const RECEIVED_CORE_USER_INFO = Symbol('RECEIVED_CORE_USER_INFO');
67 1 : export const REQUESTING_DATA_CLOUD_ACCESS_TOKEN = Symbol('REQUESTING_DATA_CLOUD_ACCESS_TOKEN');
68 1 : export const RECEIVED_DATA_CLOUD_ACCESS_TOKEN = Symbol('RECEIVED_DATA_CLOUD_ACCESS_TOKEN');
69 :
70 : export type SalesforceConnectionStateAction =
71 : | VariantKind<typeof SETUP_CANCELLED, DetailedError>
72 : | VariantKind<typeof SETUP_FAILED, DetailedError>
73 : | VariantKind<typeof SETUP_STARTED, connection.SalesforceConnectionParams>
74 : | VariantKind<typeof GENERATED_PKCE_CHALLENGE, auth.OAuthPKCEChallenge>
75 : | VariantKind<typeof GENERATING_PKCE_CHALLENGE, null>
76 : | VariantKind<typeof OAUTH_NATIVE_LINK_OPENED, null>
77 : | VariantKind<typeof OAUTH_WEB_WINDOW_CLOSED, null>
78 : | VariantKind<typeof OAUTH_WEB_WINDOW_OPENED, null>
79 : | VariantKind<typeof RECEIVED_CORE_AUTH_CODE, auth.TemporaryToken>
80 : | VariantKind<typeof RECEIVED_CORE_AUTH_TOKEN, connection.SalesforceCoreAccessToken>
81 : | VariantKind<typeof RECEIVED_CORE_USER_INFO, connection.SalesforceCoreUserInfo>
82 : | VariantKind<typeof RECEIVED_DATA_CLOUD_ACCESS_TOKEN, connection.SalesforceDataCloudAccessToken>
83 : | VariantKind<typeof REQUESTING_CORE_AUTH_TOKEN, null>
84 : | VariantKind<typeof REQUESTING_DATA_CLOUD_ACCESS_TOKEN, null>
85 : | VariantKind<typeof SF_CHANNEL_READY, SalesforceDatabaseChannel>
86 : | VariantKind<typeof SF_CHANNEL_SETUP_CANCELLED, DetailedError>
87 : | VariantKind<typeof SF_CHANNEL_SETUP_FAILED, DetailedError>
88 : | VariantKind<typeof SF_CHANNEL_SETUP_STARTED, connection.HyperConnectionParams>
89 : | VariantKind<typeof RESET_CONNECTION, null>
90 : | VariantKind<typeof DELETE_CONNECTION, null>
91 : ;
92 :
93 1 : export function reduceSalesforceConnectionState(state: ConnectionState, action: SalesforceConnectionStateAction): ConnectionState | null {
94 1 : const details = state.details.value as SalesforceConnectionStateDetails;
95 1 : const detailsType = state.details.type as SalesforceDetailsType;
96 1 : let next: ConnectionState | null = null;
97 1 : switch (action.type) {
98 1 : case DELETE_CONNECTION:
99 1 : case RESET_CONNECTION:
100 1 : details.channel?.close();
101 1 : next = {
102 1 : ...state,
103 1 : details: {
104 1 : type: detailsType,
105 1 : value: {
106 1 : ...details,
107 1 : proto: {
108 1 : ...details.proto,
109 1 : setupTimings: {},
110 1 : setupParams: details.proto.setupParams,
111 1 : setupError: undefined,
112 1 : channelError: undefined,
113 1 : healthCheckError: undefined,
114 1 : },
115 1 : channel: null,
116 1 : }
117 1 : },
118 1 : };
119 1 : break;
120 1 : case SETUP_STARTED: {
121 1 : const requestedAt = Date.now();
122 1 : const newDetails: SalesforceConnectionStateDetails = {
123 1 : proto: {
124 1 : ...details.proto,
125 1 : setupTimings: {
126 1 : ...details.proto.setupTimings,
127 1 : authStartedAt: dateToTimestamp(new Date()),
128 1 : },
129 1 : setupParams: action.value,
130 1 : setupError: undefined,
131 1 : channelError: undefined,
132 1 : healthCheckError: undefined,
133 1 : oauthState: {
134 1 : oauthParams: {
135 1 : instanceUrl: action.value.instanceUrl,
136 1 : appConsumerKey: action.value.appConsumerKey,
137 1 : requestedAt,
138 1 : expiresAt: requestedAt + (2 * 60 * 60 * 1000), // 2 hours
139 1 : },
140 1 : oauthPkce: details.proto.oauthState?.oauthPkce ?? ({ value: "", verifier: "" } as auth.OAuthPKCEChallenge),
141 1 : coreAuthCode: details.proto.oauthState?.coreAuthCode ?? ({ token: "", createdAt: new Date().toISOString() } as auth.TemporaryToken),
142 1 : coreAccessToken: details.proto.oauthState?.coreAccessToken ?? ({} as connection.SalesforceCoreAccessToken),
143 1 : dataCloudAccessToken: details.proto.oauthState?.dataCloudAccessToken ?? ({} as connection.SalesforceDataCloudAccessToken),
144 1 : }
145 1 : },
146 1 : channel: null,
147 1 : };
148 1 : next = {
149 1 : ...state,
150 1 : connectionStatus: ConnectionStatus.AUTH_STARTED,
151 1 : connectionHealth: ConnectionHealth.CONNECTING,
152 1 : metrics: state.metrics,
153 1 : details: {
154 1 : type: detailsType,
155 1 : value: newDetails,
156 1 : },
157 1 : };
158 1 : break
159 1 : }
160 1 : case SETUP_CANCELLED:
161 1 : next = {
162 1 : ...state,
163 1 : connectionStatus: ConnectionStatus.AUTH_CANCELLED,
164 1 : connectionHealth: ConnectionHealth.CANCELLED,
165 :
166 1 : details: {
167 1 : type: detailsType,
168 1 : value: {
169 1 : ...details,
170 1 : proto: {
171 1 : ...details.proto,
172 1 : setupTimings: {
173 1 : ...details.proto.setupTimings,
174 1 : authCancelledAt: dateToTimestamp(new Date()),
175 1 : },
176 1 : setupError: action.value
177 1 : },
178 1 : channel: null,
179 1 : }
180 1 : }
181 1 : };
182 1 : break;
183 1 : case SETUP_FAILED:
184 1 : next = {
185 1 : ...state,
186 1 : connectionStatus: ConnectionStatus.AUTH_FAILED,
187 1 : connectionHealth: ConnectionHealth.FAILED,
188 1 : details: {
189 1 : type: detailsType,
190 1 : value: {
191 1 : ...details,
192 1 : proto: {
193 1 : ...details.proto,
194 1 : setupTimings: {
195 1 : ...details.proto.setupTimings,
196 1 : authFailedAt: dateToTimestamp(new Date()),
197 1 : },
198 1 : setupError: action.value
199 1 : },
200 1 : channel: null,
201 1 : }
202 1 : }
203 1 : };
204 1 : break;
205 1 : case GENERATING_PKCE_CHALLENGE:
206 1 : next = {
207 1 : ...state,
208 1 : connectionStatus: ConnectionStatus.PKCE_GENERATION_STARTED,
209 1 : connectionHealth: ConnectionHealth.CONNECTING,
210 1 : details: {
211 1 : type: detailsType,
212 1 : value: {
213 1 : ...details,
214 1 : proto: {
215 1 : ...details.proto,
216 1 : setupTimings: {
217 1 : ...details.proto.setupTimings,
218 1 : pkceGenStartedAt: dateToTimestamp(new Date()),
219 1 : },
220 1 : },
221 1 : channel: null,
222 1 : }
223 1 : }
224 1 : };
225 1 : break;
226 1 : case GENERATED_PKCE_CHALLENGE:
227 1 : next = {
228 1 : ...state,
229 1 : connectionStatus: ConnectionStatus.PKCE_GENERATED,
230 1 : connectionHealth: ConnectionHealth.CONNECTING,
231 1 : details: {
232 1 : type: detailsType,
233 1 : value: {
234 1 : ...details,
235 1 : proto: {
236 1 : ...details.proto,
237 1 : setupTimings: {
238 1 : ...details.proto.setupTimings,
239 1 : pkceGenFinishedAt: dateToTimestamp(new Date()),
240 1 : },
241 1 : oauthState: {
242 1 : oauthParams: details.proto.oauthState?.oauthParams ?? { instanceUrl: "", appConsumerKey: "", requestedAt: Date.now(), expiresAt: Date.now() },
243 1 : oauthPkce: action.value,
244 1 : coreAuthCode: details.proto.oauthState?.coreAuthCode ?? ({ token: "", createdAt: new Date().toISOString() } as auth.TemporaryToken),
245 1 : coreAccessToken: details.proto.oauthState?.coreAccessToken ?? ({} as connection.SalesforceCoreAccessToken),
246 1 : dataCloudAccessToken: details.proto.oauthState?.dataCloudAccessToken ?? ({} as connection.SalesforceDataCloudAccessToken),
247 1 : }
248 1 : },
249 1 : }
250 1 : }
251 1 : };
252 1 : break;
253 1 : case OAUTH_NATIVE_LINK_OPENED:
254 1 : next = {
255 1 : ...state,
256 1 : connectionStatus: ConnectionStatus.WAITING_FOR_OAUTH_CODE_VIA_LINK,
257 1 : connectionHealth: ConnectionHealth.CONNECTING,
258 1 : details: {
259 1 : type: detailsType,
260 1 : value: {
261 1 : ...details,
262 1 : proto: {
263 1 : ...details.proto,
264 1 : setupTimings: {
265 1 : ...details.proto.setupTimings,
266 1 : openedNativeAuthLinkAt: dateToTimestamp(new Date()),
267 1 : },
268 1 : },
269 1 : }
270 1 : }
271 1 : };
272 1 : break;
273 1 : case OAUTH_WEB_WINDOW_OPENED:
274 1 : next = {
275 1 : ...state,
276 1 : connectionStatus: ConnectionStatus.WAITING_FOR_OAUTH_CODE_VIA_WINDOW,
277 1 : connectionHealth: ConnectionHealth.CONNECTING,
278 1 : details: {
279 1 : type: detailsType,
280 1 : value: {
281 1 : ...details,
282 1 : proto: {
283 1 : ...details.proto,
284 1 : setupTimings: {
285 1 : ...details.proto.setupTimings,
286 1 : openedWebAuthWindowAt: dateToTimestamp(new Date()),
287 1 : },
288 1 : },
289 1 : }
290 1 : }
291 1 : };
292 1 : break;
293 1 : case OAUTH_WEB_WINDOW_CLOSED:
294 1 : next = {
295 1 : ...state,
296 1 : details: {
297 1 : type: detailsType,
298 1 : value: {
299 1 : ...details,
300 1 : proto: {
301 1 : ...details.proto,
302 1 : setupTimings: {
303 1 : ...details.proto.setupTimings,
304 1 : closedWebAuthWindowAt: dateToTimestamp(new Date()),
305 1 : },
306 1 : },
307 1 : }
308 1 : }
309 1 : };
310 1 : break;
311 1 : case RECEIVED_CORE_AUTH_CODE:
312 1 : next = {
313 1 : ...state,
314 1 : connectionStatus: ConnectionStatus.OAUTH_CODE_RECEIVED,
315 1 : connectionHealth: ConnectionHealth.CONNECTING,
316 1 : details: {
317 1 : type: detailsType,
318 1 : value: {
319 1 : ...details,
320 1 : proto: {
321 1 : ...details.proto,
322 1 : setupTimings: {
323 1 : ...details.proto.setupTimings,
324 1 : oauthCodeReceivedAt: dateToTimestamp(new Date()),
325 1 : },
326 1 : oauthState: {
327 1 : oauthParams: details.proto.oauthState?.oauthParams ?? { instanceUrl: "", appConsumerKey: "", requestedAt: Date.now(), expiresAt: Date.now() },
328 1 : oauthPkce: details.proto.oauthState?.oauthPkce ?? ({ value: "", verifier: "" } as auth.OAuthPKCEChallenge),
329 1 : coreAuthCode: action.value,
330 1 : coreAccessToken: details.proto.oauthState?.coreAccessToken ?? ({} as connection.SalesforceCoreAccessToken),
331 1 : dataCloudAccessToken: details.proto.oauthState?.dataCloudAccessToken ?? ({} as connection.SalesforceDataCloudAccessToken),
332 1 : }
333 1 : },
334 1 : }
335 1 : }
336 1 : };
337 1 : break;
338 1 : case REQUESTING_CORE_AUTH_TOKEN:
339 1 : next = {
340 1 : ...state,
341 1 : connectionStatus: ConnectionStatus.ACCESS_TOKEN_REQUESTED,
342 1 : connectionHealth: ConnectionHealth.CONNECTING,
343 1 : details: {
344 1 : type: detailsType,
345 1 : value: {
346 1 : ...details,
347 1 : proto: {
348 1 : ...details.proto,
349 1 : setupTimings: {
350 1 : ...details.proto.setupTimings,
351 1 : coreAccessTokenRequestedAt: dateToTimestamp(new Date()),
352 1 : },
353 1 : },
354 1 : }
355 1 : }
356 1 : };
357 1 : break;
358 1 : case RECEIVED_CORE_AUTH_TOKEN:
359 1 : next = {
360 1 : ...state,
361 1 : connectionStatus: ConnectionStatus.CORE_ACCESS_TOKEN_RECEIVED,
362 1 : connectionHealth: ConnectionHealth.CONNECTING,
363 1 : details: {
364 1 : type: detailsType,
365 1 : value: {
366 1 : ...details,
367 1 : proto: {
368 1 : ...details.proto,
369 1 : setupTimings: {
370 1 : ...details.proto.setupTimings,
371 1 : coreAccessTokenReceivedAt: dateToTimestamp(new Date()),
372 1 : },
373 1 : oauthState: {
374 1 : oauthParams: details.proto.oauthState?.oauthParams ?? { instanceUrl: "", appConsumerKey: "", requestedAt: Date.now(), expiresAt: Date.now() },
375 1 : oauthPkce: details.proto.oauthState?.oauthPkce ?? ({ value: "", verifier: "" } as auth.OAuthPKCEChallenge),
376 1 : coreAuthCode: details.proto.oauthState?.coreAuthCode ?? ({ token: "", createdAt: new Date().toISOString() } as auth.TemporaryToken),
377 1 : coreAccessToken: action.value,
378 1 : dataCloudAccessToken: details.proto.oauthState?.dataCloudAccessToken ?? ({} as connection.SalesforceDataCloudAccessToken),
379 1 : }
380 1 : },
381 1 : }
382 1 : }
383 1 : };
384 1 : break;
385 1 : case RECEIVED_CORE_USER_INFO: {
386 : // Persist the resolved account identity into setupParams.login so it is emitted as
387 : // the OAuth `login_hint` on subsequent connects and can be carried into shared
388 : // links/files. Resolving user info is best-effort (see salesforce_connection_setup):
389 : // if the connected app lacks the openid/profile scope we never reach this action.
390 1 : const resolvedLogin = action.value.preferredUsername ?? action.value.email ?? details.proto.setupParams?.login ?? "";
391 1 : next = {
392 1 : ...state,
393 1 : details: {
394 1 : type: detailsType,
395 1 : value: {
396 1 : ...details,
397 1 : proto: {
398 1 : ...details.proto,
399 1 : setupParams: details.proto.setupParams
400 1 : ? { ...details.proto.setupParams, login: resolvedLogin }
401 0 : : details.proto.setupParams,
402 1 : },
403 1 : }
404 1 : }
405 1 : };
406 1 : break;
407 1 : }
408 1 : case REQUESTING_DATA_CLOUD_ACCESS_TOKEN:
409 1 : next = {
410 1 : ...state,
411 1 : connectionStatus: ConnectionStatus.DATA_CLOUD_TOKEN_REQUESTED,
412 1 : connectionHealth: ConnectionHealth.CONNECTING,
413 1 : details: {
414 1 : type: detailsType,
415 1 : value: {
416 1 : ...details,
417 1 : proto: {
418 1 : ...details.proto,
419 1 : setupTimings: {
420 1 : ...details.proto.setupTimings,
421 1 : dataCloudAccessTokenRequestedAt: dateToTimestamp(new Date()),
422 1 : },
423 1 : },
424 1 : }
425 1 : }
426 1 : };
427 1 : break;
428 1 : case RECEIVED_DATA_CLOUD_ACCESS_TOKEN:
429 1 : next = {
430 1 : ...state,
431 1 : connectionStatus: ConnectionStatus.DATA_CLOUD_TOKEN_RECEIVED,
432 1 : connectionHealth: ConnectionHealth.CONNECTING,
433 1 : details: {
434 1 : type: detailsType,
435 1 : value: {
436 1 : ...details,
437 1 : proto: {
438 1 : ...details.proto,
439 1 : setupTimings: {
440 1 : ...details.proto.setupTimings,
441 1 : dataCloudAccessTokenReceivedAt: dateToTimestamp(new Date()),
442 1 : },
443 1 : oauthState: {
444 1 : oauthParams: details.proto.oauthState?.oauthParams ?? { instanceUrl: "", appConsumerKey: "", requestedAt: Date.now(), expiresAt: Date.now() },
445 1 : oauthPkce: details.proto.oauthState?.oauthPkce ?? ({ value: "", verifier: "" } as auth.OAuthPKCEChallenge),
446 1 : coreAuthCode: details.proto.oauthState?.coreAuthCode ?? ({ token: "", createdAt: new Date().toISOString() } as auth.TemporaryToken),
447 1 : coreAccessToken: details.proto.oauthState?.coreAccessToken ?? ({} as connection.SalesforceCoreAccessToken),
448 1 : dataCloudAccessToken: action.value,
449 1 : }
450 1 : },
451 1 : }
452 1 : }
453 1 : };
454 1 : break;
455 1 : case SF_CHANNEL_SETUP_STARTED:
456 1 : next = {
457 1 : ...state,
458 1 : connectionStatus: ConnectionStatus.CHANNEL_SETUP_STARTED,
459 1 : connectionHealth: ConnectionHealth.CONNECTING,
460 1 : details: {
461 1 : type: detailsType,
462 1 : value: {
463 1 : ...details,
464 1 : proto: {
465 1 : ...details.proto,
466 1 : setupTimings: {
467 1 : ...details.proto.setupTimings,
468 1 : channelSetupStartedAt: dateToTimestamp(new Date()),
469 1 : },
470 1 : },
471 1 : channel: null,
472 1 : }
473 1 : },
474 1 : };
475 1 : break;
476 1 : case SF_CHANNEL_SETUP_CANCELLED:
477 1 : next = {
478 1 : ...state,
479 1 : connectionStatus: ConnectionStatus.CHANNEL_SETUP_CANCELLED,
480 1 : connectionHealth: ConnectionHealth.CANCELLED,
481 1 : details: {
482 1 : type: detailsType,
483 1 : value: {
484 1 : ...details,
485 1 : proto: {
486 1 : ...details.proto,
487 1 : setupTimings: {
488 1 : ...details.proto.setupTimings,
489 1 : channelSetupCancelledAt: dateToTimestamp(new Date()),
490 1 : },
491 1 : channelError: action.value
492 1 : },
493 1 : channel: null
494 1 : }
495 1 : },
496 1 : };
497 1 : break;
498 1 : case SF_CHANNEL_SETUP_FAILED:
499 1 : next = {
500 1 : ...state,
501 1 : connectionStatus: ConnectionStatus.CHANNEL_SETUP_FAILED,
502 1 : connectionHealth: ConnectionHealth.FAILED,
503 1 : details: {
504 1 : type: detailsType,
505 1 : value: {
506 1 : ...details,
507 1 : proto: {
508 1 : ...details.proto,
509 1 : setupTimings: {
510 1 : ...details.proto.setupTimings,
511 1 : channelSetupFailedAt: dateToTimestamp(new Date()),
512 1 : },
513 1 : channelError: action.value
514 1 : },
515 1 : channel: null
516 1 : }
517 1 : },
518 1 : };
519 1 : break;
520 1 : case SF_CHANNEL_READY:
521 1 : next = {
522 1 : ...state,
523 1 : connectionStatus: ConnectionStatus.CHANNEL_READY,
524 1 : connectionHealth: ConnectionHealth.CONNECTING,
525 1 : details: {
526 1 : type: detailsType,
527 1 : value: {
528 1 : ...details,
529 1 : proto: {
530 1 : ...details.proto,
531 1 : setupTimings: {
532 1 : ...details.proto.setupTimings,
533 1 : channelReadyAt: dateToTimestamp(new Date()),
534 1 : },
535 1 : },
536 1 : channel: action.value
537 1 : }
538 1 : },
539 1 : };
540 : break;
541 1 : }
542 1 : return next;
543 1 : }
|