@salesforce/agentforce-conversation-client 11.67.1 → 11.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,10 +5,13 @@
5
5
  */
6
6
  // @vitest-environment jsdom
7
7
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
- import { embedAgentforceClient, AGENTFORCE_CLIENT_ELEMENT_TAG, AGENTFORCE_CLIENT_NAMESPACE, AGENTFORCE_CLIENT_COMPONENT_NAME, } from "./index";
9
- const { mockSetAttribute, mockAddEventListener } = vi.hoisted(() => ({
8
+ import { embedAgentforceClient, AGENTFORCE_CLIENT_ELEMENT_TAG, AGENTFORCE_CLIENT_NAMESPACE, AGENTFORCE_CLIENT_COMPONENT_NAME, AGENTFORCE_FRONTDOOR_URL_REQUIRED_EVENT, } from "./index";
9
+ const { mockSetAttribute, mockAddEventListener, mockSessionExpiry, mockSessionRefresh, mockFrontdoorUrlSetter, } = vi.hoisted(() => ({
10
10
  mockSetAttribute: vi.fn(),
11
11
  mockAddEventListener: vi.fn(),
12
+ mockSessionExpiry: vi.fn(),
13
+ mockSessionRefresh: vi.fn(),
14
+ mockFrontdoorUrlSetter: vi.fn(),
12
15
  }));
13
16
  vi.mock("@salesforce/lightning-out", () => ({
14
17
  LightningOutApplication: class {
@@ -16,22 +19,62 @@ vi.mock("@salesforce/lightning-out", () => ({
16
19
  const element = document.createElement("lightning-out-application");
17
20
  element.setAttribute = mockSetAttribute;
18
21
  element.addEventListener = mockAddEventListener;
22
+ Object.assign(element, {
23
+ sessionExpiry: mockSessionExpiry,
24
+ sessionRefresh: mockSessionRefresh,
25
+ });
26
+ // Real accessor rather than an expando, so tests observe the write
27
+ // the SDK makes rather than a value they set themselves.
28
+ let frontdoorUrl;
29
+ Object.defineProperty(element, "frontdoorUrl", {
30
+ configurable: true,
31
+ get: () => frontdoorUrl,
32
+ set: (value) => {
33
+ frontdoorUrl = value;
34
+ mockFrontdoorUrlSetter(value);
35
+ },
36
+ });
19
37
  return element;
20
38
  }
21
39
  },
22
40
  }));
41
+ const FRONTDOOR_URL = "https://test.salesforce.com/secur/frontdoor.jsp";
42
+ const FRESH_FRONTDOOR_URL = "https://test.salesforce.com/secur/frontdoor.jsp?otp=fresh";
23
43
  describe("Agentforce Conversation Client SDK", () => {
24
44
  let container;
45
+ let onFrontdoorUrlRequired;
25
46
  beforeEach(() => {
26
47
  container = document.createElement("div");
27
48
  container.id = "test-container";
28
49
  document.body.appendChild(container);
50
+ onFrontdoorUrlRequired = vi.fn();
29
51
  });
30
52
  afterEach(() => {
31
53
  document.body.innerHTML = "";
32
54
  mockSetAttribute.mockClear();
33
55
  mockAddEventListener.mockClear();
56
+ mockSessionExpiry.mockReset();
57
+ mockSessionRefresh.mockReset();
58
+ mockFrontdoorUrlSetter.mockReset();
59
+ vi.useRealTimers();
34
60
  });
61
+ /** Invokes every registered handler, so tests don't depend on listener order. */
62
+ function fireLoEvent(name) {
63
+ mockAddEventListener.mock.calls
64
+ .filter(([eventName]) => eventName === name)
65
+ .forEach(([, handler]) => handler(new Event(name)));
66
+ }
67
+ function embedWithKeepalive(overrides = {}) {
68
+ return embedAgentforceClient({
69
+ container: "#test-container",
70
+ frontdoorUrl: FRONTDOOR_URL,
71
+ sessionManagement: {
72
+ onFrontdoorUrlRequired,
73
+ checkIntervalMs: 1_000,
74
+ ...overrides,
75
+ },
76
+ });
77
+ }
35
78
  it("exports element/tag constants", () => {
36
79
  expect(AGENTFORCE_CLIENT_ELEMENT_TAG).toBe("runtime_copilot-acc-sdk-wrapper");
37
80
  expect(AGENTFORCE_CLIENT_NAMESPACE).toBe("runtime_copilot");
@@ -88,19 +131,19 @@ describe("Agentforce Conversation Client SDK", () => {
88
131
  embedAgentforceClient({
89
132
  container: "#test-container",
90
133
  salesforceOrigin: "https://test.salesforce.com",
91
- frontdoorUrl: "https://test.salesforce.com/secur/frontdoor.jsp",
134
+ frontdoorUrl: FRONTDOOR_URL,
92
135
  });
93
136
  expect(mockSetAttribute).toHaveBeenCalledWith("org-url", "https://test.salesforce.com");
94
137
  expect(mockSetAttribute).toHaveBeenCalledWith("components", `${AGENTFORCE_CLIENT_NAMESPACE}/${AGENTFORCE_CLIENT_COMPONENT_NAME} as ${AGENTFORCE_CLIENT_ELEMENT_TAG}`);
95
- expect(mockSetAttribute).toHaveBeenCalledWith("frontdoor-url", "https://test.salesforce.com/secur/frontdoor.jsp");
138
+ expect(mockSetAttribute).toHaveBeenCalledWith("frontdoor-url", FRONTDOOR_URL);
96
139
  });
97
140
  it("supports frontdoor URL without salesforceOrigin", () => {
98
141
  embedAgentforceClient({
99
142
  container: "#test-container",
100
- frontdoorUrl: "https://test.salesforce.com/secur/frontdoor.jsp",
143
+ frontdoorUrl: FRONTDOOR_URL,
101
144
  });
102
145
  expect(mockSetAttribute).not.toHaveBeenCalledWith("org-url", expect.anything());
103
- expect(mockSetAttribute).toHaveBeenCalledWith("frontdoor-url", "https://test.salesforce.com/secur/frontdoor.jsp");
146
+ expect(mockSetAttribute).toHaveBeenCalledWith("frontdoor-url", FRONTDOOR_URL);
104
147
  });
105
148
  it("sets site-prefix when provided", () => {
106
149
  embedAgentforceClient({
@@ -218,4 +261,255 @@ describe("Agentforce Conversation Client SDK", () => {
218
261
  salesforceOrigin: "https://test.salesforce.com",
219
262
  })).not.toThrow();
220
263
  });
264
+ // ── Session: setFrontdoorUrl ─────────────────────────────────────────────
265
+ it("hands a host-supplied URL to Lightning Out via setFrontdoorUrl", () => {
266
+ const { setFrontdoorUrl } = embedAgentforceClient({
267
+ container: "#test-container",
268
+ frontdoorUrl: FRONTDOOR_URL,
269
+ });
270
+ setFrontdoorUrl(FRESH_FRONTDOOR_URL);
271
+ expect(mockFrontdoorUrlSetter).toHaveBeenCalledWith(FRESH_FRONTDOOR_URL);
272
+ });
273
+ it("exposes typed LO session primitives on loApp", () => {
274
+ const { loApp } = embedAgentforceClient({
275
+ container: "#test-container",
276
+ frontdoorUrl: FRONTDOOR_URL,
277
+ });
278
+ expect(typeof loApp.sessionExpiry).toBe("function");
279
+ expect(typeof loApp.sessionRefresh).toBe("function");
280
+ });
281
+ it("ignores a frontdoor URL pushed after logout", () => {
282
+ const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
283
+ const { setFrontdoorUrl } = embedAgentforceClient({
284
+ container: "#test-container",
285
+ frontdoorUrl: FRONTDOOR_URL,
286
+ });
287
+ fireLoEvent("lo.application.logout");
288
+ setFrontdoorUrl(FRESH_FRONTDOOR_URL);
289
+ expect(mockFrontdoorUrlSetter).not.toHaveBeenCalled();
290
+ consoleWarn.mockRestore();
291
+ });
292
+ it("rejects an empty frontdoor URL", () => {
293
+ const { setFrontdoorUrl } = embedAgentforceClient({
294
+ container: "#test-container",
295
+ frontdoorUrl: FRONTDOOR_URL,
296
+ });
297
+ expect(() => setFrontdoorUrl("")).toThrow("setFrontdoorUrl requires a non-empty URL");
298
+ expect(mockFrontdoorUrlSetter).not.toHaveBeenCalled();
299
+ });
300
+ it("rejects setFrontdoorUrl in org-url mode", () => {
301
+ const { setFrontdoorUrl } = embedAgentforceClient({
302
+ container: "#test-container",
303
+ salesforceOrigin: "https://test.salesforce.com",
304
+ });
305
+ expect(() => setFrontdoorUrl(FRESH_FRONTDOOR_URL)).toThrow("setFrontdoorUrl is not supported in org-url mode");
306
+ });
307
+ // ── Session keepalive (option B) ─────────────────────────────────────────
308
+ it("requires frontdoor-URL-only mode for sessionManagement overrides", () => {
309
+ expect(() => embedAgentforceClient({
310
+ container: "#test-container",
311
+ salesforceOrigin: "https://test.salesforce.com",
312
+ frontdoorUrl: FRONTDOOR_URL,
313
+ sessionManagement: {},
314
+ })).toThrow("Agentforce Conversation Client: sessionManagement requires frontdoorUrl-only mode");
315
+ });
316
+ it("enables keepalive by default in frontdoor-URL-only mode", () => {
317
+ expect(() => embedAgentforceClient({
318
+ container: "#test-container",
319
+ frontdoorUrl: FRONTDOOR_URL,
320
+ })).not.toThrow();
321
+ });
322
+ it("allows keepalive without an onFrontdoorUrlRequired callback", () => {
323
+ expect(() => embedAgentforceClient({
324
+ container: "#test-container",
325
+ frontdoorUrl: FRONTDOOR_URL,
326
+ sessionManagement: { checkIntervalMs: 1_000 },
327
+ })).not.toThrow();
328
+ });
329
+ it("validates keepalive thresholds before mutating the container", () => {
330
+ expect(() => embedWithKeepalive({ checkIntervalMs: 0 })).toThrow("sessionManagement.checkIntervalMs must be greater than 0");
331
+ expect(container.classList.contains("acc-container")).toBe(false);
332
+ });
333
+ it("waits for Lightning Out readiness before checking the session", async () => {
334
+ vi.useFakeTimers();
335
+ mockSessionExpiry.mockResolvedValue(1_000);
336
+ embedWithKeepalive();
337
+ await vi.advanceTimersByTimeAsync(2_000);
338
+ expect(mockSessionExpiry).not.toHaveBeenCalled();
339
+ });
340
+ it("leaves healthy sessions alone", async () => {
341
+ vi.useFakeTimers();
342
+ mockSessionExpiry.mockResolvedValue(601);
343
+ embedWithKeepalive();
344
+ fireLoEvent("lo.application.ready");
345
+ await vi.advanceTimersByTimeAsync(1_000);
346
+ expect(mockSessionExpiry).toHaveBeenCalledOnce();
347
+ expect(mockSessionRefresh).not.toHaveBeenCalled();
348
+ expect(onFrontdoorUrlRequired).not.toHaveBeenCalled();
349
+ });
350
+ it("calls sessionRefresh inside the keepalive window", async () => {
351
+ vi.useFakeTimers();
352
+ mockSessionExpiry.mockResolvedValue(600);
353
+ mockSessionRefresh.mockResolvedValue(undefined);
354
+ embedWithKeepalive();
355
+ fireLoEvent("lo.application.ready");
356
+ await vi.advanceTimersByTimeAsync(1_000);
357
+ expect(mockSessionRefresh).toHaveBeenCalledOnce();
358
+ expect(onFrontdoorUrlRequired).not.toHaveBeenCalled();
359
+ });
360
+ it("calls sessionRefresh by default without sessionManagement", async () => {
361
+ vi.useFakeTimers();
362
+ mockSessionExpiry.mockResolvedValue(600);
363
+ mockSessionRefresh.mockResolvedValue(undefined);
364
+ embedAgentforceClient({
365
+ container: "#test-container",
366
+ frontdoorUrl: FRONTDOOR_URL,
367
+ });
368
+ fireLoEvent("lo.application.ready");
369
+ await vi.advanceTimersByTimeAsync(60_000);
370
+ expect(mockSessionRefresh).toHaveBeenCalledOnce();
371
+ expect(onFrontdoorUrlRequired).not.toHaveBeenCalled();
372
+ });
373
+ it("asks the host when the session has expired", async () => {
374
+ vi.useFakeTimers();
375
+ mockSessionExpiry.mockResolvedValue(0);
376
+ embedWithKeepalive();
377
+ fireLoEvent("lo.application.ready");
378
+ await vi.advanceTimersByTimeAsync(1_000);
379
+ expect(mockSessionRefresh).not.toHaveBeenCalled();
380
+ expect(onFrontdoorUrlRequired).toHaveBeenCalledWith({ reason: "expired", ttlSeconds: 0 });
381
+ });
382
+ it("dispatches agentforce:frontdoorurlrequired on the container", async () => {
383
+ vi.useFakeTimers();
384
+ mockSessionExpiry.mockResolvedValue(0);
385
+ const listener = vi.fn();
386
+ container.addEventListener(AGENTFORCE_FRONTDOOR_URL_REQUIRED_EVENT, listener);
387
+ embedAgentforceClient({
388
+ container: "#test-container",
389
+ frontdoorUrl: FRONTDOOR_URL,
390
+ sessionManagement: { checkIntervalMs: 1_000 },
391
+ });
392
+ fireLoEvent("lo.application.ready");
393
+ await vi.advanceTimersByTimeAsync(1_000);
394
+ expect(listener).toHaveBeenCalledOnce();
395
+ const firstCall = listener.mock.calls[0];
396
+ expect(firstCall).toBeDefined();
397
+ const event = firstCall[0];
398
+ expect(event.type).toBe(AGENTFORCE_FRONTDOOR_URL_REQUIRED_EVENT);
399
+ expect(event.bubbles).toBe(true);
400
+ expect(event.detail).toEqual({ reason: "expired", ttlSeconds: 0 });
401
+ });
402
+ it("asks the host when sessionRefresh fails", async () => {
403
+ vi.useFakeTimers();
404
+ const refreshError = new Error("refresh failed");
405
+ mockSessionExpiry.mockResolvedValue(600);
406
+ mockSessionRefresh.mockRejectedValue(refreshError);
407
+ embedWithKeepalive();
408
+ fireLoEvent("lo.application.ready");
409
+ await vi.advanceTimersByTimeAsync(1_000);
410
+ expect(onFrontdoorUrlRequired).toHaveBeenCalledWith({
411
+ reason: "refresh-failed",
412
+ ttlSeconds: 600,
413
+ error: refreshError,
414
+ });
415
+ });
416
+ it("asks the host once per outage rather than once per check", async () => {
417
+ vi.useFakeTimers();
418
+ mockSessionExpiry.mockResolvedValue(0);
419
+ embedWithKeepalive();
420
+ fireLoEvent("lo.application.ready");
421
+ await vi.advanceTimersByTimeAsync(20_000);
422
+ expect(mockSessionExpiry.mock.calls.length).toBeGreaterThan(1);
423
+ expect(onFrontdoorUrlRequired).toHaveBeenCalledOnce();
424
+ });
425
+ it("asks again after a pushed URL expires in turn", async () => {
426
+ vi.useFakeTimers();
427
+ mockSessionExpiry.mockResolvedValue(0);
428
+ const { setFrontdoorUrl } = embedWithKeepalive();
429
+ fireLoEvent("lo.application.ready");
430
+ await vi.advanceTimersByTimeAsync(1_000);
431
+ expect(onFrontdoorUrlRequired).toHaveBeenCalledOnce();
432
+ setFrontdoorUrl(FRESH_FRONTDOOR_URL);
433
+ await vi.advanceTimersByTimeAsync(1_000);
434
+ expect(onFrontdoorUrlRequired).toHaveBeenCalledTimes(2);
435
+ });
436
+ it("stops keepalive after Lightning Out logout", async () => {
437
+ vi.useFakeTimers();
438
+ mockSessionExpiry.mockResolvedValue(1_000);
439
+ embedWithKeepalive();
440
+ fireLoEvent("lo.application.ready");
441
+ fireLoEvent("lo.application.logout");
442
+ await vi.advanceTimersByTimeAsync(2_000);
443
+ expect(mockSessionExpiry).not.toHaveBeenCalled();
444
+ });
445
+ it("does not poll in org-url mode", async () => {
446
+ vi.useFakeTimers();
447
+ mockSessionExpiry.mockResolvedValue(0);
448
+ embedAgentforceClient({
449
+ container: "#test-container",
450
+ salesforceOrigin: "https://test.salesforce.com",
451
+ });
452
+ fireLoEvent("lo.application.ready");
453
+ await vi.advanceTimersByTimeAsync(120_000);
454
+ expect(mockSessionExpiry).not.toHaveBeenCalled();
455
+ });
456
+ it("disables keepalive when LO session APIs are missing at ready", async () => {
457
+ vi.useFakeTimers();
458
+ const onError = vi.fn();
459
+ const { loApp } = embedAgentforceClient({
460
+ container: "#test-container",
461
+ frontdoorUrl: FRONTDOOR_URL,
462
+ sessionManagement: { checkIntervalMs: 1_000 },
463
+ onError,
464
+ });
465
+ Reflect.deleteProperty(loApp, "sessionExpiry");
466
+ fireLoEvent("lo.application.ready");
467
+ await vi.advanceTimersByTimeAsync(2_000);
468
+ expect(mockSessionExpiry).not.toHaveBeenCalled();
469
+ expect(onError).toHaveBeenCalledWith(expect.objectContaining({ type: "lo.session.error" }));
470
+ });
471
+ it("forwards async onFrontdoorUrlRequired rejections to onError", async () => {
472
+ vi.useFakeTimers();
473
+ const onError = vi.fn();
474
+ const mintError = new Error("mint failed");
475
+ onFrontdoorUrlRequired.mockImplementation(() => Promise.reject(mintError));
476
+ mockSessionExpiry.mockResolvedValue(0);
477
+ embedAgentforceClient({
478
+ container: "#test-container",
479
+ frontdoorUrl: FRONTDOOR_URL,
480
+ onError,
481
+ sessionManagement: { onFrontdoorUrlRequired, checkIntervalMs: 1_000 },
482
+ });
483
+ fireLoEvent("lo.application.ready");
484
+ await vi.advanceTimersByTimeAsync(1_000);
485
+ await Promise.resolve();
486
+ expect(onError).toHaveBeenCalledWith({ type: "lo.session.error", detail: mintError });
487
+ });
488
+ it("stops keepalive when the LO app is unmounted", async () => {
489
+ vi.useFakeTimers();
490
+ mockSessionExpiry.mockResolvedValue(601);
491
+ const { loApp } = embedWithKeepalive();
492
+ fireLoEvent("lo.application.ready");
493
+ await vi.advanceTimersByTimeAsync(1_000);
494
+ expect(mockSessionExpiry).toHaveBeenCalledOnce();
495
+ loApp.remove();
496
+ await vi.advanceTimersByTimeAsync(5_000);
497
+ expect(mockSessionExpiry).toHaveBeenCalledOnce();
498
+ });
499
+ it("keeps waiting when the host embeds into a detached container, then polls after attach", async () => {
500
+ vi.useFakeTimers();
501
+ mockSessionExpiry.mockResolvedValue(601);
502
+ const detached = document.createElement("div");
503
+ embedAgentforceClient({
504
+ container: detached,
505
+ frontdoorUrl: FRONTDOOR_URL,
506
+ sessionManagement: { onFrontdoorUrlRequired, checkIntervalMs: 1_000 },
507
+ });
508
+ fireLoEvent("lo.application.ready");
509
+ await vi.advanceTimersByTimeAsync(3_000);
510
+ expect(mockSessionExpiry).not.toHaveBeenCalled();
511
+ document.body.appendChild(detached);
512
+ await vi.advanceTimersByTimeAsync(1_000);
513
+ expect(mockSessionExpiry).toHaveBeenCalledOnce();
514
+ });
221
515
  });
@@ -0,0 +1,17 @@
1
+ import type { AgentforceErrorHandler, AgentforceLightningOutApplication, AgentforceSessionManagementConfig, AgentforceSessionRecoveryRequest } from "./types.js";
2
+ export interface ResolvedSessionManagementConfig {
3
+ onFrontdoorUrlRequired?: (request: AgentforceSessionRecoveryRequest) => void | Promise<void>;
4
+ checkIntervalMs: number;
5
+ skipThresholdSeconds: number;
6
+ refreshThresholdSeconds: number;
7
+ }
8
+ export declare function resolveSessionManagementConfig(config: AgentforceSessionManagementConfig): ResolvedSessionManagementConfig;
9
+ /** Runtime guard — LO package types do not guarantee session primitives exist. */
10
+ export declare function isSessionCapable(loApp: HTMLElement): loApp is AgentforceLightningOutApplication;
11
+ /**
12
+ * ACC polls and calls `sessionRefresh()` while the session is
13
+ * alive. Credential minting stays with the host, when ACC cannot extend the
14
+ * session it notifies once and waits for `setFrontdoorUrl`.
15
+ */
16
+ export declare function createSessionKeepalive(loApp: HTMLElement, container: HTMLElement, isOrgUrlMode: boolean, resolved: ResolvedSessionManagementConfig | undefined, onError?: AgentforceErrorHandler): (frontdoorUrl: string) => void;
17
+ //# sourceMappingURL=session-keepalive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-keepalive.d.ts","sourceRoot":"","sources":["../src/session-keepalive.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACX,sBAAsB,EACtB,iCAAiC,EACjC,iCAAiC,EACjC,gCAAgC,EAChC,MAAM,YAAY,CAAC;AAQpB,MAAM,WAAW,+BAA+B;IAC/C,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE,gCAAgC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7F,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uBAAuB,EAAE,MAAM,CAAC;CAChC;AAED,wBAAgB,8BAA8B,CAC7C,MAAM,EAAE,iCAAiC,GACvC,+BAA+B,CAqCjC;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,KAAK,IAAI,iCAAiC,CAO/F;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACrC,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,WAAW,EACtB,YAAY,EAAE,OAAO,EACrB,QAAQ,EAAE,+BAA+B,GAAG,SAAS,EACrD,OAAO,CAAC,EAAE,sBAAsB,GAC9B,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAkJhC"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ import { AGENTFORCE_FRONTDOOR_URL_REQUIRED_EVENT } from "./constants.js";
7
+ const SESSION_KEEPALIVE_DEFAULTS = Object.freeze({
8
+ CHECK_INTERVAL_MS: 60_000,
9
+ SKIP_THRESHOLD_SECONDS: 600,
10
+ REFRESH_THRESHOLD_SECONDS: 300,
11
+ });
12
+ export function resolveSessionManagementConfig(config) {
13
+ const resolved = {
14
+ onFrontdoorUrlRequired: config.onFrontdoorUrlRequired,
15
+ checkIntervalMs: config.checkIntervalMs ?? SESSION_KEEPALIVE_DEFAULTS.CHECK_INTERVAL_MS,
16
+ skipThresholdSeconds: config.skipThresholdSeconds ?? SESSION_KEEPALIVE_DEFAULTS.SKIP_THRESHOLD_SECONDS,
17
+ refreshThresholdSeconds: config.refreshThresholdSeconds ?? SESSION_KEEPALIVE_DEFAULTS.REFRESH_THRESHOLD_SECONDS,
18
+ };
19
+ if (resolved.onFrontdoorUrlRequired !== undefined &&
20
+ typeof resolved.onFrontdoorUrlRequired !== "function") {
21
+ throw new Error("Agentforce Conversation Client: sessionManagement.onFrontdoorUrlRequired must be a function");
22
+ }
23
+ if (!Number.isFinite(resolved.checkIntervalMs) || resolved.checkIntervalMs <= 0) {
24
+ throw new Error("Agentforce Conversation Client: sessionManagement.checkIntervalMs must be greater than 0");
25
+ }
26
+ if (!Number.isFinite(resolved.skipThresholdSeconds) || resolved.skipThresholdSeconds <= 0) {
27
+ throw new Error("Agentforce Conversation Client: sessionManagement.skipThresholdSeconds must be greater than 0");
28
+ }
29
+ if (!Number.isFinite(resolved.refreshThresholdSeconds) ||
30
+ resolved.refreshThresholdSeconds < 0 ||
31
+ resolved.refreshThresholdSeconds >= resolved.skipThresholdSeconds) {
32
+ throw new Error("Agentforce Conversation Client: sessionManagement.refreshThresholdSeconds must be between 0 and skipThresholdSeconds");
33
+ }
34
+ return resolved;
35
+ }
36
+ /** Runtime guard — LO package types do not guarantee session primitives exist. */
37
+ export function isSessionCapable(loApp) {
38
+ const candidate = loApp;
39
+ return (typeof candidate.sessionExpiry === "function" &&
40
+ typeof candidate.sessionRefresh === "function" &&
41
+ "frontdoorUrl" in loApp);
42
+ }
43
+ /**
44
+ * ACC polls and calls `sessionRefresh()` while the session is
45
+ * alive. Credential minting stays with the host, when ACC cannot extend the
46
+ * session it notifies once and waits for `setFrontdoorUrl`.
47
+ */
48
+ export function createSessionKeepalive(loApp, container, isOrgUrlMode, resolved, onError) {
49
+ let timer;
50
+ let checkInFlight = false;
51
+ let stopped = false;
52
+ // Edge-triggered: ask the host once per outage, not once per check.
53
+ let recoveryPending = false;
54
+ /** Set after `lo.application.ready` confirms session primitives exist. */
55
+ let sessionApp;
56
+ const stop = () => {
57
+ stopped = true;
58
+ if (timer !== undefined) {
59
+ window.clearTimeout(timer);
60
+ timer = undefined;
61
+ }
62
+ };
63
+ loApp.addEventListener("lo.application.logout", stop);
64
+ const reportSessionError = (error, context) => {
65
+ console.error(`Agentforce Conversation Client: ${context}`, error);
66
+ onError?.({ type: "lo.session.error", detail: error });
67
+ };
68
+ const requestRecovery = (request) => {
69
+ if (recoveryPending || !resolved)
70
+ return;
71
+ recoveryPending = true;
72
+ container.dispatchEvent(new CustomEvent(AGENTFORCE_FRONTDOOR_URL_REQUIRED_EVENT, {
73
+ detail: request,
74
+ bubbles: true,
75
+ }));
76
+ if (!resolved.onFrontdoorUrlRequired)
77
+ return;
78
+ try {
79
+ const result = resolved.onFrontdoorUrlRequired(request);
80
+ void Promise.resolve(result).catch((error) => {
81
+ reportSessionError(error, "onFrontdoorUrlRequired rejected");
82
+ });
83
+ }
84
+ catch (error) {
85
+ reportSessionError(error, "onFrontdoorUrlRequired threw");
86
+ }
87
+ };
88
+ const setFrontdoorUrl = (frontdoorUrl) => {
89
+ if (isOrgUrlMode) {
90
+ throw new Error("Agentforce Conversation Client: setFrontdoorUrl is not supported in org-url mode");
91
+ }
92
+ if (typeof frontdoorUrl !== "string" || !frontdoorUrl) {
93
+ throw new Error("Agentforce Conversation Client: setFrontdoorUrl requires a non-empty URL");
94
+ }
95
+ if (stopped) {
96
+ console.warn("Agentforce Conversation Client: ignoring frontdoor URL after logout");
97
+ return;
98
+ }
99
+ if (!isSessionCapable(loApp)) {
100
+ throw new Error("Agentforce Conversation Client: Lightning Out session APIs are unavailable (frontdoorUrl)");
101
+ }
102
+ recoveryPending = false;
103
+ loApp.frontdoorUrl = frontdoorUrl;
104
+ };
105
+ if (resolved)
106
+ startKeepalive(resolved);
107
+ return setFrontdoorUrl;
108
+ function startKeepalive(config) {
109
+ /** True once LO + container have been in the document; disconnect after that is teardown. */
110
+ let wasMounted = false;
111
+ const isMounted = () => loApp.isConnected && container.isConnected;
112
+ const checkSession = async () => {
113
+ timer = undefined;
114
+ if (stopped)
115
+ return;
116
+ if (!isMounted()) {
117
+ if (wasMounted) {
118
+ stop();
119
+ return;
120
+ }
121
+ // Detached embed (modal / template): keep waiting until the host attaches.
122
+ schedule();
123
+ return;
124
+ }
125
+ wasMounted = true;
126
+ if (!sessionApp) {
127
+ stop();
128
+ return;
129
+ }
130
+ checkInFlight = true;
131
+ try {
132
+ let ttlSeconds;
133
+ try {
134
+ ttlSeconds = await sessionApp.sessionExpiry();
135
+ }
136
+ catch (error) {
137
+ requestRecovery({ reason: "expiry-unavailable", error });
138
+ return;
139
+ }
140
+ if (ttlSeconds > config.skipThresholdSeconds) {
141
+ recoveryPending = false;
142
+ return;
143
+ }
144
+ if (ttlSeconds > config.refreshThresholdSeconds) {
145
+ try {
146
+ await sessionApp.sessionRefresh();
147
+ recoveryPending = false;
148
+ return;
149
+ }
150
+ catch (error) {
151
+ requestRecovery({ reason: "refresh-failed", ttlSeconds, error });
152
+ return;
153
+ }
154
+ }
155
+ requestRecovery({ reason: "expired", ttlSeconds });
156
+ }
157
+ finally {
158
+ checkInFlight = false;
159
+ schedule();
160
+ }
161
+ };
162
+ function schedule() {
163
+ if (stopped || timer !== undefined || checkInFlight)
164
+ return;
165
+ if (!isMounted() && wasMounted) {
166
+ stop();
167
+ return;
168
+ }
169
+ timer = window.setTimeout(() => void checkSession(), config.checkIntervalMs);
170
+ }
171
+ loApp.addEventListener("lo.application.ready", () => {
172
+ if (stopped)
173
+ return;
174
+ if (!isSessionCapable(loApp)) {
175
+ reportSessionError(new Error("Lightning Out session APIs unavailable (sessionExpiry / sessionRefresh / frontdoorUrl)"), "session keepalive disabled");
176
+ stop();
177
+ return;
178
+ }
179
+ sessionApp = loApp;
180
+ schedule();
181
+ });
182
+ }
183
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ export declare function injectStyles(additionalCss?: string): void;
7
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgGH,wBAAgB,YAAY,CAAC,aAAa,SAAK,GAAG,IAAI,CAMrD"}
package/dist/styles.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ const STYLE_ID = "agentforce-client-injected-styles";
7
+ const AGENTFORCE_CLIENT_CSS = `
8
+ :root {
9
+ --minimized-iframe-width: 180px !important;
10
+ --minimized-iframe-height: 70px !important;
11
+ }
12
+
13
+ .acc-container:has(> .acc-frame.inline) {
14
+ height: 100%;
15
+ width: 100%;
16
+ }
17
+
18
+ .sds-overrides {
19
+ width: auto;
20
+ }
21
+
22
+ .acc-container > .acc-frame {
23
+ display: block;
24
+ position: fixed;
25
+ background: transparent;
26
+ border: none;
27
+ outline: none;
28
+ border-radius: 20px;
29
+ bottom: 24px;
30
+ right: 24px;
31
+ height: 0 !important;
32
+ width: 0 !important;
33
+ max-width: calc(100vw - 4rem);
34
+ max-height: calc(100dvh - 4rem);
35
+ z-index: 999;
36
+ }
37
+
38
+ .acc-container > .acc-frame.inline,
39
+ .acc-container > .acc-frame.inline.maximize {
40
+ height: 100% !important;
41
+ width: 100% !important;
42
+ position: static;
43
+ bottom: 0;
44
+ right: 0;
45
+ max-height: 100%;
46
+ max-width: 100%;
47
+ box-shadow: none;
48
+ border-radius: 0;
49
+ }
50
+
51
+ .acc-container > .acc-frame.inline:not(.maximize) {
52
+ width: var(--acc-width, auto) !important;
53
+ height: var(--acc-height, auto) !important;
54
+ }
55
+
56
+ .acc-container > .acc-frame.initial {
57
+ height: var(--minimized-iframe-height, 56px) !important;
58
+ width: var(--minimized-iframe-width, 145px) !important;
59
+ transition: all 0s ease;
60
+ }
61
+
62
+ .acc-container > .acc-frame.minimize {
63
+ height: var(--minimized-iframe-height, 56px) !important;
64
+ width: var(--minimized-iframe-width, 145px) !important;
65
+ transition: width 0.25s ease-in-out, height 0.25s ease-in-out;
66
+ }
67
+
68
+ .acc-container > .acc-frame.maximize {
69
+ width: 450px !important;
70
+ height: 700px !important;
71
+ background: #ffffff;
72
+ box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 12px -2px rgba(0, 0, 0, 0.05),
73
+ 0 0 2px 0 rgba(0, 0, 0, 0.05), 0 20px 45px -5px rgba(0, 0, 0, 0.08);
74
+ transition: all 0.3s ease;
75
+ }
76
+
77
+ .acc-container > lightning-out-application {
78
+ background: transparent;
79
+ }
80
+
81
+ @media (max-width: 639px) {
82
+ .acc-container > .acc-frame.maximize {
83
+ bottom: 0;
84
+ right: 0;
85
+ width: 100% !important;
86
+ height: 100vh !important;
87
+ height: 100dvh !important;
88
+ border-radius: 0;
89
+ max-width: 100%;
90
+ max-height: 100vh;
91
+ overflow-y: auto;
92
+ overflow-x: hidden;
93
+ -webkit-overflow-scrolling: touch;
94
+ transition: height 0.2s ease-in-out;
95
+ }
96
+ }
97
+ `;
98
+ export function injectStyles(additionalCss = "") {
99
+ if (document.getElementById(STYLE_ID))
100
+ return;
101
+ const style = document.createElement("style");
102
+ style.id = STYLE_ID;
103
+ style.textContent = AGENTFORCE_CLIENT_CSS + (additionalCss ? "\n" + additionalCss : "");
104
+ document.head.appendChild(style);
105
+ }