@hubspot/app-connect-sdk 1.0.0-alpha.21 → 1.0.0-alpha.23

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.
Files changed (28) hide show
  1. package/.turbo/turbo-format$colon$check.log +1 -1
  2. package/.turbo/turbo-test.log +64 -50
  3. package/.turbo/turbo-tsdown.log +7 -7
  4. package/build/tsconfig.browser.tsbuildinfo +1 -1
  5. package/build/tsconfig.server.tsbuildinfo +1 -1
  6. package/dist/browser/{create-DxEyGG-k.js → create-ULhURoJ_.js} +68 -32
  7. package/dist/browser/create-ULhURoJ_.js.map +1 -0
  8. package/dist/browser/index.d.ts +1 -1
  9. package/dist/browser/index.js +1 -1
  10. package/dist/browser/react/lovable.js +1 -1
  11. package/dist/server/hono/hubspot-connect-routes/whoami.js +5 -2
  12. package/dist/server/hono/hubspot-connect-routes/whoami.js.map +1 -1
  13. package/package.json +3 -3
  14. package/src/browser/app-connect-controller/connect-start.test.ts +4 -3
  15. package/src/browser/app-connect-controller/constants.ts +3 -4
  16. package/src/browser/app-connect-controller/create.ts +49 -6
  17. package/src/browser/app-connect-controller/default-local-storage.ts +31 -0
  18. package/src/browser/app-connect-controller/disconnect.ts +3 -3
  19. package/src/browser/app-connect-controller/init.test.ts +9 -10
  20. package/src/browser/app-connect-controller/oauth-complete.test.ts +8 -7
  21. package/src/browser/app-connect-controller/oauth-complete.ts +3 -3
  22. package/src/browser/app-connect-controller/oauth-popup.test.ts +4 -3
  23. package/src/browser/app-connect-controller/types.ts +19 -5
  24. package/src/browser/app-connect-controller/utils/session-utils.test.ts +30 -32
  25. package/src/browser/app-connect-controller/utils/session-utils.ts +13 -13
  26. package/src/server/hono/hubspot-connect-routes/whoami.ts +49 -2
  27. package/dist/browser/create-DxEyGG-k.js.map +0 -1
  28. package/src/browser/app-connect-controller/default-session-storage.ts +0 -21
@@ -56,12 +56,11 @@ const OAUTH_POPUP_CALLBACK_MESSAGE_TYPE = "hubspot-app-connect:oauth-callback";
56
56
  //#region src/browser/app-connect-controller/constants.ts
57
57
  /**
58
58
  * Key under which the controller persists the entire session blob
59
- * (expiresAt + whoami) as a single JSON string in `sessionStorage`.
59
+ * (expiresAt + whoami) as a single JSON string in `localStorage`.
60
60
  * Using one key makes logout trivially correct — one `removeItem` call
61
- * clears all session state. Survives full-page navigations within the
62
- * same tab.
61
+ * clears all session state.
63
62
  */
64
- const SESSION_STORAGE_KEY = "hubspot_connect_session";
63
+ const LOCAL_STORAGE_KEY = "hubspot_connect_session";
65
64
  /**
66
65
  * Number of milliseconds before `expiresAt` that the refresh
67
66
  * scheduler attempts to mint a new access token. 60s is comfortably
@@ -71,39 +70,39 @@ const REFRESH_BUFFER_MS = 6e4;
71
70
  //#endregion
72
71
  //#region src/browser/app-connect-controller/utils/session-utils.ts
73
72
  function readStoredSession(context) {
74
- const raw = context.sessionStorage.getItem(SESSION_STORAGE_KEY);
73
+ const raw = context.localStorage.getItem(LOCAL_STORAGE_KEY);
75
74
  if (!raw) return null;
76
75
  let parsed;
77
76
  try {
78
77
  parsed = JSON.parse(raw);
79
78
  } catch {
80
- context.sessionStorage.removeItem(SESSION_STORAGE_KEY);
79
+ context.localStorage.removeItem(LOCAL_STORAGE_KEY);
81
80
  return null;
82
81
  }
83
82
  if (typeof parsed.expiresAt !== "number" || isNaN(parsed.expiresAt)) {
84
- context.sessionStorage.removeItem(SESSION_STORAGE_KEY);
83
+ context.localStorage.removeItem(LOCAL_STORAGE_KEY);
85
84
  return null;
86
85
  }
87
86
  if (Date.now() > parsed.expiresAt) {
88
- context.sessionStorage.removeItem(SESSION_STORAGE_KEY);
87
+ context.localStorage.removeItem(LOCAL_STORAGE_KEY);
89
88
  return null;
90
89
  }
91
90
  return parsed;
92
91
  }
93
92
  function writeStoredSession(context, session) {
94
- context.sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
93
+ context.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(session));
95
94
  }
96
95
  /**
97
- * Reads the persisted session blob from `sessionStorage`. Returns
96
+ * Reads the persisted session blob from `localStorage`. Returns
98
97
  * `null` (and auto-removes the entry) if the blob is missing,
99
98
  * malformed, or already expired.
100
99
  */
101
- function getSessionFromSessionStorage(context) {
100
+ function getSessionFromLocalStorage(context) {
102
101
  return readStoredSession(context);
103
102
  }
104
103
  /**
105
104
  * Persists both `expiresAt` and `whoami` to the in-memory store and
106
- * `sessionStorage` as a single JSON blob. Called from `init.ts` after
105
+ * `localStorage` as a single JSON blob. Called from `init.ts` after
107
106
  * a successful OAuth token exchange.
108
107
  */
109
108
  function storeSession(options) {
@@ -118,7 +117,7 @@ function storeSession(options) {
118
117
  });
119
118
  }
120
119
  /**
121
- * Updates only `expiresAt` in both the store and `sessionStorage`,
120
+ * Updates only `expiresAt` in both the store and `localStorage`,
122
121
  * preserving the existing `whoami` from in-memory store state. Called
123
122
  * from `refresh.ts` after a successful token refresh.
124
123
  */
@@ -132,11 +131,11 @@ function storeExpiresAt(options) {
132
131
  });
133
132
  }
134
133
  /**
135
- * Removes the single session-storage key, clearing all persisted
134
+ * Removes the single local-storage key, clearing all persisted
136
135
  * session state. Called on disconnect and on auth-complete errors.
137
136
  */
