@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
@@ -5,20 +5,20 @@ import {
5
5
  OAUTH_POPUP_CALLBACK_MESSAGE_TYPE,
6
6
  } from '../../shared/constants.ts';
7
7
  import { noopLogger } from '../../shared/logger.ts';
8
- import { SESSION_STORAGE_KEY } from './constants.ts';
8
+ import { LOCAL_STORAGE_KEY } from './constants.ts';
9
9
  import { initAppConnect } from './init.ts';
10
10
  import { OAUTH_POPUP_WINDOW_NAME } from './oauth-popup.ts';
11
11
  import type {
12
12
  AppConnectContext,
13
13
  AppConnectInternalState,
14
- SessionStorage,
14
+ LocalStorageAdapter,
15
15
  } from './types.ts';
16
16
  import { createStore } from './utils/store-utils.ts';
17
17
 
18
18
  const HUBSPOT_CONNECT_BASE_URL =
19
19
  'https://edge.example.com/functions/v1/hubspot-connect';
20
20
 
21
- function createInMemorySessionStorage(): SessionStorage {
21
+ function createInMemoryLocalStorageAdapter(): LocalStorageAdapter {
22
22
  const map = new Map<string, string>();
23
23
  return {
24
24
  getItem: (key) => map.get(key) ?? null,
@@ -28,6 +28,7 @@ function createInMemorySessionStorage(): SessionStorage {
28
28
  removeItem: (key) => {
29
29
  map.delete(key);
30
30
  },
31
+ addStorageListener: (_listener) => () => {},
31
32
  };
32
33
  }
33
34
 
@@ -44,7 +45,7 @@ function createTestContext(): AppConnectContext {
44
45
  return {
45
46
  config: { hubSpotConnectBaseUrl: HUBSPOT_CONNECT_BASE_URL },
46
47
  logger: noopLogger,
47
- sessionStorage: createInMemorySessionStorage(),
48
+ localStorage: createInMemoryLocalStorageAdapter(),
48
49
  store: createStore<AppConnectInternalState>(initialState),
49
50
  };
50
51
  }
@@ -151,9 +152,7 @@ describe('initAppConnect', () => {
151
152
  expect((calledInit as RequestInit).credentials).toBe('include');
152
153
 
153
154
  expect(context.store.getSnapshot().expiresAt).toBe(expiresAt);
154
- const stored = JSON.parse(
155
- context.sessionStorage.getItem(SESSION_STORAGE_KEY)!
156
- );
155
+ const stored = JSON.parse(context.localStorage.getItem(LOCAL_STORAGE_KEY)!);
157
156
  expect(stored.expiresAt).toBe(expiresAt);
158
157
  expect(window.location.pathname).toBe('/dashboard');
159
158
  });
@@ -169,13 +168,13 @@ describe('initAppConnect', () => {
169
168
  })
170
169
  );
171
170
  const context = createTestContext();
172
- context.sessionStorage.setItem(
173
- SESSION_STORAGE_KEY,
171
+ context.localStorage.setItem(
172
+ LOCAL_STORAGE_KEY,
174
173
  '{"expiresAt":999,"whoami":null}'
175
174
  );
176
175
 
177
176
  await expect(initAppConnect(context)).rejects.toThrow(/Failed to complete/);
178
- expect(context.sessionStorage.getItem(SESSION_STORAGE_KEY)).toBeNull();
177
+ expect(context.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull();
179
178
  });
180
179
 
181
180
  it('relays code and state to the opener without calling auth/complete when in an OAuth popup', async () => {
@@ -1,19 +1,19 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { noopLogger } from '../../shared/logger.ts';
4
- import { SESSION_STORAGE_KEY } from './constants.ts';
4
+ import { LOCAL_STORAGE_KEY } from './constants.ts';
5
5
  import { completeHubSpotOAuthSession } from './oauth-complete.ts';
6
6
  import type {
7
7
  AppConnectContext,
8
8
  AppConnectInternalState,
9
- SessionStorage,
9
+ LocalStorageAdapter,
10
10
  } from './types.ts';
11
11
  import { createStore } from './utils/store-utils.ts';
12
12
 
13
13
  const HUBSPOT_CONNECT_BASE_URL =
14
14
  'https://edge.example.com/functions/v1/hubspot-connect';
15
15
 
16
- function createInMemorySessionStorage(): SessionStorage {
16
+ function createInMemoryLocalStorageAdapter(): LocalStorageAdapter {
17
17
  const map = new Map<string, string>();
18
18
  return {
19
19
  getItem: (key) => map.get(key) ?? null,
@@ -23,6 +23,7 @@ function createInMemorySessionStorage(): SessionStorage {
23
23
  removeItem: (key) => {
24
24
  map.delete(key);
25
25
  },
26
+ addStorageListener: (_listener) => () => {},
26
27
  };
27
28
  }
28
29
 
@@ -39,7 +40,7 @@ function createTestContext(): AppConnectContext {
39
40
  return {
40
41
  config: { hubSpotConnectBaseUrl: HUBSPOT_CONNECT_BASE_URL },
41
42
  logger: noopLogger,
42
- sessionStorage: createInMemorySessionStorage(),
43
+ localStorage: createInMemoryLocalStorageAdapter(),
43
44
  store: createStore<AppConnectInternalState>(initialState),
44
45
  };
45
46
  }
@@ -94,8 +95,8 @@ describe('completeHubSpotOAuthSession', () => {
94
95
  })
95
96
  );
96
97
  const context = createTestContext();
97
- context.sessionStorage.setItem(
98
- SESSION_STORAGE_KEY,
98
+ context.localStorage.setItem(
99
+ LOCAL_STORAGE_KEY,
99
100
  '{"expiresAt":999,"whoami":null}'
100
101
  );
101
102
 
@@ -105,6 +106,6 @@ describe('completeHubSpotOAuthSession', () => {
105
106
  state: 'bad-state',
106
107
  })
107
108
  ).rejects.toThrow(/Failed to complete/);
108
- expect(context.sessionStorage.getItem(SESSION_STORAGE_KEY)).toBeNull();
109
+ expect(context.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull();
109
110
  });
110
111
  });
@@ -4,7 +4,7 @@ import {
4
4
  } from '../../shared/constants.ts';
5
5
  import type { AuthCompleteResponse } from '../../shared/wire-types.ts';
6
6
  import type { AppConnectContext } from './types.ts';
7
- import { clearSessionStorage } from './utils/session-utils.ts';
7
+ import { clearLocalStorage } from './utils/session-utils.ts';
8
8
 
9
9
  interface CompleteHubSpotOAuthSessionOptions {
10
10
  code: string;
@@ -36,14 +36,14 @@ export async function completeHubSpotOAuthSession(
36
36
  credentials: 'include',
37
37
  });
38
38
  } catch (err) {
39
- clearSessionStorage(context);
39
+ clearLocalStorage(context);
40
40
  throw new Error(
41
41
  `Failed to complete HubSpot OAuth: ${err instanceof Error ? err.message : String(err)}`
42
42
  );
43
43
  }