138
- function clearSessionStorage(context) {
139
- context.sessionStorage.removeItem(SESSION_STORAGE_KEY);
137
+ function clearLocalStorage(context) {
138
+ context.localStorage.removeItem(LOCAL_STORAGE_KEY);
140
139
  }
141
140
  /**
142
141
  * Returns `true` when the controller has an `expiresAt` whose value
@@ -166,11 +165,11 @@ async function completeHubSpotOAuthSession(context, options) {
166
165
  credentials: "include"
167
166
  });
168
167
  } catch (err) {
169
- clearSessionStorage(context);
168
+ clearLocalStorage(context);
170
169
  throw new Error(`Failed to complete HubSpot OAuth: ${err instanceof Error ? err.message : String(err)}`);
171
170
  }
172
171
  if (!response.ok) {
173
- clearSessionStorage(context);
172
+ clearLocalStorage(context);
174
173
  throw new Error(`Failed to complete HubSpot OAuth: ${response.status} ${response.statusText}`);
175
174
  }
176
175
  return await response.json();
@@ -353,23 +352,32 @@ async function startHubSpotConnection(context) {
353
352
  window.location.href = authorizationUrl;
354
353
  }
355
354
  //#endregion
356
- //#region src/browser/app-connect-controller/default-session-storage.ts
355
+ //#region src/browser/app-connect-controller/default-local-storage.ts
357
356
  /**
358
- * Builds a `SessionStorage` adapter that delegates to the global
359
- * `sessionStorage` object exposed by browsers. The controller uses
357
+ * Builds a `LocalStorageAdapter` that delegates to the global
358
+ * `localStorage` object exposed by browsers. The controller uses
360
359
  * this when no custom storage is supplied (e.g. for tests or non-DOM
361
360
  * environments).
362
361
  */
363
- function createDefaultSessionStorage() {
362
+ function createDefaultLocalStorageAdapter() {
364
363
  return {
365
364
  setItem: (key, value) => {
366
- sessionStorage.setItem(key, value);
365
+ localStorage.setItem(key, value);
367
366
  },
368
367
  getItem: (key) => {
369
- return sessionStorage.getItem(key);
368
+ return localStorage.getItem(key);
370
369
  },
371
370
  removeItem: (key) => {
372
- sessionStorage.removeItem(key);
371
+ localStorage.removeItem(key);
372
+ },
373
+ addStorageListener: (listener) => {
374
+ const handler = (event) => listener({
375
+ key: event.key,
376
+ oldValue: event.oldValue,
377
+ newValue: event.newValue
378
+ });
379
+ window.addEventListener("storage", handler);
380
+ return () => window.removeEventListener("storage", handler);
373
381
  }
374
382
  };
375
383
  }
@@ -380,7 +388,7 @@ function createDefaultSessionStorage() {
380
388
  *
381
389
  * 1. Calls the SDK's `auth/logout` route to revoke the upstream token
382
390
  * and clear the refresh-token cookie.
383
- * 2. Clears the local session-storage `expiresAt` entry.
391
+ * 2. Clears the local storage `expiresAt` entry.
384
392
  * 3. Updates the controller state to `disconnected` so the UI re-renders
385
393
  * without a page reload.
386
394
  *
@@ -396,7 +404,7 @@ async function disconnectFromHubSpot(context) {
396
404
  });
397
405
  const { hubSpotConnectBaseUrl: appConnectBaseUrl } = config;
398
406
  try {
399
- clearSessionStorage(context);
407
+ clearLocalStorage(context);
400
408
  const response = await fetch(`${appConnectBaseUrl}/auth/logout`, {
401
409
  method: "POST",
402
410
  credentials: "include"
@@ -660,13 +668,13 @@ function getDerivedStatus(state) {
660
668
  * as a prop and exposes it via context.
661
669
  *
662
670
  * The returned controller is inert until `start()` is called: nothing
663
- * is read from session storage, no refresh timer is scheduled, and no
671
+ * is read from local storage, no refresh timer is scheduled, and no
664
672
  * fetches are issued. Tests can construct a controller and inspect
665
673
  * its initial snapshot without triggering side effects.
666
674
  */
667
675
  function createAppConnectController(options) {
668
676
  const { config, logger = noopLogger } = options;
669
- const sessionStorage = createDefaultSessionStorage();
677
+ const localStorage = createDefaultLocalStorageAdapter();
670
678
  const store = createStore({
671
679
  isInitComplete: false,
672
680
  isConnectInFlight: false,
@@ -679,10 +687,10 @@ function createAppConnectController(options) {
679
687
  const context = {
680
688
  config,
681
689
  logger,
682
- sessionStorage,
690
+ localStorage,
683
691
  store
684
692
  };
685
- const storedSession = getSessionFromSessionStorage(context);
693
+ const storedSession = getSessionFromLocalStorage(context);
686
694
  store.setState({
687
695
  expiresAt: storedSession?.expiresAt ?? null,
688
696
  whoami: storedSession?.whoami ?? null
@@ -724,6 +732,34 @@ function createAppConnectController(options) {
724
732
  }
725
733
  hasStarted = true;
726
734
  startRefreshScheduler(context);
735
+ context.localStorage.addStorageListener((event) => {
736
+ if (event.key !== "hubspot_connect_session") return;
737
+ if (event.newValue === null) store.setState({
738
+ expiresAt: null,
739
+ whoami: null,
740
+ isSessionConnected: false
741
+ });
742
+ else try {
743
+ const parsed = JSON.parse(event.newValue);
744
+ const expiresAt = typeof parsed.expiresAt === "number" && !isNaN(parsed.expiresAt) ? parsed.expiresAt : null;
745
+ if (expiresAt !== null) store.setState({
746
+ expiresAt,
747
+ whoami: parsed.whoami ?? null,
748
+ isSessionConnected: Date.now() < expiresAt
749
+ });
750
+ else store.setState({
751
+ expiresAt: null,
752
+ whoami: null,
753
+ isSessionConnected: false
754
+ });
755
+ } catch {
756
+ store.setState({
757
+ expiresAt: null,
758
+ whoami: null,
759
+ isSessionConnected: false
760
+ });
761
+ }
762
+ });
727
763
  logger.info("start: initSdk (OAuth return handling if applicable)");
728
764
  (async () => {
729
765
  try {
@@ -750,4 +786,4 @@ function createAppConnectController(options) {
750
786
  //#endregion
751
787
  export { createLogger as n, createAppConnectController as t };
752
788
 
753
- //# sourceMappingURL=create-DxEyGG-k.js.map
789
+ //# sourceMappingURL=create-ULhURoJ_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-ULhURoJ_.js","names":["disconnectFromHubSpot","runDisconnectFromHubSpot"],"sources":["../../src/shared/logger.ts","../../src/shared/constants.ts","../../src/browser/app-connect-controller/constants.ts","../../src/browser/app-connect-controller/utils/session-utils.ts","../../src/browser/app-connect-controller/oauth-complete.ts","../../src/browser/app-connect-controller/oauth-popup.ts","../../src/browser/app-connect-controller/utils/iframe-utils.ts","../../src/browser/app-connect-controller/utils/resolve-oauth-connect-mode.ts","../../src/browser/app-connect-controller/utils/timeout-utils.ts","../../src/browser/app-connect-controller/connect-start.ts","../../src/browser/app-connect-controller/default-local-storage.ts","../../src/browser/app-connect-controller/disconnect.ts","../../src/browser/app-connect-controller/init.ts","../../src/browser/app-connect-controller/refresh.ts","../../src/browser/app-connect-controller/utils/memoize-utils.ts","../../src/browser/app-connect-controller/utils/store-utils.ts","../../src/browser/app-connect-controller/view-state.ts","../../src/browser/app-connect-controller/create.ts"],"sourcesContent":["/**\n * Pluggable logger contract used by the SDK on both the browser and\n * server. Consumers can pass `console`-like loggers, structured\n * loggers (pino / winston / etc.) or no-op stubs in tests.\n */\nexport interface Logger {\n debug: (message: string, ...args: unknown[]) => void;\n info: (message: string, ...args: unknown[]) => void;\n warn: (message: string, ...args: unknown[]) => void;\n error: (message: string, ...args: unknown[]) => void;\n}\n\nfunction formatPrefix(name: string): string {\n return `[${name}]`;\n}\n\n/**\n * Creates a console-backed logger that prefixes every line with the\n * supplied `name`. Used as the default when no custom logger is\n * provided.\n */\nexport function createLogger(name: string): Logger {\n const prefix = formatPrefix(name);\n return {\n debug: (message, ...args) => {\n console.debug(prefix, message, ...args);\n },\n info: (message, ...args) => {\n console.info(prefix, message, ...args);\n },\n warn: (message, ...args) => {\n console.warn(prefix, message, ...args);\n },\n error: (message, ...args) => {\n console.error(prefix, message, ...args);\n },\n };\n}\n\n/**\n * Logger that swallows every message. Convenient for tests and for\n * the SDK's server-side handlers when no logger is provided by the\n * host application.\n */\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n};\n","/**\n * Constants whose values are part of the contract between the browser\n * controller and the server-side hubspot-connect routes. Both halves\n * import from this module so the wire format stays in sync.\n */\n\n/**\n * Query parameter on the OAuth return URL that carries the new access\n * token's expiry (Unix epoch milliseconds). The browser controller\n * sets this in the URL after a successful `auth/complete` call and\n * then strips it during `initAppConnect` via `history.replaceState`.\n */\nexport const EXPIRES_AT_URL_PARAM = '__hs_expires_at';\n\n/**\n * Path the browser visits after HubSpot's authorize endpoint\n * redirects back to the app. Mounted on the **frontend** origin (not\n * the SDK's edge function host) so all OAuth-related cookies live in\n * the `(frontend, edge)` CHIPS partition.\n *\n * The SDK's `auth/init-session` builds the OAuth `redirect_uri` as\n * `${requestOrigin}${HUBSPOT_FRONTEND_CALLBACK_PATH}`. The browser\n * controller, on `start()`, recognizes this path on `window.location`\n * and forwards `?code` + `?state` to the SDK's `auth/complete`\n * endpoint via a credentialed cross-site fetch. The host app must\n * register `${app_origin}${HUBSPOT_FRONTEND_CALLBACK_PATH}` as a\n * redirect URI in its HubSpot app settings.\n */\nexport const OAUTH_CALLBACK_PATH = '/__hubspot_oauth_callback';\n\n/**\n * Query parameter on the `auth/complete` POST request carrying the\n * authorization `code` HubSpot returned to the frontend callback.\n */\nexport const AUTH_COMPLETE_CODE_PARAM = 'code';\n\n/**\n * Query parameter on the `auth/complete` POST request carrying the\n * OAuth `state` HubSpot echoed back to the frontend callback.\n */\nexport const AUTH_COMPLETE_STATE_PARAM = 'state';\n\n/**\n * `postMessage` `data.type` value the OAuth popup sends to its opener\n * with the authorization `code` and `state` from the callback URL. The\n * opener POSTs them to `auth/complete` so credentialed cookies stay in\n * the same CHIPS partition as `auth/init-session`.\n */\nexport const OAUTH_POPUP_CALLBACK_MESSAGE_TYPE =\n 'hubspot-app-connect:oauth-callback';\n","export { EXPIRES_AT_URL_PARAM } from '../../shared/constants.ts';\n\n/**\n * Key under which the controller persists the entire session blob\n * (expiresAt + whoami) as a single JSON string in `localStorage`.\n * Using one key makes logout trivially correct — one `removeItem` call\n * clears all session state.\n */\nexport const LOCAL_STORAGE_KEY = 'hubspot_connect_session';\n\n/**\n * Number of milliseconds before `expiresAt` that the refresh\n * scheduler attempts to mint a new access token. 60s is comfortably\n * larger than typical network latency without burning lifetime.\n */\nexport const REFRESH_BUFFER_MS = 60_000;\n","import type { AuthCompleteWhoami } from '../../../shared/wire-types.ts';\nimport { LOCAL_STORAGE_KEY } from '../constants.ts';\nimport type { AppConnectContext } from '../types.ts';\n\ninterface StoredSession {\n expiresAt: number;\n whoami: AuthCompleteWhoami | null;\n}\n\nfunction readStoredSession(context: AppConnectContext): StoredSession | null {\n const raw = context.localStorage.getItem(LOCAL_STORAGE_KEY);\n if (!raw) return null;\n let parsed: StoredSession;\n try {\n parsed = JSON.parse(raw) as StoredSession;\n } catch {\n context.localStorage.removeItem(LOCAL_STORAGE_KEY);\n return null;\n }\n if (typeof parsed.expiresAt !== 'number' || isNaN(parsed.expiresAt)) {\n context.localStorage.removeItem(LOCAL_STORAGE_KEY);\n return null;\n }\n if (Date.now() > parsed.expiresAt) {\n context.localStorage.removeItem(LOCAL_STORAGE_KEY);\n return null;\n }\n return parsed;\n}\n\nfunction writeStoredSession(\n context: AppConnectContext,\n session: StoredSession\n): void {\n context.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(session));\n}\n\n/**\n * Reads the persisted session blob from `localStorage`. Returns\n * `null` (and auto-removes the entry) if the blob is missing,\n * malformed, or already expired.\n */\nexport function getSessionFromLocalStorage(\n context: AppConnectContext\n): StoredSession | null {\n return readStoredSession(context);\n}\n\n/**\n * Persists both `expiresAt` and `whoami` to the in-memory store and\n * `localStorage` as a single JSON blob. Called from `init.ts` after\n * a successful OAuth token exchange.\n */\nexport function storeSession(options: {\n context: AppConnectContext;\n expiresAtMs: number;\n whoami: AuthCompleteWhoami;\n}): void {\n const { context, expiresAtMs, whoami } = options;\n context.store.setState({ expiresAt: expiresAtMs, whoami });\n writeStoredSession(context, { expiresAt: expiresAtMs, whoami });\n}\n\n/**\n * Updates only `expiresAt` in both the store and `localStorage`,\n * preserving the existing `whoami` from in-memory store state. Called\n * from `refresh.ts` after a successful token refresh.\n */\nexport function storeExpiresAt(options: {\n context: AppConnectContext;\n expiresAtMs: number;\n}): void {\n const { context, expiresAtMs } = options;\n const currentWhoami = context.store.getSnapshot().whoami;\n context.store.setState({ expiresAt: expiresAtMs });\n writeStoredSession(context, {\n expiresAt: expiresAtMs,\n whoami: currentWhoami,\n });\n}\n\n/**\n * Removes the single local-storage key, clearing all persisted\n * session state. Called on disconnect and on auth-complete errors.\n */\nexport function clearLocalStorage(context: AppConnectContext): void {\n context.localStorage.removeItem(LOCAL_STORAGE_KEY);\n}\n\n/**\n * Returns `true` when the controller has an `expiresAt` whose value\n * is still in the future. Used to drive the UI status and the refresh\n * scheduler.\n */\nexport function isClientSessionActive(context: AppConnectContext): boolean {\n const state = context.store.getSnapshot();\n const expiresAt = state.expiresAt;\n return expiresAt !== null && Date.now() < expiresAt;\n}\n","import {\n AUTH_COMPLETE_CODE_PARAM,\n AUTH_COMPLETE_STATE_PARAM,\n} from '../../shared/constants.ts';\nimport type { AuthCompleteResponse } from '../../shared/wire-types.ts';\nimport type { AppConnectContext } from './types.ts';\nimport { clearLocalStorage } from './utils/session-utils.ts';\n\ninterface CompleteHubSpotOAuthSessionOptions {\n code: string;\n state: string;\n}\n\n/**\n * Finishes the OAuth token exchange by POSTing `code` and `state` to the\n * SDK's `auth/complete` route with credentialed cookies from the current\n * browsing context (must match the partition used by `auth/init-session`).\n */\nexport async function completeHubSpotOAuthSession(\n context: AppConnectContext,\n options: CompleteHubSpotOAuthSessionOptions\n): Promise<AuthCompleteResponse> {\n const { code, state } = options;\n\n const completeUrl = new URL(\n `${context.config.hubSpotConnectBaseUrl}/auth/complete`,\n window.location.origin\n );\n completeUrl.searchParams.set(AUTH_COMPLETE_CODE_PARAM, code);\n completeUrl.searchParams.set(AUTH_COMPLETE_STATE_PARAM, state);\n\n let response: Response;\n try {\n response = await fetch(completeUrl.toString(), {\n method: 'POST',\n credentials: 'include',\n });\n } catch (err) {\n clearLocalStorage(context);\n throw new Error(\n `Failed to complete HubSpot OAuth: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n\n if (!response.ok) {\n clearLocalStorage(context);\n throw new Error(\n `Failed to complete HubSpot OAuth: ${response.status} ${response.statusText}`\n );\n }\n\n return (await response.json()) as AuthCompleteResponse;\n}\n","import { OAUTH_POPUP_CALLBACK_MESSAGE_TYPE } from '../../shared/constants.ts';\nimport { completeHubSpotOAuthSession } from './oauth-complete.ts';\nimport type { AppConnectContext } from './types.ts';\nimport { storeExpiresAt } from './utils/session-utils.ts';\n\nexport const OAUTH_POPUP_WINDOW_NAME = 'hubspot-app-connect-oauth';\nconst OAUTH_POPUP_POLL_INTERVAL_MS = 300;\n\ninterface OAuthPopupCallbackMessage {\n type: typeof OAUTH_POPUP_CALLBACK_MESSAGE_TYPE;\n code: string;\n state: string;\n}\n\ninterface WaitForHubSpotOAuthPopupOptions {\n context: AppConnectContext;\n authorizationUrl: string;\n}\n\ninterface RelayOAuthCallbackToOpenerOptions {\n code: string;\n state: string;\n}\n\nfunction isOAuthPopupCallbackMessage(\n data: unknown\n): data is OAuthPopupCallbackMessage {\n if (typeof data !== 'object' || data === null) return false;\n const record = data as Record<string, unknown>;\n return (\n record.type === OAUTH_POPUP_CALLBACK_MESSAGE_TYPE &&\n typeof record.code === 'string' &&\n record.code.length > 0 &&\n typeof record.state === 'string' &&\n record.state.length > 0\n );\n}\n\n/**\n * Opens HubSpot's authorize URL in a popup and waits for the callback\n * page to relay `code` + `state` back. The opener POSTs to\n * `auth/complete` so credentialed cookies use the same CHIPS partition\n * as `auth/init-session`.\n */\nexport async function waitForHubSpotOAuthPopup(\n options: WaitForHubSpotOAuthPopupOptions\n): Promise<void> {\n const { context, authorizationUrl } = options;\n const targetOrigin = window.location.origin;\n\n return new Promise<void>((resolve, reject) => {\n let popup: Window | null = null;\n let pollTimer: ReturnType<typeof setInterval> | undefined;\n let isSettled = false;\n let isCallbackReceived = false;\n\n const cleanup = () => {\n window.removeEventListener('message', onMessage);\n if (pollTimer !== undefined) clearInterval(pollTimer);\n };\n\n const settleSuccess = (expiresAtMs: number) => {\n if (isSettled) return;\n isSettled = true;\n cleanup();\n storeExpiresAt({ context, expiresAtMs });\n context.store.setState({ isSessionConnected: true, error: null });\n resolve();\n };\n\n const settleFailure = (message: string) => {\n if (isSettled) return;\n isSettled = true;\n cleanup();\n try {\n popup?.close();\n } catch {\n // ignore close errors\n }\n reject(new Error(message));\n };\n\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== targetOrigin) return;\n if (!isOAuthPopupCallbackMessage(event.data)) return;\n\n if (isCallbackReceived) return;\n isCallbackReceived = true;\n\n void (async () => {\n try {\n const body = await completeHubSpotOAuthSession(context, {\n code: event.data.code,\n state: event.data.state,\n });\n settleSuccess(body.expires_at);\n } catch (err) {\n const message =\n err instanceof Error ? err.message : 'HubSpot OAuth failed';\n settleFailure(message);\n }\n })();\n };\n\n window.addEventListener('message', onMessage);\n\n popup = window.open(\n authorizationUrl,\n OAUTH_POPUP_WINDOW_NAME,\n 'popup=yes,width=600,height=700'\n );\n if (!popup) {\n settleFailure('Popup blocked. Allow popups for this site and try again.');\n return;\n }\n\n pollTimer = setInterval(() => {\n if (popup?.closed && !isCallbackReceived) {\n settleFailure('Connect to HubSpot was cancelled.');\n }\n }, OAUTH_POPUP_POLL_INTERVAL_MS);\n });\n}\n\n/**\n * Called from the OAuth callback page inside the popup. Relays `code`\n * and `state` to the opener (which runs `auth/complete`) and closes.\n */\nexport function relayOAuthCallbackToOpener(\n options: RelayOAuthCallbackToOpenerOptions\n): boolean {\n const { code, state } = options;\n const opener = window.opener;\n if (!opener || opener.closed) return false;\n\n const message: OAuthPopupCallbackMessage = {\n type: OAUTH_POPUP_CALLBACK_MESSAGE_TYPE,\n code,\n state,\n };\n opener.postMessage(message, window.location.origin);\n try {\n window.close();\n } catch {\n // ignore close errors\n }\n return true;\n}\n\nexport function hasOAuthPopupOpener(): boolean {\n try {\n return Boolean(window.opener && !window.opener.closed);\n } catch {\n return false;\n }\n}\n\nexport function isOAuthPopupCallback(): boolean {\n return window.name === OAUTH_POPUP_WINDOW_NAME && hasOAuthPopupOpener();\n}\n","/**\n * Returns `true` when the app runs inside a parent frame (same-origin\n * or cross-origin). Cross-origin parent access throws; treat that as\n * embedded so OAuth uses a popup instead of a top-level redirect.\n */\nexport function isAppEmbeddedInIframe(): boolean {\n try {\n return window.self !== window.top;\n } catch {\n return true;\n }\n}\n","import type { OAuthConnectMode } from '../../types.ts';\nimport { isAppEmbeddedInIframe } from './iframe-utils.ts';\n\nexport type ResolvedOAuthConnectMode = 'redirect' | 'popup';\n\ninterface ResolveOAuthConnectModeOptions {\n oauthConnectMode?: OAuthConnectMode;\n}\n\n/**\n * Maps the configured {@link OAuthConnectMode} to the concrete connect\n * behavior for the current browsing context.\n */\nexport function resolveOAuthConnectMode(\n options: ResolveOAuthConnectModeOptions\n): ResolvedOAuthConnectMode {\n const mode = options.oauthConnectMode ?? 'auto';\n if (mode === 'popup') return 'popup';\n if (mode === 'redirect') return 'redirect';\n return isAppEmbeddedInIframe() ? 'popup' : 'redirect';\n}\n","export function delay(ms: number): Promise<void> {\n if (ms <= 0) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n","import type { InitSessionResponse } from '../../shared/wire-types.ts';\nimport { waitForHubSpotOAuthPopup } from './oauth-popup.ts';\nimport type { AppConnectContext } from './types.ts';\nimport { resolveOAuthConnectMode } from './utils/resolve-oauth-connect-mode.ts';\nimport { delay } from './utils/timeout-utils.ts';\n\n/** Extra wait before redirect so the connect progress UI is visible; set to `0` to disable. */\nconst ARTIFICIAL_CONNECT_REDIRECT_DELAY_MS = 500;\n\n/**\n * Begins the OAuth connect flow:\n *\n * 1. Calls the SDK's `auth/init-session` route to mint a fresh PKCE\n * verifier + state and obtain HubSpot's `authorize` URL.\n * 2. Navigates to that URL via full-page redirect, or opens it in a\n * popup when embedded in an iframe or when `oauthConnectMode` is\n * `'popup'`.\n *\n * The `return_path` is the current path + query so the user lands\n * back where they started after authorizing (redirect mode only).\n *\n * Throws when the init call fails. Does not return after a redirect\n * begins because the page is unloaded.\n */\nexport async function startHubSpotConnection(\n context: AppConnectContext\n): Promise<void> {\n const { config } = context;\n\n const returnPath = `${window.location.pathname}${window.location.search}`;\n\n const initUrl = new URL(\n `${config.hubSpotConnectBaseUrl}/auth/init-session`,\n window.location.origin\n );\n initUrl.searchParams.set('return_path', returnPath);\n\n const initResponse = await fetch(initUrl.toString(), {\n credentials: 'include',\n });\n if (!initResponse.ok)\n throw new Error(`Failed to init session: ${initResponse.status}`);\n const { authorization_url: authorizationUrl } =\n (await initResponse.json()) as InitSessionResponse;\n\n await delay(ARTIFICIAL_CONNECT_REDIRECT_DELAY_MS);\n\n const connectMode = resolveOAuthConnectMode(\n config.oauthConnectMode !== undefined\n ? { oauthConnectMode: config.oauthConnectMode }\n : {}\n );\n\n if (connectMode === 'popup') {\n await waitForHubSpotOAuthPopup({ context, authorizationUrl });\n return;\n }\n\n window.location.href = authorizationUrl;\n}\n","import type { LocalStorageAdapter } from './types.ts';\n\n/**\n * Builds a `LocalStorageAdapter` that delegates to the global\n * `localStorage` object exposed by browsers. The controller uses\n * this when no custom storage is supplied (e.g. for tests or non-DOM\n * environments).\n */\nexport function createDefaultLocalStorageAdapter(): LocalStorageAdapter {\n return {\n setItem: (key, value) => {\n localStorage.setItem(key, value);\n },\n getItem: (key) => {\n return localStorage.getItem(key);\n },\n removeItem: (key) => {\n localStorage.removeItem(key);\n },\n addStorageListener: (listener) => {\n const handler = (event: StorageEvent) =>\n listener({\n key: event.key,\n oldValue: event.oldValue,\n newValue: event.newValue,\n });\n window.addEventListener('storage', handler);\n return () => window.removeEventListener('storage', handler);\n },\n };\n}\n","import type { AppConnectContext } from './types.ts';\nimport { clearLocalStorage } from './utils/session-utils.ts';\n\n/**\n * Disconnect flow:\n *\n * 1. Calls the SDK's `auth/logout` route to revoke the upstream token\n * and clear the refresh-token cookie.\n * 2. Clears the local storage `expiresAt` entry.\n * 3. Updates the controller state to `disconnected` so the UI re-renders\n * without a page reload.\n *\n * Errors are caught, logged, and surfaced via the controller's\n * `error` field so the UI can show a retry state.\n */\nexport async function disconnectFromHubSpot(\n context: AppConnectContext\n): Promise<void> {\n const { config, logger, store } = context;\n logger.info('disconnectFromHubSpot: starting');\n store.setState({ error: null, isDisconnectInFlight: true });\n const { hubSpotConnectBaseUrl: appConnectBaseUrl } = config;\n\n try {\n clearLocalStorage(context);\n\n const response = await fetch(`${appConnectBaseUrl}/auth/logout`, {\n method: 'POST',\n credentials: 'include',\n });\n if (!response.ok) {\n throw new Error(`Logout failed: ${response.status}`);\n }\n await response.body?.cancel();\n\n store.setState({\n expiresAt: null,\n whoami: null,\n isSessionConnected: false,\n isDisconnectInFlight: false,\n });\n\n logger.info('disconnectFromHubSpot: complete');\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Disconnect failed';\n logger.error('disconnectFromHubSpot: failed', err);\n store.setState({\n error: message,\n isDisconnectInFlight: false,\n });\n }\n}\n","import {\n AUTH_COMPLETE_CODE_PARAM,\n AUTH_COMPLETE_STATE_PARAM,\n OAUTH_CALLBACK_PATH,\n} from '../../shared/constants.ts';\nimport { completeHubSpotOAuthSession } from './oauth-complete.ts';\nimport {\n isOAuthPopupCallback,\n relayOAuthCallbackToOpener,\n} from './oauth-popup.ts';\nimport type { AppConnectContext } from './types.ts';\nimport { storeSession } from './utils/session-utils.ts';\n\n/**\n * On `controller.start()`:\n *\n * 1. If the browser has been redirected back to the SDK's frontend\n * OAuth callback path (`/__hubspot_oauth_callback`) with `?code` +\n * `?state`:\n * - **Redirect flow** (no `window.opener`): POST to `auth/complete`\n * from this window, persist `expires_at`, and `history.replaceState`\n * to `return_path`.\n * - **Popup flow** (`window.opener` present): relay `code` + `state`\n * to the opener via `postMessage` and close. The opener POSTs to\n * `auth/complete` so partitioned cookies match `init-session`.\n * 2. Pick up `?__hs_expires_at` from `window.location` (refresh hop),\n * persist it, and strip it from the address bar.\n *\n * A no-op when neither set of parameters is present.\n */\nexport async function initAppConnect(\n context: AppConnectContext\n): Promise<void> {\n await consumeOAuthCallback(context);\n}\n\nasync function consumeOAuthCallback(context: AppConnectContext): Promise<void> {\n if (window.location.pathname !== OAUTH_CALLBACK_PATH) return;\n\n const params = new URLSearchParams(window.location.search);\n const code = params.get(AUTH_COMPLETE_CODE_PARAM);\n const state = params.get(AUTH_COMPLETE_STATE_PARAM);\n if (!code || !state) return;\n\n if (isOAuthPopupCallback() && relayOAuthCallbackToOpener({ code, state })) {\n return;\n }\n\n const body = await completeHubSpotOAuthSession(context, { code, state });\n const { expires_at: expiresAt, return_path: returnPath } = body;\n\n storeSession({ context, expiresAtMs: expiresAt, whoami: body.whoami });\n\n const targetUrl = new URL(returnPath, window.location.origin);\n history.replaceState(\n null,\n '',\n `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`\n );\n}\n","import type { RefreshTokenResponse } from '../../shared/wire-types.ts';\nimport { REFRESH_BUFFER_MS } from './constants.ts';\nimport type { AppConnectContext } from './types.ts';\nimport {\n isClientSessionActive,\n storeExpiresAt,\n} from './utils/session-utils.ts';\n\n/**\n * Tear-down handle returned by {@link startRefreshScheduler}. Calling\n * `stop()` clears any pending refresh timer and unsubscribes from the\n * store so the controller can be garbage-collected.\n */\nexport interface RefreshSchedulerHandle {\n stop: () => void;\n}\n\nasync function refreshAccessToken(context: AppConnectContext): Promise<void> {\n const { config } = context;\n\n const refreshResponse = await fetch(\n `${config.hubSpotConnectBaseUrl}/auth/refresh`,\n {\n method: 'POST',\n credentials: 'include',\n }\n );\n if (!refreshResponse.ok) {\n throw new Error(`Refresh failed: ${refreshResponse.status}`);\n }\n const { expires_in: expiresInSeconds } =\n (await refreshResponse.json()) as RefreshTokenResponse;\n if (\n typeof expiresInSeconds !== 'number' ||\n !Number.isFinite(expiresInSeconds) ||\n expiresInSeconds <= 0\n ) {\n throw new Error('Refresh response missing or invalid expires_in');\n }\n const expiresAtMs = Date.now() + expiresInSeconds * 1000;\n\n storeExpiresAt({ context, expiresAtMs });\n}\n\n/**\n * Subscribes to store changes and (re)schedules a token refresh\n * whenever `expiresAt` moves. Returns a handle that the caller can\n * use to stop the scheduler when the controller is destroyed.\n */\nexport function startRefreshScheduler(\n context: AppConnectContext\n): RefreshSchedulerHandle {\n const { logger, store } = context;\n\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let stopped = false;\n\n const scheduleRefresh = () => {\n if (refreshTimer) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n if (stopped) return;\n\n const state = store.getSnapshot();\n if (!state.isInitComplete || !state.isSessionConnected) {\n return;\n }\n\n const expiresAt = state.expiresAt;\n if (!expiresAt) {\n logger.debug('scheduleRefresh: no expiresAt, skipping');\n return;\n }\n const delayMs = Math.max(0, expiresAt - Date.now() - REFRESH_BUFFER_MS);\n logger.debug(\n 'scheduleRefresh: next refresh in ',\n (delayMs / 1000).toFixed(1),\n 's',\n {\n expiresAt,\n }\n );\n refreshTimer = setTimeout(() => {\n logger.debug('scheduleRefresh: timer fired, refreshing token');\n refreshTimer = null;\n if (stopped) return;\n\n void (async () => {\n try {\n await refreshAccessToken(context);\n if (stopped) return;\n if (isClientSessionActive(context)) {\n logger.info('token refresh: success, session still active');\n } else {\n logger.warn(\n 'token refresh: success but no active session in storage'\n );\n store.setState({ isSessionConnected: false });\n }\n } catch (err) {\n logger.error('token refresh: failed', err);\n if (stopped) return;\n store.setState({ isSessionConnected: false });\n }\n })();\n }, delayMs);\n };\n\n const unsubscribe = store.subscribe(() => {\n scheduleRefresh();\n });\n\n return {\n stop: () => {\n stopped = true;\n unsubscribe();\n if (refreshTimer) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n },\n };\n}\n","/**\n * Wraps `fn` so that calls with the same input (compared via\n * `Object.is`) return the previous output without re-invoking `fn`.\n * The cache holds at most one entry, so this is safe to use for\n * derived view-state from a single store snapshot.\n *\n * Used by `getSnapshot` to keep the React state reference stable\n * between unrelated store updates — `useSyncExternalStore` would\n * otherwise re-render every consumer on every change.\n */\nexport function memoizeLast<TInput, TOutput>(\n fn: (input: TInput) => TOutput\n): (input: TInput) => TOutput {\n let lastInput: TInput;\n let lastOutput: TOutput;\n let hasValue = false;\n return (input: TInput): TOutput => {\n if (hasValue && Object.is(lastInput, input)) {\n return lastOutput;\n }\n lastInput = input;\n lastOutput = fn(input);\n hasValue = true;\n return lastOutput;\n };\n}\n","/**\n * Tiny external store used by the controller. Shaped to be compatible\n * with React's `useSyncExternalStore` while remaining usable outside\n * React.\n */\nexport interface Store<TState extends object> {\n /** Returns the current state. The reference changes on every update. */\n getSnapshot: () => Readonly<TState>;\n /**\n * Subscribes to state changes. Returns an unsubscribe function the\n * caller can invoke at teardown.\n */\n subscribe: (onChange: () => void) => () => void;\n /**\n * Merges `update` into the current state. When `update` is a\n * function, it receives the current state and returns a partial.\n * Listeners are only notified when at least one key actually\n * changed (shallow compare).\n */\n setState: (\n update:\n | Partial<TState>\n | ((prev: Readonly<TState>) => Partial<TState> | TState)\n ) => void;\n /** Reads a single key from the current state. */\n get: <K extends keyof TState>(key: K) => TState[K];\n /** Writes a single key. Listeners only fire when the value changes. */\n set: <K extends keyof TState>(key: K, value: TState[K]) => void;\n /**\n * Drops every listener and prevents future `setState`/`set` calls\n * from notifying. Used by `controller.destroy()`.\n */\n destroy: () => void;\n}\n\nfunction shallowEqualState<TState extends object>(\n a: TState,\n b: TState\n): boolean {\n const keys = new Set([\n ...Object.keys(a),\n ...Object.keys(b),\n ] as (keyof TState)[]);\n for (const k of keys) {\n if (!Object.is(a[k], b[k])) {\n return false;\n }\n }\n return true;\n}\n\nfunction mergeState<TState extends object>(\n prev: TState,\n partial: Partial<TState>\n): TState {\n return { ...prev, ...partial } as TState;\n}\n\n/**\n * Creates a new {@link Store}. The store starts with a shallow copy\n * of `initialState`; subsequent mutations never touch the caller's\n * object.\n */\nexport function createStore<TState extends object>(\n initialState: TState\n): Store<TState> {\n let state: TState = { ...initialState };\n const listeners = new Set<() => void>();\n let destroyed = false;\n\n const notify = () => {\n for (const listener of listeners) {\n listener();\n }\n };\n\n return {\n getSnapshot() {\n return state as Readonly<TState>;\n },\n subscribe(onChange) {\n listeners.add(onChange);\n return () => {\n listeners.delete(onChange);\n };\n },\n setState(update) {\n if (destroyed) return;\n const patch =\n typeof update === 'function'\n ? update(state as Readonly<TState>)\n : update;\n if (typeof patch !== 'object' || patch == null) {\n return;\n }\n const next = mergeState(state, patch as Partial<TState>);\n if (shallowEqualState(state, next)) {\n return;\n }\n state = next;\n notify();\n },\n get(key) {\n return state[key];\n },\n set(key, value) {\n if (destroyed) return;\n if (Object.is(state[key], value)) {\n return;\n }\n state = { ...state, [key]: value } as TState;\n notify();\n },\n destroy() {\n destroyed = true;\n listeners.clear();\n },\n };\n}\n","import type { AppConnectState, AppConnectStatus } from '../types.ts';\nimport type { AppConnectInternalState } from './types.ts';\n\nconst noop = (): Promise<void> => Promise.resolve();\n\n/**\n * Snapshot returned by `getServerSnapshot` for SSR. Has stable\n * references and inert connect/disconnect actions because actions are\n * meaningless before hydration.\n */\nexport const SERVER_VIEW: AppConnectState = {\n status: 'initializing',\n error: null,\n whoami: null,\n connectToHubSpot: noop,\n disconnectFromHubSpot: noop,\n};\n\n/**\n * Reduces the boolean lifecycle flags into the user-facing\n * `AppConnectStatus` enum value. The order of checks matters:\n * disconnect-in-flight beats connect-in-flight (a transitional logout\n * shouldn't show a \"connecting\" spinner), and connected beats default.\n */\nexport function getDerivedStatus(\n state: AppConnectInternalState\n): AppConnectStatus {\n const {\n isInitComplete,\n isConnectInFlight,\n isSessionConnected,\n isDisconnectInFlight,\n } = state;\n if (!isInitComplete) {\n return 'initializing';\n }\n if (isDisconnectInFlight) return 'disconnecting';\n if (isConnectInFlight) return 'connecting';\n if (isSessionConnected) return 'connected';\n return 'disconnected';\n}\n","import { noopLogger, type Logger } from '../../shared/logger.ts';\nimport type {\n AppConnectBrowserConfig,\n AppConnectController,\n AppConnectState,\n} from '../types.ts';\nimport { startHubSpotConnection } from './connect-start.ts';\nimport { LOCAL_STORAGE_KEY } from './constants.ts';\nimport { createDefaultLocalStorageAdapter } from './default-local-storage.ts';\nimport { disconnectFromHubSpot as runDisconnectFromHubSpot } from './disconnect.ts';\nimport { initAppConnect } from './init.ts';\nimport { startRefreshScheduler } from './refresh.ts';\nimport type {\n AppConnectContext,\n AppConnectInternalState,\n AppConnectStore,\n} from './types.ts';\nimport { memoizeLast } from './utils/memoize-utils.ts';\nimport {\n getSessionFromLocalStorage,\n isClientSessionActive,\n} from './utils/session-utils.ts';\nimport { createStore } from './utils/store-utils.ts';\nimport { getDerivedStatus, SERVER_VIEW } from './view-state.ts';\n\n/**\n * Options accepted by {@link createAppConnectController}.\n */\nexport interface CreateAppConnectControllerOptions {\n /** Runtime configuration; see {@link AppConnectBrowserConfig}. */\n config: AppConnectBrowserConfig;\n /** Logger the controller uses for status/debug messages. */\n logger?: Logger;\n}\n\n/**\n * Creates an `AppConnectController`. Exactly one controller should be\n * shared by the entire app — the React provider takes the controller\n * as a prop and exposes it via context.\n *\n * The returned controller is inert until `start()` is called: nothing\n * is read from local storage, no refresh timer is scheduled, and no\n * fetches are issued. Tests can construct a controller and inspect\n * its initial snapshot without triggering side effects.\n */\nexport function createAppConnectController(\n options: CreateAppConnectControllerOptions\n): AppConnectController {\n const { config, logger = noopLogger } = options;\n const localStorage = createDefaultLocalStorageAdapter();\n const store: AppConnectStore = createStore<AppConnectInternalState>({\n isInitComplete: false,\n isConnectInFlight: false,\n isDisconnectInFlight: false,\n isSessionConnected: false,\n error: null,\n expiresAt: null,\n whoami: null,\n });\n const context: AppConnectContext = {\n config,\n logger,\n localStorage,\n store,\n };\n\n const storedSession = getSessionFromLocalStorage(context);\n store.setState({\n expiresAt: storedSession?.expiresAt ?? null,\n whoami: storedSession?.whoami ?? null,\n });\n\n let hasStarted = false;\n\n const connectToHubSpot = async () => {\n logger.info('connectToHubSpot: starting');\n store.setState({ error: null, isConnectInFlight: true });\n try {\n await startHubSpotConnection(context);\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Connection failed';\n logger.error('connectToHubSpot: failed', err);\n store.setState({ error: message });\n } finally {\n logger.debug(\n 'connectToHubSpot: connect flow step finished (may redirect to HubSpot)'\n );\n store.setState({ isConnectInFlight: false });\n }\n };\n const disconnectFromHubSpot = () => runDisconnectFromHubSpot(context);\n\n const getViewStateMemoized = memoizeLast<\n Readonly<AppConnectInternalState>,\n AppConnectState\n >((storeState) => ({\n status: getDerivedStatus(storeState),\n error: storeState.error,\n whoami: storeState.whoami,\n connectToHubSpot,\n disconnectFromHubSpot,\n }));\n\n function getSnapshot() {\n return getViewStateMemoized(store.getSnapshot());\n }\n\n return {\n start() {\n if (hasStarted) {\n logger.debug('start skipped (already started)');\n return;\n }\n hasStarted = true;\n startRefreshScheduler(context);\n\n context.localStorage.addStorageListener((event) => {\n if (event.key !== LOCAL_STORAGE_KEY) return;\n if (event.newValue === null) {\n store.setState({\n expiresAt: null,\n whoami: null,\n isSessionConnected: false,\n });\n } else {\n try {\n const parsed = JSON.parse(event.newValue) as {\n expiresAt: unknown;\n whoami: unknown;\n };\n const expiresAt =\n typeof parsed.expiresAt === 'number' && !isNaN(parsed.expiresAt)\n ? parsed.expiresAt\n : null;\n if (expiresAt !== null) {\n store.setState({\n expiresAt,\n whoami:\n (parsed.whoami as AppConnectInternalState['whoami']) ?? null,\n isSessionConnected: Date.now() < expiresAt,\n });\n } else {\n store.setState({\n expiresAt: null,\n whoami: null,\n isSessionConnected: false,\n });\n }\n } catch {\n store.setState({\n expiresAt: null,\n whoami: null,\n isSessionConnected: false,\n });\n }\n }\n });\n\n logger.info('start: initSdk (OAuth return handling if applicable)');\n void (async () => {\n try {\n await initAppConnect(context);\n logger.info('initSdk: completed without error');\n } catch (err) {\n logger.error('initSdk: failed', err);\n store.setState({\n error:\n err instanceof Error\n ? err.message\n : 'App Connect initialization failed',\n });\n } finally {\n const sessionActive = isClientSessionActive(context);\n logger.info('start: init complete, session active:', sessionActive);\n store.setState({\n isInitComplete: true,\n isSessionConnected: sessionActive,\n });\n }\n })();\n },\n subscribe: (fn) => store.subscribe(fn),\n getSnapshot,\n getServerSnapshot: () => SERVER_VIEW,\n };\n}\n"],"mappings":"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"}
@@ -35,7 +35,7 @@ interface CreateAppConnectControllerOptions {
35
35
  * as a prop and exposes it via context.
36
36
  *
37
37
  * The returned controller is inert until `start()` is called: nothing
38
- * is read from session storage, no refresh timer is scheduled, and no
38
+ * is read from local storage, no refresh timer is scheduled, and no
39
39
  * fetches are issued. Tests can construct a controller and inspect
40
40
  * its initial snapshot without triggering side effects.
41
41
  */
@@ -1,3 +1,3 @@
1
- import { n as createLogger, t as createAppConnectController } from "./create-DxEyGG-k.js";
1
+ import { n as createLogger, t as createAppConnectController } from "./create-ULhURoJ_.js";
2
2
  import { n as themeVars, t as themeClass } from "./theme.css-CJbxi5hC.js";
3
3
  export { createAppConnectController, createLogger, themeClass, themeVars };
@@ -1,4 +1,4 @@
1
- import { t as createAppConnectController } from "../create-DxEyGG-k.js";
1
+ import { t as createAppConnectController } from "../create-ULhURoJ_.js";
2
2
  import { t as HubSpotAppConnect } from "../HubSpotAppConnect-721kYr9d.js";
3
3
  import { useRef } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
@@ -26,8 +26,11 @@ async function fetchWhoami(accessToken, hubspotConnectEnv) {
26
26
  whoami.hub.uiDomain = portal.uiDomain;
27
27
  }
28
28
  if (introspectResult.status === "fulfilled" && introspectResult.value.token_use === "access_token") {
29
- whoami.hub.domain = introspectResult.value.hub_domain;
30
- const userId = String(introspectResult.value.user_id);
29
+ const introspectData = introspectResult.value;
30
+ whoami.hub.domain = introspectData.hub_domain;
31
+ if (typeof introspectData.user === "string" && introspectData.user.includes("@")) whoami.user.email = introspectData.user;
32
+ whoami.user.id = String(introspectData.user_id);
33
+ const userId = String(introspectData.user_id);
31
34
  const userResult = await apiClient.send(settingsUsers.get({
32
35
  userId,
33
36
  idProperty: "USER_ID"
@@ -1 +1 @@
1
- {"version":3,"file":"whoami.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/whoami.ts"],"sourcesContent":["import type { AuthCompleteWhoami } from '../../../shared/wire-types.ts';\nimport {\n account,\n authOauth,\n createHubSpotClient,\n settingsUsers,\n} from '../../api-client-core/index.ts';\nimport { fetchTransportPlugin } from '../../api-client-core/plugins/fetch-transport.ts';\nimport { getHubSpotApiOrigin } from '../../utils/env-utils.ts';\nimport type { HubSpotConnectRoutesEnv } from './load-hubspot-connect-routes-env.ts';\n\nexport async function fetchWhoami(\n accessToken: string,\n hubspotConnectEnv: HubSpotConnectRoutesEnv\n): Promise<AuthCompleteWhoami> {\n const apiClient = createHubSpotClient({\n plugins: [\n fetchTransportPlugin({\n getEndpoint: getHubSpotApiOrigin,\n getAccessToken: () => accessToken,\n }),\n ],\n });\n\n const introspectInput = hubspotConnectEnv.isCimdEnabled\n ? { token: accessToken }\n : {\n client_id: hubspotConnectEnv.hubspotClientId,\n client_secret: hubspotConnectEnv.hubspotClientSecret,\n token: accessToken,\n };\n\n // Introspect and account.get are independent — run in parallel.\n // settingsUsers.get requires user_id from introspect, so it runs after.\n const [introspectResult, hubResult] = await Promise.allSettled([\n apiClient.send(authOauth.introspectToken(introspectInput)),\n apiClient.send(account.get()),\n ]);\n\n const whoami: AuthCompleteWhoami = {\n hub: {},\n user: {},\n };\n\n if (hubResult.status === 'fulfilled') {\n const portal = hubResult.value;\n whoami.hub.id = portal.portalId;\n whoami.hub.uiDomain = portal.uiDomain;\n }\n\n if (\n introspectResult.status === 'fulfilled' &&\n introspectResult.value.token_use === 'access_token'\n ) {\n whoami.hub.domain = introspectResult.value.hub_domain;\n\n const userId = String(introspectResult.value.user_id);\n const userResult = await apiClient\n .send(settingsUsers.get({ userId, idProperty: 'USER_ID' }))\n .then(\n (u) => ({ ok: true as const, value: u }),\n () => ({ ok: false as const })\n );\n if (userResult.ok) {\n const u = userResult.value;\n whoami.user.id = u.id;\n whoami.user.email = u.email;\n whoami.user.firstName = u.firstName;\n whoami.user.lastName = u.lastName;\n }\n }\n\n return whoami;\n}\n"],"mappings":";;;;;;;AAWA,eAAsB,YACpB,aACA,mBAC6B;CAC7B,MAAM,YAAY,oBAAoB,EACpC,SAAS,CACP,qBAAqB;EACnB,aAAa;EACb,sBAAsB;CACxB,CAAC,CACH,EACF,CAAC;CAED,MAAM,kBAAkB,kBAAkB,gBACtC,EAAE,OAAO,YAAY,IACrB;EACE,WAAW,kBAAkB;EAC7B,eAAe,kBAAkB;EACjC,OAAO;CACT;CAIJ,MAAM,CAAC,kBAAkB,aAAa,MAAM,QAAQ,WAAW,CAC7D,UAAU,KAAK,UAAU,gBAAgB,eAAe,CAAC,GACzD,UAAU,KAAK,QAAQ,IAAI,CAAC,CAC9B,CAAC;CAED,MAAM,SAA6B;EACjC,KAAK,CAAC;EACN,MAAM,CAAC;CACT;CAEA,IAAI,UAAU,WAAW,aAAa;EACpC,MAAM,SAAS,UAAU;EACzB,OAAO,IAAI,KAAK,OAAO;EACvB,OAAO,IAAI,WAAW,OAAO;CAC/B;CAEA,IACE,iBAAiB,WAAW,eAC5B,iBAAiB,MAAM,cAAc,gBACrC;EACA,OAAO,IAAI,SAAS,iBAAiB,MAAM;EAE3C,MAAM,SAAS,OAAO,iBAAiB,MAAM,OAAO;EACpD,MAAM,aAAa,MAAM,UACtB,KAAK,cAAc,IAAI;GAAE;GAAQ,YAAY;EAAU,CAAC,CAAC,EACzD,MACE,OAAO;GAAE,IAAI;GAAe,OAAO;EAAE,WAC/B,EAAE,IAAI,MAAe,EAC9B;EACF,IAAI,WAAW,IAAI;GACjB,MAAM,IAAI,WAAW;GACrB,OAAO,KAAK,KAAK,EAAE;GACnB,OAAO,KAAK,QAAQ,EAAE;GACtB,OAAO,KAAK,YAAY,EAAE;GAC1B,OAAO,KAAK,WAAW,EAAE;EAC3B;CACF;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"whoami.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/whoami.ts"],"sourcesContent":["import type { AuthCompleteWhoami } from '../../../shared/wire-types.ts';\nimport {\n account,\n authOauth,\n createHubSpotClient,\n settingsUsers,\n} from '../../api-client-core/index.ts';\nimport { fetchTransportPlugin } from '../../api-client-core/plugins/fetch-transport.ts';\nimport { getHubSpotApiOrigin } from '../../utils/env-utils.ts';\nimport type { HubSpotConnectRoutesEnv } from './load-hubspot-connect-routes-env.ts';\n\nexport async function fetchWhoami(\n accessToken: string,\n hubspotConnectEnv: HubSpotConnectRoutesEnv\n): Promise<AuthCompleteWhoami> {\n const apiClient = createHubSpotClient({\n plugins: [\n fetchTransportPlugin({\n getEndpoint: getHubSpotApiOrigin,\n getAccessToken: () => accessToken,\n }),\n ],\n });\n\n const introspectInput = hubspotConnectEnv.isCimdEnabled\n ? { token: accessToken }\n : {\n client_id: hubspotConnectEnv.hubspotClientId,\n client_secret: hubspotConnectEnv.hubspotClientSecret,\n token: accessToken,\n };\n\n // Introspect and account.get are independent — run in parallel.\n // settingsUsers.get requires user_id from introspect, so it runs after.\n const [introspectResult, hubResult] = await Promise.allSettled([\n apiClient.send(authOauth.introspectToken(introspectInput)),\n apiClient.send(account.get()),\n ]);\n\n const whoami: AuthCompleteWhoami = {\n hub: {},\n user: {},\n };\n\n if (hubResult.status === 'fulfilled') {\n const portal = hubResult.value;\n whoami.hub.id = portal.portalId;\n whoami.hub.uiDomain = portal.uiDomain;\n }\n\n if (\n introspectResult.status === 'fulfilled' &&\n introspectResult.value.token_use === 'access_token'\n ) {\n const introspectData = introspectResult.value;\n /*\n Sample output from the introspect endpoint:\n {\n active: true,\n token: \"...\",\n hub_id: 51192929,\n user_id: 82931853,\n client_id: \"41dc0bc9-9766-4934-989b-86b6d403bdf7\",\n app_id: 38772786,\n user: \"psteeleidem@hubspot.com\",\n hub_domain: \"nestiq.psteeleidem.com\",\n scopes: [\n \"oauth\",\n \"crm.objects.contacts.read\",\n \"crm.objects.contacts.write\",\n \"settings.users.read\",\n \"crm.objects.users.read\"\n ],\n signed_access_token: {\n expiresAt: 1779382954033,\n scopes: \"QlNQMl8kQEwrAgUACAkWEgxz\",\n hubId: 51192929,\n userId: 82931853,\n appId: 38772786,\n signature: \"h572Md+LUq6YZQKpkdIM+kvaSKE=\",\n scopeToScopeGroupPks: \"QlNQMl8kQEwrAxIAAY4CBBkfkAHeAZQCtwL1AvoCowSkBKUEpwSoBJWaBQ==\",\n newSignature: \"TKz044UdHDmzy2IZmvhQrf6k7ko=\",\n hublet: \"na1\",\n trialScopes: \"\",\n trialScopeToScopeGroupPks: \"\",\n isUserLevel: false,\n isPrivateDistribution: true\n },\n expires_in: 1799,\n is_private_distribution: true,\n token_use: \"access_token\",\n token_type: \"Bearer\"\n }\n */\n whoami.hub.domain = introspectData.hub_domain;\n if (\n typeof introspectData.user === 'string' &&\n introspectData.user.includes('@')\n ) {\n whoami.user.email = introspectData.user;\n }\n whoami.user.id = String(introspectData.user_id);\n\n const userId = String(introspectData.user_id);\n const userResult = await apiClient\n .send(settingsUsers.get({ userId, idProperty: 'USER_ID' }))\n .then(\n (u) => ({ ok: true as const, value: u }),\n () => ({ ok: false as const })\n );\n if (userResult.ok) {\n const u = userResult.value;\n whoami.user.id = u.id;\n whoami.user.email = u.email;\n whoami.user.firstName = u.firstName;\n whoami.user.lastName = u.lastName;\n }\n }\n\n return whoami;\n}\n"],"mappings":";;;;;;;AAWA,eAAsB,YACpB,aACA,mBAC6B;CAC7B,MAAM,YAAY,oBAAoB,EACpC,SAAS,CACP,qBAAqB;EACnB,aAAa;EACb,sBAAsB;CACxB,CAAC,CACH,EACF,CAAC;CAED,MAAM,kBAAkB,kBAAkB,gBACtC,EAAE,OAAO,YAAY,IACrB;EACE,WAAW,kBAAkB;EAC7B,eAAe,kBAAkB;EACjC,OAAO;CACT;CAIJ,MAAM,CAAC,kBAAkB,aAAa,MAAM,QAAQ,WAAW,CAC7D,UAAU,KAAK,UAAU,gBAAgB,eAAe,CAAC,GACzD,UAAU,KAAK,QAAQ,IAAI,CAAC,CAC9B,CAAC;CAED,MAAM,SAA6B;EACjC,KAAK,CAAC;EACN,MAAM,CAAC;CACT;CAEA,IAAI,UAAU,WAAW,aAAa;EACpC,MAAM,SAAS,UAAU;EACzB,OAAO,IAAI,KAAK,OAAO;EACvB,OAAO,IAAI,WAAW,OAAO;CAC/B;CAEA,IACE,iBAAiB,WAAW,eAC5B,iBAAiB,MAAM,cAAc,gBACrC;EACA,MAAM,iBAAiB,iBAAiB;EAwCxC,OAAO,IAAI,SAAS,eAAe;EACnC,IACE,OAAO,eAAe,SAAS,YAC/B,eAAe,KAAK,SAAS,GAAG,GAEhC,OAAO,KAAK,QAAQ,eAAe;EAErC,OAAO,KAAK,KAAK,OAAO,eAAe,OAAO;EAE9C,MAAM,SAAS,OAAO,eAAe,OAAO;EAC5C,MAAM,aAAa,MAAM,UACtB,KAAK,cAAc,IAAI;GAAE;GAAQ,YAAY;EAAU,CAAC,CAAC,EACzD,MACE,OAAO;GAAE,IAAI;GAAe,OAAO;EAAE,WAC/B,EAAE,IAAI,MAAe,EAC9B;EACF,IAAI,WAAW,IAAI;GACjB,MAAM,IAAI,WAAW;GACrB,OAAO,KAAK,KAAK,EAAE;GACnB,OAAO,KAAK,QAAQ,EAAE;GACtB,OAAO,KAAK,YAAY,EAAE;GAC1B,OAAO,KAAK,WAAW,EAAE;EAC3B;CACF;CAEA,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/app-connect-sdk",
3
- "version": "1.0.0-alpha.21",
3
+ "version": "1.0.0-alpha.23",
4
4
  "description": "HubSpot App Connect SDK (alpha release). Documentation and integration guidance forthcoming.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -45,8 +45,8 @@
45
45
  "typescript": "6.0.3",
46
46
  "vitest": "4.1.6",
47
47
  "@private/eslint-config": "0.1.0",
48
- "@private/tsconfig": "0.1.0",
49
- "@private/prettier-config": "0.1.0"
48
+ "@private/prettier-config": "0.1.0",
49
+ "@private/tsconfig": "0.1.0"
50
50
  },
51
51
  "scripts": {
52
52
  "clean": "rm -rf dist build *.tsbuildinfo node_modules .turbo",
@@ -5,14 +5,14 @@ import { startHubSpotConnection } from './connect-start.ts';
5
5
  import type {
6
6
  AppConnectContext,
7
7
  AppConnectInternalState,
8
- SessionStorage,
8
+ LocalStorageAdapter,
9
9
  } from './types.ts';
10
10
  import { createStore } from './utils/store-utils.ts';
11
11
 
12
12
  const HUBSPOT_CONNECT_BASE_URL =
13
13
  'https://edge.example.com/functions/v1/hubspot-connect';
14
14
 
15
- function createInMemorySessionStorage(): SessionStorage {
15
+ function createInMemoryLocalStorageAdapter(): LocalStorageAdapter {
16
16
  const map = new Map<string, string>();
17
17
  return {
18
18
  getItem: (key) => map.get(key) ?? null,
@@ -22,6 +22,7 @@ function createInMemorySessionStorage(): SessionStorage {
22
22
  removeItem: (key) => {
23
23
  map.delete(key);
24
24
  },
25
+ addStorageListener: (_listener) => () => {},
25
26
  };
26
27
  }
27
28
 
@@ -43,7 +44,7 @@ function createTestContext(
43
44
  ...(oauthConnectMode !== undefined ? { oauthConnectMode } : {}),
44
45
  },
45
46
  logger: noopLogger,
46
- sessionStorage: createInMemorySessionStorage(),
47
+ localStorage: createInMemoryLocalStorageAdapter(),
47
48
  store: createStore<AppConnectInternalState>(initialState),
48
49
  };
49
50
  }
@@ -2,12 +2,11 @@ export { EXPIRES_AT_URL_PARAM } from '../../shared/constants.ts';
2
2
 
3
3
  /**
4
4
  * Key under which the controller persists the entire session blob
5
- * (expiresAt + whoami) as a single JSON string in `sessionStorage`.
5
+ * (expiresAt + whoami) as a single JSON string in `localStorage`.
6
6
  * Using one key makes logout trivially correct — one `removeItem` call
7
- * clears all session state. Survives full-page navigations within the
8
- * same tab.
7
+ * clears all session state.
9
8
  */
10
- export const SESSION_STORAGE_KEY = 'hubspot_connect_session';
9
+ export const LOCAL_STORAGE_KEY = 'hubspot_connect_session';
11
10
 
12
11
  /**
13
12
  * Number of milliseconds before `expiresAt` that the refresh
@@ -5,7 +5,8 @@ import type {
5
5
  AppConnectState,
6
6
  } from '../types.ts';
7
7
  import { startHubSpotConnection } from './connect-start.ts';
8
- import { createDefaultSessionStorage } from './default-session-storage.ts';
8
+ import { LOCAL_STORAGE_KEY } from './constants.ts';
9
+ import { createDefaultLocalStorageAdapter } from './default-local-storage.ts';
9
10
  import { disconnectFromHubSpot as runDisconnectFromHubSpot } from './disconnect.ts';
10
11
  import { initAppConnect } from './init.ts';
11
12
  import { startRefreshScheduler } from './refresh.ts';
@@ -16,7 +17,7 @@ import type {
16
17
  } from './types.ts';
17
18
  import { memoizeLast } from './utils/memoize-utils.ts';
18
19
  import {
19
- getSessionFromSessionStorage,
20
+ getSessionFromLocalStorage,
20
21
  isClientSessionActive,
21
22
  } from './utils/session-utils.ts';
22
23
  import { createStore } from './utils/store-utils.ts';
@@ -38,7 +39,7 @@ export interface CreateAppConnectControllerOptions {
38
39
  * as a prop and exposes it via context.
39
40
  *
40
41
  * The returned controller is inert until `start()` is called: nothing
41
- * is read from session storage, no refresh timer is scheduled, and no
42
+ * is read from local storage, no refresh timer is scheduled, and no
42
43
  * fetches are issued. Tests can construct a controller and inspect
43
44
  * its initial snapshot without triggering side effects.
44
45
  */
@@ -46,7 +47,7 @@ export function createAppConnectController(
46
47
  options: CreateAppConnectControllerOptions
47
48
  ): AppConnectController {
48
49
  const { config, logger = noopLogger } = options;
49
- const sessionStorage = createDefaultSessionStorage();
50
+ const localStorage = createDefaultLocalStorageAdapter();
50
51
  const store: AppConnectStore = createStore<AppConnectInternalState>({
51
52
  isInitComplete: false,
52
53
  isConnectInFlight: false,
@@ -59,11 +60,11 @@ export function createAppConnectController(
59
60
  const context: AppConnectContext = {
60
61
  config,
61
62
  logger,
62
- sessionStorage,
63
+ localStorage,
63
64
  store,
64
65
  };
65
66
 
66
- const storedSession = getSessionFromSessionStorage(context);
67
+ const storedSession = getSessionFromLocalStorage(context);
67
68
  store.setState({
68
69
  expiresAt: storedSession?.expiresAt ?? null,
69
70
  whoami: storedSession?.whoami ?? null,
@@ -113,6 +114,48 @@ export function createAppConnectController(
113
114
  hasStarted = true;
114
115
  startRefreshScheduler(context);
115
116
 
117
+ context.localStorage.addStorageListener((event) => {
118
+ if (event.key !== LOCAL_STORAGE_KEY) return;
119
+ if (event.newValue === null) {
120
+ store.setState({
121
+ expiresAt: null,
122
+ whoami: null,
123
+ isSessionConnected: false,
124
+ });
125
+ } else {
126
+ try {
127
+ const parsed = JSON.parse(event.newValue) as {
128
+ expiresAt: unknown;
129
+ whoami: unknown;
130
+ };
131
+ const expiresAt =
132
+ typeof parsed.expiresAt === 'number' && !isNaN(parsed.expiresAt)
133
+ ? parsed.expiresAt
134
+ : null;
135
+ if (expiresAt !== null) {
136
+ store.setState({
137
+ expiresAt,
138
+ whoami:
139
+ (parsed.whoami as AppConnectInternalState['whoami']) ?? null,
140
+ isSessionConnected: Date.now() < expiresAt,
141
+ });
142
+ } else {
143
+ store.setState({
144
+ expiresAt: null,
145
+ whoami: null,
146
+ isSessionConnected: false,
147
+ });
148
+ }
149
+ } catch {
150
+ store.setState({
151
+ expiresAt: null,
152
+ whoami: null,
153
+ isSessionConnected: false,
154
+ });
155
+ }
156
+ }
157
+ });
158
+
116
159
  logger.info('start: initSdk (OAuth return handling if applicable)');
117
160
  void (async () => {
118
161
  try {
@@ -0,0 +1,31 @@
1
+ import type { LocalStorageAdapter } from './types.ts';
2
+
3
+ /**
4
+ * Builds a `LocalStorageAdapter` that delegates to the global
5
+ * `localStorage` object exposed by browsers. The controller uses
6
+ * this when no custom storage is supplied (e.g. for tests or non-DOM
7
+ * environments).
8
+ */
9
+ export function createDefaultLocalStorageAdapter(): LocalStorageAdapter {
10
+ return {
11
+ setItem: (key, value) => {
12
+ localStorage.setItem(key, value);
13
+ },
14
+ getItem: (key) => {
15
+ return localStorage.getItem(key);
16
+ },
17
+ removeItem: (key) => {
18
+ localStorage.removeItem(key);
19
+ },
20
+ addStorageListener: (listener) => {
21
+ const handler = (event: StorageEvent) =>
22
+ listener({
23
+ key: event.key,
24
+ oldValue: event.oldValue,
25
+ newValue: event.newValue,
26
+ });
27
+ window.addEventListener('storage', handler);
28
+ return () => window.removeEventListener('storage', handler);
29
+ },
30
+ };
31
+ }
@@ -1,12 +1,12 @@
1
1
  import type { AppConnectContext } from './types.ts';
2
- import { clearSessionStorage } from './utils/session-utils.ts';
2
+ import { clearLocalStorage } from './utils/session-utils.ts';
3
3
 
4
4
  /**
5
5
  * Disconnect flow:
6
6
  *
7
7
  * 1. Calls the SDK's `auth/logout` route to revoke the upstream token
8
8
  * and clear the refresh-token cookie.
9
- * 2. Clears the local session-storage `expiresAt` entry.
9
+ * 2. Clears the local storage `expiresAt` entry.
10
10
  * 3. Updates the controller state to `disconnected` so the UI re-renders
11
11
  * without a page reload.
12
12
  *
@@ -22,7 +22,7 @@ export async function disconnectFromHubSpot(
22
22
  const { hubSpotConnectBaseUrl: appConnectBaseUrl } = config;
23
23
 
24
24
  try {
25
- clearSessionStorage(context);
25
+ clearLocalStorage(context);
26
26
 
27
27
  const response = await fetch(`${appConnectBaseUrl}/auth/logout`, {
28
28
  method: 'POST',