44
44
 
45
45
  if (!response.ok) {
46
- clearSessionStorage(context);
46
+ clearLocalStorage(context);
47
47
  throw new Error(
48
48
  `Failed to complete HubSpot OAuth: ${response.status} ${response.statusText}`
49
49
  );
@@ -6,14 +6,14 @@ import { waitForHubSpotOAuthPopup } from './oauth-popup.ts';
6
6
  import type {
7
7
  AppConnectContext,
8
8
  AppConnectInternalState,
9
- SessionStorage,
9
+ LocalStorageAdapter,
10
10
  } from './types.ts';
11
11
  import { createStore } from './utils/store-utils.ts';
12
12
 
13
13
  const HUBSPOT_CONNECT_BASE_URL =
14
14
  'https://edge.example.com/functions/v1/hubspot-connect';
15
15
 
16
- function createInMemorySessionStorage(): SessionStorage {
16
+ function createInMemoryLocalStorageAdapter(): LocalStorageAdapter {
17
17
  const map = new Map<string, string>();
18
18
  return {
19
19
  getItem: (key) => map.get(key) ?? null,
@@ -23,6 +23,7 @@ function createInMemorySessionStorage(): SessionStorage {
23
23
  removeItem: (key) => {
24
24
  map.delete(key);
25
25
  },
26
+ addStorageListener: (_listener) => () => {},
26
27
  };
27
28
  }
28
29
 
@@ -39,7 +40,7 @@ function createTestContext(): AppConnectContext {
39
40
  return {
40
41
  config: { hubSpotConnectBaseUrl: HUBSPOT_CONNECT_BASE_URL },
41
42
  logger: noopLogger,
42
- sessionStorage: createInMemorySessionStorage(),
43
+ localStorage: createInMemoryLocalStorageAdapter(),
43
44
  store: createStore<AppConnectInternalState>(initialState),
44
45
  };
45
46
  }
@@ -4,14 +4,28 @@ import type { AppConnectBrowserConfig } from '../types.ts';
4
4
  import type { Store } from './utils/store-utils.ts';
5
5
 
6
6
  /**
7
- * Subset of the browser `Storage` interface the controller relies on.
8
- * Lets tests substitute an in-memory implementation without spinning
9
- * up a DOM.
7
+ * Subset of `StorageEvent` containing only the data fields. Avoids
8
+ * exposing the `storageArea: Storage` reference and keeps tests free
9
+ * of DOM dependencies.
10
10
  */
11
- export interface SessionStorage {
11
+ export interface StorageEventAdapter {
12
+ key: string | null;
13
+ oldValue: string | null;
14
+ newValue: string | null;
15
+ }
16
+
17
+ /**
18
+ * Subset of the browser `Storage` interface the controller relies on,
19
+ * plus a listener hook for cross-tab storage changes. Lets tests
20
+ * substitute an in-memory implementation without spinning up a DOM.
21
+ */
22
+ export interface LocalStorageAdapter {
12
23
  setItem: (key: string, value: string) => void;
13
24
  getItem: (key: string) => string | null;
14
25
  removeItem: (key: string) => void;
26
+ addStorageListener: (
27
+ listener: (event: StorageEventAdapter) => void
28
+ ) => () => void;
15
29
  }
16
30
 
17
31
  /**
@@ -46,6 +60,6 @@ export type AppConnectStore = Store<AppConnectInternalState>;
46
60
  export interface AppConnectContext {
47
61
  config: AppConnectBrowserConfig;
48
62
  logger: Logger;
49
- sessionStorage: SessionStorage;
63
+ localStorage: LocalStorageAdapter;
50
64
  store: AppConnectStore;
51
65
  }
@@ -2,22 +2,22 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { noopLogger } from '../../../shared/logger.ts';
4
4
  import type { AuthCompleteWhoami } from '../../../shared/wire-types.ts';
5
- import { SESSION_STORAGE_KEY } from '../constants.ts';
5
+ import { LOCAL_STORAGE_KEY } from '../constants.ts';
6
6
  import type {
7
7
  AppConnectContext,
8
8
  AppConnectInternalState,
9
- SessionStorage,
9
+ LocalStorageAdapter,
10
10
  } from '../types.ts';
11
11
  import {
12
- clearSessionStorage,
13
- getSessionFromSessionStorage,
12
+ clearLocalStorage,
13
+ getSessionFromLocalStorage,
14
14
  isClientSessionActive,
15
15
  storeExpiresAt,
16
16
  storeSession,
17
17
  } from './session-utils.ts';
18
18
  import { createStore } from './store-utils.ts';
19
19
 
20
- function createInMemorySessionStorage(): SessionStorage {
20
+ function createInMemoryLocalStorageAdapter(): LocalStorageAdapter {
21
21
  const map = new Map<string, string>();
22
22
  return {
23
23
  getItem: (key) => map.get(key) ?? null,
@@ -27,6 +27,7 @@ function createInMemorySessionStorage(): SessionStorage {
27
27
  removeItem: (key) => {
28
28
  map.delete(key);
29
29
  },
30
+ addStorageListener: (_listener) => () => {},
30
31
  };
31
32
  }
32
33
 
@@ -43,7 +44,7 @@ function createTestContext(): AppConnectContext {
43
44
  return {
44
45
  config: { hubSpotConnectBaseUrl: '/functions/v1/hubspot-connect' },
45
46
  logger: noopLogger,
46
- sessionStorage: createInMemorySessionStorage(),
47
+ localStorage: createInMemoryLocalStorageAdapter(),
47
48
  store: createStore<AppConnectInternalState>(initialState),
48
49
  };
49
50
  }
@@ -69,7 +70,7 @@ describe('session-utils', () => {
69
70
  });
70
71
 
71
72
  describe('storeSession', () => {
72
- it('writes expiresAt and whoami to both store and session storage', () => {
73
+ it('writes expiresAt and whoami to both store and local storage', () => {
73
74
  const context = createTestContext();
74
75
  storeSession({
75
76
  context,
@@ -80,7 +81,7 @@ describe('session-utils', () => {
80
81
  expect(context.store.getSnapshot().expiresAt).toBe(FIXED_NOW + 60_000);
81
82
  expect(context.store.getSnapshot().whoami).toEqual(MOCK_WHOAMI);
82
83
  const stored = JSON.parse(
83
- context.sessionStorage.getItem(SESSION_STORAGE_KEY)!
84
+ context.localStorage.getItem(LOCAL_STORAGE_KEY)!
84
85
  );
85
86
  expect(stored.expiresAt).toBe(FIXED_NOW + 60_000);
86
87
  expect(stored.whoami).toEqual(MOCK_WHOAMI);
@@ -97,63 +98,60 @@ describe('session-utils', () => {
97
98
  expect(context.store.getSnapshot().expiresAt).toBe(FIXED_NOW + 60_000);
98
99
  expect(context.store.getSnapshot().whoami).toEqual(MOCK_WHOAMI);
99
100
  const stored = JSON.parse(
100
- context.sessionStorage.getItem(SESSION_STORAGE_KEY)!
101
+ context.localStorage.getItem(LOCAL_STORAGE_KEY)!
101
102
  );
102
103
  expect(stored.expiresAt).toBe(FIXED_NOW + 60_000);
103
104
  expect(stored.whoami).toEqual(MOCK_WHOAMI);
104
105
  });
105
106
  });
106
107
 
107
- describe('getSessionFromSessionStorage', () => {
108
+ describe('getSessionFromLocalStorage', () => {
108
109
  it('returns null when nothing is stored', () => {
109
110
  const context = createTestContext();
110
- expect(getSessionFromSessionStorage(context)).toBeNull();
111
+ expect(getSessionFromLocalStorage(context)).toBeNull();
111
112
  });
112
113
 
113
114
  it('returns the stored session when expiresAt is in the future', () => {
114
115
  const context = createTestContext();
115
116
  const session = { expiresAt: FIXED_NOW + 60_000, whoami: MOCK_WHOAMI };
116
- context.sessionStorage.setItem(
117
- SESSION_STORAGE_KEY,
118
- JSON.stringify(session)
119
- );
120
- expect(getSessionFromSessionStorage(context)).toEqual(session);
117
+ context.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(session));
118
+ expect(getSessionFromLocalStorage(context)).toEqual(session);
121
119
  });
122
120
 
123
121
  it('clears and returns null on malformed JSON', () => {
124
122
  const context = createTestContext();
125
- context.sessionStorage.setItem(SESSION_STORAGE_KEY, 'not-json');
126
- expect(getSessionFromSessionStorage(context)).toBeNull();
127
- expect(context.sessionStorage.getItem(SESSION_STORAGE_KEY)).toBeNull();
123
+ context.localStorage.setItem(LOCAL_STORAGE_KEY, 'not-json');
124
+ expect(getSessionFromLocalStorage(context)).toBeNull();
125
+ expect(context.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull();
128
126
  });
129
127
 
130
128
  it('clears and returns null when expiresAt is not a number', () => {
131
129
  const context = createTestContext();
132
- context.sessionStorage.setItem(
133
- SESSION_STORAGE_KEY,
130
+ context.localStorage.setItem(
131
+ LOCAL_STORAGE_KEY,
134
132
  JSON.stringify({ expiresAt: 'bad', whoami: null })
135
133
  );
136
- expect(getSessionFromSessionStorage(context)).toBeNull();
137
- expect(context.sessionStorage.getItem(SESSION_STORAGE_KEY)).toBeNull();
134
+ expect(getSessionFromLocalStorage(context)).toBeNull();
135
+ expect(context.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull();
138
136
  });
139
137
 
140
138
  it('clears and returns null when the session is already expired', () => {
141
139
  const context = createTestContext();
142
- context.sessionStorage.setItem(
143
- SESSION_STORAGE_KEY,
140
+ context.localStorage.setItem(
141
+ LOCAL_STORAGE_KEY,
144
142
  JSON.stringify({ expiresAt: FIXED_NOW - 1, whoami: null })
145
143
  );
146
- expect(getSessionFromSessionStorage(context)).toBeNull();
147
- expect(context.sessionStorage.getItem(SESSION_STORAGE_KEY)).toBeNull();
144
+ expect(getSessionFromLocalStorage(context)).toBeNull();
145
+ expect(context.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull();
148
146
  });
149
147
  });
150
148
 
151
- describe('clearSessionStorage', () => {
152
- it('removes the session storage key', () => {
149
+ describe('clearLocalStorage', () => {
150
+ it('removes the local storage key', () => {
153
151
  const context = createTestContext();
154
- context.sessionStorage.setItem(SESSION_STORAGE_KEY, '{}');
155
- clearSessionStorage(context);
156
- expect(context.sessionStorage.getItem(SESSION_STORAGE_KEY)).toBeNull();
152
+ context.localStorage.setItem(LOCAL_STORAGE_KEY, '{}');
153
+ clearLocalStorage(context);
154
+ expect(context.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull();
157
155
  });
158
156
  });
159
157
 
@@ -1,5 +1,5 @@
1
1
  import type { AuthCompleteWhoami } from '../../../shared/wire-types.ts';
2
- import { SESSION_STORAGE_KEY } from '../constants.ts';
2
+ import { LOCAL_STORAGE_KEY } from '../constants.ts';
3
3
  import type { AppConnectContext } from '../types.ts';
4
4
 
5
5
  interface StoredSession {
@@ -8,21 +8,21 @@ interface StoredSession {
8
8
  }
9
9
 
10
10
  function readStoredSession(context: AppConnectContext): StoredSession | null {
11
- const raw = context.sessionStorage.getItem(SESSION_STORAGE_KEY);
11
+ const raw = context.localStorage.getItem(LOCAL_STORAGE_KEY);
12
12
  if (!raw) return null;
13
13
  let parsed: StoredSession;
14
14
  try {
15
15
  parsed = JSON.parse(raw) as StoredSession;
16
16
  } catch {
17
- context.sessionStorage.removeItem(SESSION_STORAGE_KEY);
17
+ context.localStorage.removeItem(LOCAL_STORAGE_KEY);
18
18
  return null;
19
19
  }
20
20
  if (typeof parsed.expiresAt !== 'number' || isNaN(parsed.expiresAt)) {
21
- context.sessionStorage.removeItem(SESSION_STORAGE_KEY);
21
+ context.localStorage.removeItem(LOCAL_STORAGE_KEY);
22
22
  return null;
23
23
  }
24
24
  if (Date.now() > parsed.expiresAt) {
25
- context.sessionStorage.removeItem(SESSION_STORAGE_KEY);
25
+ context.localStorage.removeItem(LOCAL_STORAGE_KEY);
26
26
  return null;
27
27
  }
28
28
  return parsed;
@@ -32,15 +32,15 @@ function writeStoredSession(
32
32
  context: AppConnectContext,
33
33
  session: StoredSession
34
34
  ): void {
35
- context.sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
35
+ context.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(session));
36
36
  }
37
37
 
38
38
  /**
39
- * Reads the persisted session blob from `sessionStorage`. Returns
39
+ * Reads the persisted session blob from `localStorage`. Returns
40
40
  * `null` (and auto-removes the entry) if the blob is missing,
41
41
  * malformed, or already expired.
42
42
  */
43
- export function getSessionFromSessionStorage(
43
+ export function getSessionFromLocalStorage(
44
44
  context: AppConnectContext
45
45
  ): StoredSession | null {
46
46
  return readStoredSession(context);
@@ -48,7 +48,7 @@ export function getSessionFromSessionStorage(
48
48
 
49
49
  /**
50
50
  * Persists both `expiresAt` and `whoami` to the in-memory store and
51
- * `sessionStorage` as a single JSON blob. Called from `init.ts` after
51
+ * `localStorage` as a single JSON blob. Called from `init.ts` after
52
52
  * a successful OAuth token exchange.
53
53
  */
54
54
  export function storeSession(options: {
@@ -62,7 +62,7 @@ export function storeSession(options: {
62
62
  }
63
63
 
64
64
  /**
65
- * Updates only `expiresAt` in both the store and `sessionStorage`,
65
+ * Updates only `expiresAt` in both the store and `localStorage`,
66
66
  * preserving the existing `whoami` from in-memory store state. Called
67
67
  * from `refresh.ts` after a successful token refresh.
68
68
  */
@@ -80,11 +80,11 @@ export function storeExpiresAt(options: {
80
80
  }
81
81
 
82
82
  /**
83
- * Removes the single session-storage key, clearing all persisted
83
+ * Removes the single local-storage key, clearing all persisted
84
84
  * session state. Called on disconnect and on auth-complete errors.
85
85
  */
86
- export function clearSessionStorage(context: AppConnectContext): void {
87
- context.sessionStorage.removeItem(SESSION_STORAGE_KEY);
86
+ export function clearLocalStorage(context: AppConnectContext): void {
87
+ context.localStorage.removeItem(LOCAL_STORAGE_KEY);
88
88
  }
89
89
 
90
90
  /**
@@ -52,9 +52,56 @@ export async function fetchWhoami(
52
52
  introspectResult.status === 'fulfilled' &&
53
53
  introspectResult.value.token_use === 'access_token'
54
54
  ) {
55
- whoami.hub.domain = introspectResult.value.hub_domain;
55
+ const introspectData = introspectResult.value;
56
+ /*
57
+ Sample output from the introspect endpoint:
58
+ {
59
+ active: true,
60
+ token: "...",
61
+ hub_id: 51192929,
62
+ user_id: 82931853,
63
+ client_id: "41dc0bc9-9766-4934-989b-86b6d403bdf7",
64
+ app_id: 38772786,
65
+ user: "psteeleidem@hubspot.com",
66
+ hub_domain: "nestiq.psteeleidem.com",
67
+ scopes: [
68
+ "oauth",
69
+ "crm.objects.contacts.read",
70
+ "crm.objects.contacts.write",
71
+ "settings.users.read",
72
+ "crm.objects.users.read"
73
+ ],
74
+ signed_access_token: {
75
+ expiresAt: 1779382954033,
76
+ scopes: "QlNQMl8kQEwrAgUACAkWEgxz",
77
+ hubId: 51192929,
78
+ userId: 82931853,
79
+ appId: 38772786,
80
+ signature: "h572Md+LUq6YZQKpkdIM+kvaSKE=",
81
+ scopeToScopeGroupPks: "QlNQMl8kQEwrAxIAAY4CBBkfkAHeAZQCtwL1AvoCowSkBKUEpwSoBJWaBQ==",
82
+ newSignature: "TKz044UdHDmzy2IZmvhQrf6k7ko=",
83
+ hublet: "na1",
84
+ trialScopes: "",
85
+ trialScopeToScopeGroupPks: "",
86
+ isUserLevel: false,
87
+ isPrivateDistribution: true
88
+ },
89
+ expires_in: 1799,
90
+ is_private_distribution: true,
91
+ token_use: "access_token",
92
+ token_type: "Bearer"
93
+ }
94
+ */
95
+ whoami.hub.domain = introspectData.hub_domain;
96
+ if (
97
+ typeof introspectData.user === 'string' &&
98
+ introspectData.user.includes('@')
99
+ ) {
100
+ whoami.user.email = introspectData.user;
101
+ }
102
+ whoami.user.id = String(introspectData.user_id);
56
103
 
57
- const userId = String(introspectResult.value.user_id);
104
+ const userId = String(introspectData.user_id);
58
105
  const userResult = await apiClient
59
106
  .send(settingsUsers.get({ userId, idProperty: 'USER_ID' }))
60
107
  .then(
@@ -1 +0,0 @@
1
- {"version":3,"file":"create-DxEyGG-k.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-session-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 `sessionStorage`.\n * Using one key makes logout trivially correct — one `removeItem` call\n * clears all session state. Survives full-page navigations within the\n * same tab.\n */\nexport const SESSION_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 { SESSION_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.sessionStorage.getItem(SESSION_STORAGE_KEY);\n if (!raw) return null;\n let parsed: StoredSession;\n try {\n parsed = JSON.parse(raw) as StoredSession;\n } catch {\n context.sessionStorage.removeItem(SESSION_STORAGE_KEY);\n return null;\n }\n if (typeof parsed.expiresAt !== 'number' || isNaN(parsed.expiresAt)) {\n context.sessionStorage.removeItem(SESSION_STORAGE_KEY);\n return null;\n }\n if (Date.now() > parsed.expiresAt) {\n context.sessionStorage.removeItem(SESSION_STORAGE_KEY);\n return null;\n }\n return parsed;\n}\n\nfunction writeStoredSession(\n context: AppConnectContext,\n session: StoredSession\n): void {\n context.sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));\n}\n\n/**\n * Reads the persisted session blob from `sessionStorage`. Returns\n * `null` (and auto-removes the entry) if the blob is missing,\n * malformed, or already expired.\n */\nexport function getSessionFromSessionStorage(\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 * `sessionStorage` 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 `sessionStorage`,\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 session-storage key, clearing all persisted\n * session state. Called on disconnect and on auth-complete errors.\n */\nexport function clearSessionStorage(context: AppConnectContext): void {\n context.sessionStorage.removeItem(SESSION_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 { clearSessionStorage } 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 clearSessionStorage(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 clearSessionStorage(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 { SessionStorage } from './types.ts';\n\n/**\n * Builds a `SessionStorage` adapter that delegates to the global\n * `sessionStorage` 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 createDefaultSessionStorage(): SessionStorage {\n return {\n setItem: (key, value) => {\n sessionStorage.setItem(key, value);\n },\n getItem: (key) => {\n return sessionStorage.getItem(key);\n },\n removeItem: (key) => {\n sessionStorage.removeItem(key);\n },\n };\n}\n","import type { AppConnectContext } from './types.ts';\nimport { clearSessionStorage } 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 session-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 clearSessionStorage(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 { createDefaultSessionStorage } from './default-session-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 getSessionFromSessionStorage,\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 session 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 sessionStorage = createDefaultSessionStorage();\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 sessionStorage,\n store,\n };\n\n const storedSession = getSessionFromSessionStorage(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 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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"}
@@ -1,21 +0,0 @@
1
- import type { SessionStorage } from './types.ts';
2
-
3
- /**
4
- * Builds a `SessionStorage` adapter that delegates to the global
5
- * `sessionStorage` 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 createDefaultSessionStorage(): SessionStorage {
10
- return {
11
- setItem: (key, value) => {
12
- sessionStorage.setItem(key, value);
13
- },
14
- getItem: (key) => {
15
- return sessionStorage.getItem(key);
16
- },
17
- removeItem: (key) => {
18
- sessionStorage.removeItem(key);
19
- },
20
- };
21
- }