@hubspot/app-connect-sdk 1.0.0-alpha.13 → 1.0.0-alpha.14

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 (31) hide show
  1. package/.turbo/turbo-format$colon$check.log +1 -1
  2. package/.turbo/turbo-test.log +60 -55
  3. package/.turbo/turbo-tsdown.log +12 -12
  4. package/build/tsconfig.browser.tsbuildinfo +1 -1
  5. package/build/tsconfig.server.tsbuildinfo +1 -1
  6. package/dist/browser/{create-crdncXsh.js → create-CWpWpZ9y.js} +194 -60
  7. package/dist/browser/create-CWpWpZ9y.js.map +1 -0
  8. package/dist/browser/index.d.ts +2 -2
  9. package/dist/browser/index.js +1 -1
  10. package/dist/browser/react/lovable.d.ts +8 -0
  11. package/dist/browser/react/lovable.js +6 -3
  12. package/dist/browser/react/lovable.js.map +1 -1
  13. package/dist/browser/react.d.ts +1 -1
  14. package/dist/browser/{types-rTQw6A54.d.ts → types-DkAmHcZt.d.ts} +22 -7
  15. package/dist/server/shared/constants.js.map +1 -1
  16. package/package.json +3 -3
  17. package/src/browser/app-connect-controller/README.md +4 -2
  18. package/src/browser/app-connect-controller/connect-start.test.ts +145 -0
  19. package/src/browser/app-connect-controller/connect-start.ts +18 -3
  20. package/src/browser/app-connect-controller/init.test.ts +44 -2
  21. package/src/browser/app-connect-controller/init.ts +9 -4
  22. package/src/browser/app-connect-controller/oauth-popup.test.ts +166 -0
  23. package/src/browser/app-connect-controller/oauth-popup.ts +132 -0
  24. package/src/browser/app-connect-controller/utils/is-app-embedded-in-iframe.ts +12 -0
  25. package/src/browser/app-connect-controller/utils/resolve-oauth-connect-mode.test.ts +35 -0
  26. package/src/browser/app-connect-controller/utils/resolve-oauth-connect-mode.ts +21 -0
  27. package/src/browser/index.ts +1 -0
  28. package/src/browser/react/lovable/LovableHubSpotAppConnect.tsx +12 -2
  29. package/src/browser/types.ts +21 -5
  30. package/src/shared/constants.ts +6 -0
  31. package/dist/browser/create-crdncXsh.js.map +0 -1
@@ -35,63 +35,6 @@ const noopLogger = {
35
35
  warn: () => {},
36
36
  error: () => {}
37
37
  };
38
- //#endregion
39
- //#region src/browser/app-connect-controller/utils/timeout-utils.ts
40
- function delay(ms) {
41
- if (ms <= 0) return Promise.resolve();
42
- return new Promise((resolve) => {
43
- setTimeout(resolve, ms);
44
- });
45
- }
46
- //#endregion
47
- //#region src/browser/app-connect-controller/connect-start.ts
48
- /** Extra wait before redirect so the connect progress UI is visible; set to `0` to disable. */
49
- const ARTIFICIAL_CONNECT_REDIRECT_DELAY_MS = 500;
50
- /**
51
- * Begins the OAuth connect flow:
52
- *
53
- * 1. Calls the SDK's `auth/init-session` route to mint a fresh PKCE
54
- * verifier + state and obtain HubSpot's `authorize` URL.
55
- * 2. Navigates the browser to that URL (full-page redirect).
56
- *
57
- * The `return_path` is the current path + query so the user lands
58
- * back where they started after authorizing.
59
- *
60
- * Throws when the init call fails. Does not return after the redirect
61
- * begins because the page is unloaded.
62
- */
63
- async function startHubSpotConnection(context) {
64
- const { config } = context;
65
- const returnPath = `${window.location.pathname}${window.location.search}`;
66
- const initUrl = new URL(`${config.hubSpotConnectBaseUrl}/auth/init-session`, window.location.origin);
67
- initUrl.searchParams.set("return_path", returnPath);
68
- const initResponse = await fetch(initUrl.toString(), { credentials: "include" });
69
- if (!initResponse.ok) throw new Error(`Failed to init session: ${initResponse.status}`);
70
- const { authorization_url: authorizationUrl } = await initResponse.json();
71
- await delay(ARTIFICIAL_CONNECT_REDIRECT_DELAY_MS);
72
- window.location.href = authorizationUrl;
73
- }
74
- //#endregion
75
- //#region src/browser/app-connect-controller/default-session-storage.ts
76
- /**
77
- * Builds a `SessionStorage` adapter that delegates to the global
78
- * `sessionStorage` object exposed by browsers. The controller uses
79
- * this when no custom storage is supplied (e.g. for tests or non-DOM
80
- * environments).
81
- */
82
- function createDefaultSessionStorage() {
83
- return {
84
- setItem: (key, value) => {
85
- sessionStorage.setItem(key, value);
86
- },
87
- getItem: (key) => {
88
- return sessionStorage.getItem(key);
89
- },
90
- removeItem: (key) => {
91
- sessionStorage.removeItem(key);
92
- }
93
- };
94
- }
95
38
  /**
96
39
  * Query parameter on the `auth/complete` POST request carrying the
97
40
  * authorization `code` HubSpot returned to the frontend callback.
@@ -102,6 +45,11 @@ const AUTH_COMPLETE_CODE_PARAM = "code";
102
45
  * OAuth `state` HubSpot echoed back to the frontend callback.
103
46
  */
104
47
  const AUTH_COMPLETE_STATE_PARAM = "state";
48
+ /**
49
+ * `postMessage` `data.type` value the OAuth popup sends to its opener
50
+ * after a successful `auth/complete` token exchange.
51
+ */
52
+ const OAUTH_POPUP_MESSAGE_TYPE = "hubspot-app-connect:oauth-complete";
105
53
  //#endregion
106
54
  //#region src/browser/app-connect-controller/constants.ts
107
55
  /**
@@ -163,6 +111,186 @@ function isClientSessionActive(context) {
163
111
  return expiresAt !== null && Date.now() < expiresAt;
164
112
  }
165
113
  //#endregion
114
+ //#region src/browser/app-connect-controller/oauth-popup.ts
115
+ const OAUTH_POPUP_WINDOW_NAME = "hubspot-app-connect-oauth";
116
+ const OAUTH_POPUP_POLL_INTERVAL_MS = 300;
117
+ function isOAuthPopupCompleteMessage(data) {
118
+ if (typeof data !== "object" || data === null) return false;
119
+ const record = data;
120
+ return record.type === "hubspot-app-connect:oauth-complete" && typeof record.expiresAt === "number" && Number.isFinite(record.expiresAt) && record.expiresAt > 0;
121
+ }
122
+ /**
123
+ * Opens HubSpot's authorize URL in a popup and waits for the OAuth
124
+ * callback page to post the session expiry back to this window.
125
+ */
126
+ async function waitForHubSpotOAuthPopup(options) {
127
+ const { context, authorizationUrl } = options;
128
+ const targetOrigin = window.location.origin;
129
+ return new Promise((resolve, reject) => {
130
+ let popup = null;
131
+ let pollTimer;
132
+ let isSettled = false;
133
+ const cleanup = () => {
134
+ window.removeEventListener("message", onMessage);
135
+ if (pollTimer !== void 0) clearInterval(pollTimer);
136
+ };
137
+ const settleSuccess = (expiresAtMs) => {
138
+ if (isSettled) return;
139
+ isSettled = true;
140
+ cleanup();
141
+ storeExpiresAt({
142
+ context,
143
+ expiresAtMs
144
+ });
145
+ context.store.setState({
146
+ isSessionConnected: true,
147
+ error: null
148
+ });
149
+ resolve();
150
+ };
151
+ const settleFailure = (message) => {
152
+ if (isSettled) return;
153
+ isSettled = true;
154
+ cleanup();
155
+ try {
156
+ popup?.close();
157
+ } catch {}
158
+ reject(new Error(message));
159
+ };
160
+ const onMessage = (event) => {
161
+ if (event.origin !== targetOrigin) return;
162
+ if (!isOAuthPopupCompleteMessage(event.data)) return;
163
+ settleSuccess(event.data.expiresAt);
164
+ };
165
+ window.addEventListener("message", onMessage);
166
+ popup = window.open(authorizationUrl, OAUTH_POPUP_WINDOW_NAME, "popup=yes,width=600,height=700");
167
+ if (!popup) {
168
+ settleFailure("Popup blocked. Allow popups for this site and try again.");
169
+ return;
170
+ }
171
+ pollTimer = setInterval(() => {
172
+ if (popup?.closed) settleFailure("Connect to HubSpot was cancelled.");
173
+ }, OAUTH_POPUP_POLL_INTERVAL_MS);
174
+ });
175
+ }
176
+ /**
177
+ * Called from the OAuth callback page running inside the popup after
178
+ * `auth/complete` succeeds. Notifies the opener and closes this window.
179
+ */
180
+ function notifyOAuthPopupOpener(options) {
181
+ const { expiresAtMs } = options;
182
+ const opener = window.opener;
183
+ if (!opener || opener.closed) return false;
184
+ const message = {
185
+ type: OAUTH_POPUP_MESSAGE_TYPE,
186
+ expiresAt: expiresAtMs
187
+ };
188
+ opener.postMessage(message, window.location.origin);
189
+ try {
190
+ window.close();
191
+ } catch {}
192
+ return true;
193
+ }
194
+ function hasOAuthPopupOpener() {
195
+ try {
196
+ return Boolean(window.opener && !window.opener.closed);
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+ //#endregion
202
+ //#region src/browser/app-connect-controller/utils/is-app-embedded-in-iframe.ts
203
+ /**
204
+ * Returns `true` when the app runs inside a parent frame (same-origin
205
+ * or cross-origin). Cross-origin parent access throws; treat that as
206
+ * embedded so OAuth uses a popup instead of a top-level redirect.
207
+ */
208
+ function isAppEmbeddedInIframe() {
209
+ try {
210
+ return window.self !== window.top;
211
+ } catch {
212
+ return true;
213
+ }
214
+ }
215
+ //#endregion
216
+ //#region src/browser/app-connect-controller/utils/resolve-oauth-connect-mode.ts
217
+ /**
218
+ * Maps the configured {@link OAuthConnectMode} to the concrete connect
219
+ * behavior for the current browsing context.
220
+ */
221
+ function resolveOAuthConnectMode(options) {
222
+ const mode = options.oauthConnectMode ?? "auto";
223
+ if (mode === "popup") return "popup";
224
+ if (mode === "redirect") return "redirect";
225
+ return isAppEmbeddedInIframe() ? "popup" : "redirect";
226
+ }
227
+ //#endregion
228
+ //#region src/browser/app-connect-controller/utils/timeout-utils.ts
229
+ function delay(ms) {
230
+ if (ms <= 0) return Promise.resolve();
231
+ return new Promise((resolve) => {
232
+ setTimeout(resolve, ms);
233
+ });
234
+ }
235
+ //#endregion
236
+ //#region src/browser/app-connect-controller/connect-start.ts
237
+ /** Extra wait before redirect so the connect progress UI is visible; set to `0` to disable. */
238
+ const ARTIFICIAL_CONNECT_REDIRECT_DELAY_MS = 500;
239
+ /**
240
+ * Begins the OAuth connect flow:
241
+ *
242
+ * 1. Calls the SDK's `auth/init-session` route to mint a fresh PKCE
243
+ * verifier + state and obtain HubSpot's `authorize` URL.
244
+ * 2. Navigates to that URL via full-page redirect, or opens it in a
245
+ * popup when embedded in an iframe or when `oauthConnectMode` is
246
+ * `'popup'`.
247
+ *
248
+ * The `return_path` is the current path + query so the user lands
249
+ * back where they started after authorizing (redirect mode only).
250
+ *
251
+ * Throws when the init call fails. Does not return after a redirect
252
+ * begins because the page is unloaded.
253
+ */
254
+ async function startHubSpotConnection(context) {
255
+ const { config } = context;
256
+ const returnPath = `${window.location.pathname}${window.location.search}`;
257
+ const initUrl = new URL(`${config.hubSpotConnectBaseUrl}/auth/init-session`, window.location.origin);
258
+ initUrl.searchParams.set("return_path", returnPath);
259
+ const initResponse = await fetch(initUrl.toString(), { credentials: "include" });
260
+ if (!initResponse.ok) throw new Error(`Failed to init session: ${initResponse.status}`);
261
+ const { authorization_url: authorizationUrl } = await initResponse.json();
262
+ await delay(ARTIFICIAL_CONNECT_REDIRECT_DELAY_MS);
263
+ if (resolveOAuthConnectMode(config.oauthConnectMode !== void 0 ? { oauthConnectMode: config.oauthConnectMode } : {}) === "popup") {
264
+ await waitForHubSpotOAuthPopup({
265
+ context,
266
+ authorizationUrl
267
+ });
268
+ return;
269
+ }
270
+ window.location.href = authorizationUrl;
271
+ }
272
+ //#endregion
273
+ //#region src/browser/app-connect-controller/default-session-storage.ts
274
+ /**
275
+ * Builds a `SessionStorage` adapter that delegates to the global
276
+ * `sessionStorage` object exposed by browsers. The controller uses
277
+ * this when no custom storage is supplied (e.g. for tests or non-DOM
278
+ * environments).
279
+ */
280
+ function createDefaultSessionStorage() {
281
+ return {
282
+ setItem: (key, value) => {
283
+ sessionStorage.setItem(key, value);
284
+ },
285
+ getItem: (key) => {
286
+ return sessionStorage.getItem(key);
287
+ },
288
+ removeItem: (key) => {
289
+ sessionStorage.removeItem(key);
290
+ }
291
+ };
292
+ }
293
+ //#endregion
166
294
  //#region src/browser/app-connect-controller/disconnect.ts
167
295
  /**
168
296
  * Disconnect flow:
@@ -259,8 +387,14 @@ async function consumeOAuthCallback(context) {
259
387
  throw new Error(`Failed to complete HubSpot OAuth: ${response.status} ${response.statusText}`);
260
388
  }
261
389
  const { expires_at: expiresAt, return_path: returnPath } = await response.json();
262
- context.store.setState({ expiresAt });
263
- context.sessionStorage.setItem(EXPIRES_AT_KEY, String(expiresAt));
390
+ if (hasOAuthPopupOpener()) {
391
+ notifyOAuthPopupOpener({ expiresAtMs: expiresAt });
392
+ return;
393
+ }
394
+ storeExpiresAt({
395
+ context,
396
+ expiresAtMs: expiresAt
397
+ });
264
398
  const targetUrl = new URL(returnPath, window.location.origin);
265
399
  history.replaceState(null, "", `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`);
266
400
  }
@@ -543,4 +677,4 @@ function createAppConnectController(options) {
543
677
  //#endregion
544
678
  export { createLogger as n, createAppConnectController as t };
545
679
 
546
- //# sourceMappingURL=create-crdncXsh.js.map
680
+ //# sourceMappingURL=create-CWpWpZ9y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-CWpWpZ9y.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-popup.ts","../../src/browser/app-connect-controller/utils/is-app-embedded-in-iframe.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 * after a successful `auth/complete` token exchange.\n */\nexport const OAUTH_POPUP_MESSAGE_TYPE = 'hubspot-app-connect:oauth-complete';\n","export { EXPIRES_AT_URL_PARAM } from '../../shared/constants.ts';\n\n/**\n * Key the controller persists the access-token `expiresAt` (Unix\n * epoch milliseconds) under in `sessionStorage`. Survives full-page\n * navigations within the same tab.\n */\nexport const EXPIRES_AT_KEY = 'hubspot_token_expires_at';\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 { EXPIRES_AT_KEY } from '../constants.ts';\nimport type { AppConnectContext } from '../types.ts';\n\ninterface StoreExpiresAtOptions {\n context: AppConnectContext;\n expiresAtMs: number;\n}\n\n/**\n * Persists the access-token `expiresAt` (Unix epoch milliseconds) to\n * both the in-memory store and `sessionStorage` so it survives\n * full-page navigations within the same tab.\n */\nexport function storeExpiresAt(options: StoreExpiresAtOptions): void {\n const { context, expiresAtMs } = options;\n context.store.setState({ expiresAt: expiresAtMs });\n context.sessionStorage.setItem(EXPIRES_AT_KEY, String(expiresAtMs));\n}\n\n/**\n * Reads the persisted `expiresAt` from `sessionStorage`. Removes the\n * value (and returns `null`) if it is malformed or already expired,\n * so a stale entry never reactivates a dead session.\n */\nexport function getExpiresAtFromSessionStorage(\n context: AppConnectContext\n): number | null {\n const raw = context.sessionStorage.getItem(EXPIRES_AT_KEY);\n if (!raw) return null;\n const val = parseInt(raw, 10);\n if (isNaN(val)) {\n context.sessionStorage.removeItem(EXPIRES_AT_KEY);\n return null;\n }\n if (Date.now() > val) {\n context.sessionStorage.removeItem(EXPIRES_AT_KEY);\n return null;\n }\n return val;\n}\n\n/**\n * Clears the persisted session-storage state. Called on disconnect.\n */\nexport function clearSessionStorage(context: AppConnectContext): void {\n context.sessionStorage.removeItem(EXPIRES_AT_KEY);\n}\n\n/**\n * Returns `true` when the controller has an `expiresAt` whose value\n * is still in the future. Used both to drive the UI status and to\n * decide whether the refresh scheduler should run.\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 { OAUTH_POPUP_MESSAGE_TYPE } from '../../shared/constants.ts';\nimport type { AppConnectContext } from './types.ts';\nimport { storeExpiresAt } from './utils/session-utils.ts';\n\nconst OAUTH_POPUP_WINDOW_NAME = 'hubspot-app-connect-oauth';\nconst OAUTH_POPUP_POLL_INTERVAL_MS = 300;\n\ninterface OAuthPopupCompleteMessage {\n type: typeof OAUTH_POPUP_MESSAGE_TYPE;\n expiresAt: number;\n}\n\ninterface WaitForHubSpotOAuthPopupOptions {\n context: AppConnectContext;\n authorizationUrl: string;\n}\n\nfunction isOAuthPopupCompleteMessage(\n data: unknown\n): data is OAuthPopupCompleteMessage {\n if (typeof data !== 'object' || data === null) return false;\n const record = data as Record<string, unknown>;\n return (\n record.type === OAUTH_POPUP_MESSAGE_TYPE &&\n typeof record.expiresAt === 'number' &&\n Number.isFinite(record.expiresAt) &&\n record.expiresAt > 0\n );\n}\n\n/**\n * Opens HubSpot's authorize URL in a popup and waits for the OAuth\n * callback page to post the session expiry back to this window.\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\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 (!isOAuthPopupCompleteMessage(event.data)) return;\n settleSuccess(event.data.expiresAt);\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) {\n settleFailure('Connect to HubSpot was cancelled.');\n }\n }, OAUTH_POPUP_POLL_INTERVAL_MS);\n });\n}\n\ninterface NotifyOAuthPopupOpenerOptions {\n expiresAtMs: number;\n}\n\n/**\n * Called from the OAuth callback page running inside the popup after\n * `auth/complete` succeeds. Notifies the opener and closes this window.\n */\nexport function notifyOAuthPopupOpener(\n options: NotifyOAuthPopupOpenerOptions\n): boolean {\n const { expiresAtMs } = options;\n const opener = window.opener;\n if (!opener || opener.closed) return false;\n\n const message: OAuthPopupCompleteMessage = {\n type: OAUTH_POPUP_MESSAGE_TYPE,\n expiresAt: expiresAtMs,\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","/**\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 './is-app-embedded-in-iframe.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 { LogoutResponse } from '../../shared/wire-types.ts';\nimport 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` and navigates the\n * browser to the URL the server returned in `redirect_to`.\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 const { redirect_to: redirectTo } =\n (await response.json()) as LogoutResponse;\n\n store.setState({\n expiresAt: null,\n isSessionConnected: false,\n isDisconnectInFlight: false,\n });\n\n window.location.href = redirectTo;\n logger.info('disconnectFromHubSpot: redirecting');\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 type { AuthCompleteResponse } from '../../shared/wire-types.ts';\nimport { hasOAuthPopupOpener, notifyOAuthPopupOpener } from './oauth-popup.ts';\nimport type { AppConnectContext } from './types.ts';\nimport { clearSessionStorage, storeExpiresAt } 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_FRONTEND_CALLBACK_PATH`) with\n * `?code` + `?state`, POST those values to the SDK's\n * `auth/complete` endpoint to finish the token exchange. The\n * server sets the durable session cookies on the response (in the\n * same `(frontend, edge)` CHIPS partition the SDK reads them from\n * on subsequent API fetches) and returns `{ expires_at,\n * return_path }`. Replace the URL with `${return_path}?\n * ${EXPIRES_AT_URL_PARAM}=${expires_at}` so the rest of init\n * runs against the page the user actually started the connect\n * flow from.\n * 2. Pick up `?__hs_expires_at` from `window.location` (placed there\n * by step 1, or by an in-progress refresh hop), persist it to the\n * controller's store + sessionStorage, and strip it from the\n * address bar so it isn't logged or bookmarked.\n *\n * A no-op when neither set of parameters is present (every page\n * load other than the OAuth return trip).\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 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 const body = (await response.json()) as AuthCompleteResponse;\n\n const { expires_at: expiresAt, return_path: returnPath } = body;\n\n if (hasOAuthPopupOpener()) {\n notifyOAuthPopupOpener({ expiresAtMs: expiresAt });\n return;\n }\n\n storeExpiresAt({ context, expiresAtMs: expiresAt });\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 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 getExpiresAtFromSessionStorage,\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 });\n const context: AppConnectContext = {\n config,\n logger,\n sessionStorage,\n store,\n };\n\n store.setState({ expiresAt: getExpiresAtFromSessionStorage(context) });\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 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,4 +1,4 @@
1
- import { i as AppConnectStatus, n as AppConnectController, r as AppConnectState, t as AppConnectBrowserConfig } from "./types-rTQw6A54.js";
1
+ import { a as OAuthConnectMode, i as AppConnectStatus, n as AppConnectController, r as AppConnectState, t as AppConnectBrowserConfig } from "./types-DkAmHcZt.js";
2
2
 
3
3
  //#region src/shared/logger.d.ts
4
4
  /**
@@ -461,5 +461,5 @@ declare const themeClass: string, themeVars: {
461
461
  };
462
462
  };
463
463
  //#endregion
464
- export { type AppConnectBrowserConfig, type AppConnectController, type AppConnectState, type AppConnectStatus, type CreateAppConnectControllerOptions, type Logger, createAppConnectController, createLogger, themeClass, themeVars };
464
+ export { type AppConnectBrowserConfig, type AppConnectController, type AppConnectState, type AppConnectStatus, type CreateAppConnectControllerOptions, type Logger, type OAuthConnectMode, createAppConnectController, createLogger, themeClass, themeVars };
465
465
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,3 @@
1
- import { n as createLogger, t as createAppConnectController } from "./create-crdncXsh.js";
1
+ import { n as createLogger, t as createAppConnectController } from "./create-CWpWpZ9y.js";
2
2
  import { n as themeVars, t as themeClass } from "./theme.css-CJbxi5hC.js";
3
3
  export { createAppConnectController, createLogger, themeClass, themeVars };
@@ -1,3 +1,4 @@
1
+ import { a as OAuthConnectMode } from "../types-DkAmHcZt.js";
1
2
  import { ReactNode } from "react";
2
3
 
3
4
  //#region src/browser/react/lovable/LovableHubSpotAppConnect.d.ts
@@ -14,6 +15,12 @@ interface LovableHubSpotAppConnectProps {
14
15
  * @example `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/hubspot-connect`
15
16
  */
16
17
  hubSpotConnectBaseUrl: string;
18
+ /**
19
+ * How connect navigates to HubSpot OAuth. Defaults to `'auto'` (popup
20
+ * when embedded in an iframe). Set to `'popup'` to test the popup flow
21
+ * without embedding.
22
+ */
23
+ oauthConnectMode?: OAuthConnectMode;
17
24
  /** Title text rendered in the standard SDK header. */
18
25
  title: string;
19
26
  /** Content rendered when the controller is in the `connected` state. */
@@ -34,6 +41,7 @@ interface LovableHubSpotAppConnectProps {
34
41
  */
35
42
  declare function LovableHubSpotAppConnect({
36
43
  hubSpotConnectBaseUrl,
44
+ oauthConnectMode,
37
45
  title,
38
46
  connected,
39
47
  disconnectedMessage
@@ -1,4 +1,4 @@
1
- import { t as createAppConnectController } from "../create-crdncXsh.js";
1
+ import { t as createAppConnectController } from "../create-CWpWpZ9y.js";
2
2
  import { t as HubSpotAppConnect } from "../HubSpotAppConnect-DFe9b90e.js";
3
3
  import { useRef } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
@@ -11,9 +11,12 @@ import { jsx } from "react/jsx-runtime";
11
11
  * canonical `HubSpotAppConnect` from `@hubspot/app-connect-sdk/react`
12
12
  * instead.
13
13
  */
14
- function LovableHubSpotAppConnect({ hubSpotConnectBaseUrl, title, connected, disconnectedMessage }) {
14
+ function LovableHubSpotAppConnect({ hubSpotConnectBaseUrl, oauthConnectMode, title, connected, disconnectedMessage }) {
15
15
  const controllerRef = useRef(null);
16
- if (controllerRef.current === null) controllerRef.current = createAppConnectController({ config: { hubSpotConnectBaseUrl } });
16
+ if (controllerRef.current === null) controllerRef.current = createAppConnectController({ config: {
17
+ hubSpotConnectBaseUrl,
18
+ ...oauthConnectMode !== void 0 ? { oauthConnectMode } : {}
19
+ } });
17
20
  return /* @__PURE__ */ jsx(HubSpotAppConnect, {
18
21
  title,
19
22
  connected,
@@ -1 +1 @@
1
- {"version":3,"file":"lovable.js","names":[],"sources":["../../../src/browser/react/lovable/LovableHubSpotAppConnect.tsx"],"sourcesContent":["import { useRef, type ReactNode } from 'react';\n\nimport { createAppConnectController } from '../../app-connect-controller/create.ts';\nimport type { AppConnectController } from '../../types.ts';\nimport { HubSpotAppConnect } from '../components/HubSpotAppConnect/HubSpotAppConnect.tsx';\n\n/**\n * Props accepted by {@link LovableHubSpotAppConnect}.\n */\nexport interface LovableHubSpotAppConnectProps {\n /**\n * URL prefix the Lovable backend mounts the SDK's `hubspot-connect`\n * routes on (no trailing slash). In a Vite + Supabase Lovable app,\n * read the Supabase origin from `import.meta.env.VITE_SUPABASE_URL`\n * and append `/functions/v1/hubspot-connect`.\n *\n * @example `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/hubspot-connect`\n */\n hubSpotConnectBaseUrl: string;\n /** Title text rendered in the standard SDK header. */\n title: string;\n /** Content rendered when the controller is in the `connected` state. */\n connected: ReactNode;\n /**\n * Description text rendered inside the SDK-owned disconnected card,\n * above the primary \"Connect to HubSpot\" button.\n */\n disconnectedMessage: ReactNode;\n}\n\n/**\n * Lovable-preset variant of `HubSpotAppConnect`. Takes\n * `hubSpotConnectBaseUrl` as a prop and lazily creates a single\n * controller per component instance. Callers that need a custom\n * logger or full control over controller construction should use the\n * canonical `HubSpotAppConnect` from `@hubspot/app-connect-sdk/react`\n * instead.\n */\nexport function LovableHubSpotAppConnect({\n hubSpotConnectBaseUrl,\n title,\n connected,\n disconnectedMessage,\n}: LovableHubSpotAppConnectProps) {\n // useRef null-check pattern: create the controller exactly once per\n // component instance and never recreate it when props change. The\n // first hubSpotConnectBaseUrl wins; later changes are ignored, which\n // matches the \"one controller per app\" semantics the canonical\n // HubSpotAppConnect also enforces.\n const controllerRef = useRef<AppConnectController | null>(null);\n if (controllerRef.current === null) {\n controllerRef.current = createAppConnectController({\n config: { hubSpotConnectBaseUrl },\n });\n }\n return (\n <HubSpotAppConnect\n title={title}\n connected={connected}\n disconnectedMessage={disconnectedMessage}\n controller={controllerRef.current}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAsCA,SAAgB,yBAAyB,EACvC,uBACA,OACA,WACA,uBACgC;CAMhC,MAAM,gBAAgB,OAAoC,IAAI;CAC9D,IAAI,cAAc,YAAY,MAC5B,cAAc,UAAU,2BAA2B,EACjD,QAAQ,EAAE,sBAAsB,EAClC,CAAC;CAEH,OACE,oBAAC,mBAAD;EACS;EACI;EACU;EACrB,YAAY,cAAc;CAC3B,CAAA;AAEL"}
1
+ {"version":3,"file":"lovable.js","names":[],"sources":["../../../src/browser/react/lovable/LovableHubSpotAppConnect.tsx"],"sourcesContent":["import { useRef, type ReactNode } from 'react';\n\nimport { createAppConnectController } from '../../app-connect-controller/create.ts';\nimport type { AppConnectController, OAuthConnectMode } from '../../types.ts';\nimport { HubSpotAppConnect } from '../components/HubSpotAppConnect/HubSpotAppConnect.tsx';\n\n/**\n * Props accepted by {@link LovableHubSpotAppConnect}.\n */\nexport interface LovableHubSpotAppConnectProps {\n /**\n * URL prefix the Lovable backend mounts the SDK's `hubspot-connect`\n * routes on (no trailing slash). In a Vite + Supabase Lovable app,\n * read the Supabase origin from `import.meta.env.VITE_SUPABASE_URL`\n * and append `/functions/v1/hubspot-connect`.\n *\n * @example `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/hubspot-connect`\n */\n hubSpotConnectBaseUrl: string;\n /**\n * How connect navigates to HubSpot OAuth. Defaults to `'auto'` (popup\n * when embedded in an iframe). Set to `'popup'` to test the popup flow\n * without embedding.\n */\n oauthConnectMode?: OAuthConnectMode;\n /** Title text rendered in the standard SDK header. */\n title: string;\n /** Content rendered when the controller is in the `connected` state. */\n connected: ReactNode;\n /**\n * Description text rendered inside the SDK-owned disconnected card,\n * above the primary \"Connect to HubSpot\" button.\n */\n disconnectedMessage: ReactNode;\n}\n\n/**\n * Lovable-preset variant of `HubSpotAppConnect`. Takes\n * `hubSpotConnectBaseUrl` as a prop and lazily creates a single\n * controller per component instance. Callers that need a custom\n * logger or full control over controller construction should use the\n * canonical `HubSpotAppConnect` from `@hubspot/app-connect-sdk/react`\n * instead.\n */\nexport function LovableHubSpotAppConnect({\n hubSpotConnectBaseUrl,\n oauthConnectMode,\n title,\n connected,\n disconnectedMessage,\n}: LovableHubSpotAppConnectProps) {\n // useRef null-check pattern: create the controller exactly once per\n // component instance and never recreate it when props change. The\n // first hubSpotConnectBaseUrl wins; later changes are ignored, which\n // matches the \"one controller per app\" semantics the canonical\n // HubSpotAppConnect also enforces.\n const controllerRef = useRef<AppConnectController | null>(null);\n if (controllerRef.current === null) {\n controllerRef.current = createAppConnectController({\n config: {\n hubSpotConnectBaseUrl,\n ...(oauthConnectMode !== undefined ? { oauthConnectMode } : {}),\n },\n });\n }\n return (\n <HubSpotAppConnect\n title={title}\n connected={connected}\n disconnectedMessage={disconnectedMessage}\n controller={controllerRef.current}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA4CA,SAAgB,yBAAyB,EACvC,uBACA,kBACA,OACA,WACA,uBACgC;CAMhC,MAAM,gBAAgB,OAAoC,IAAI;CAC9D,IAAI,cAAc,YAAY,MAC5B,cAAc,UAAU,2BAA2B,EACjD,QAAQ;EACN;EACA,GAAI,qBAAqB,KAAA,IAAY,EAAE,iBAAiB,IAAI,CAAC;CAC/D,EACF,CAAC;CAEH,OACE,oBAAC,mBAAD;EACS;EACI;EACU;EACrB,YAAY,cAAc;CAC3B,CAAA;AAEL"}
@@ -1,4 +1,4 @@
1
- import { n as AppConnectController, r as AppConnectState } from "./types-rTQw6A54.js";
1
+ import { n as AppConnectController, r as AppConnectState } from "./types-DkAmHcZt.js";
2
2
  import { ReactNode } from "react";
3
3
 
4
4
  //#region src/browser/react/components/HubSpotAppConnect/HubSpotAppConnect.d.ts
@@ -1,4 +1,9 @@
1
1
  //#region src/browser/types.d.ts
2
+ /**
3
+ * How the browser navigates to HubSpot's OAuth authorize endpoint when
4
+ * the user calls `connectToHubSpot`.
5
+ */
6
+ type OAuthConnectMode = 'auto' | 'redirect' | 'popup';
2
7
  /**
3
8
  * Runtime configuration for an `AppConnectController`. The browser
4
9
  * controller uses these URLs to talk to the SDK's server-side OAuth
@@ -12,6 +17,15 @@ interface AppConnectBrowserConfig {
12
17
  * @example '/functions/v1/hubspot-connect'
13
18
  */
14
19
  hubSpotConnectBaseUrl: string;
20
+ /**
21
+ * Controls whether connect uses a full-page redirect or a popup.
22
+ *
23
+ * - `'auto'` (default): popup when the app is embedded in an iframe,
24
+ * full-page redirect otherwise.
25
+ * - `'redirect'`: always full-page redirect.
26
+ * - `'popup'`: always open a popup (useful for local iframe testing).
27
+ */
28
+ oauthConnectMode?: OAuthConnectMode;
15
29
  }
16
30
  /**
17
31
  * Public lifecycle of an `AppConnectController`. Every state in the
@@ -19,8 +33,7 @@ interface AppConnectBrowserConfig {
19
33
  *
20
34
  * - `initializing`: the controller hasn't finished `start()` yet.
21
35
  * - `disconnected`: no active session, ready to call `connectToHubSpot`.
22
- * - `connecting`: a connect flow is in flight (the browser is about
23
- * to redirect to HubSpot).
36
+ * - `connecting`: a connect flow is in flight (redirect or OAuth popup).
24
37
  * - `connected`: a refresh-token cookie is present and the access
25
38
  * token is being refreshed in the background.
26
39
  * - `disconnecting`: a `disconnectFromHubSpot()` call is in flight.
@@ -36,9 +49,11 @@ interface AppConnectState {
36
49
  /** Last error message that occurred, or `null` if no error. */
37
50
  error: string | null;
38
51
  /**
39
- * Begins the OAuth connect flow. On success the browser redirects
40
- * to HubSpot's authorize endpoint; the promise resolves before the
41
- * redirect.
52
+ * Begins the OAuth connect flow. On success the browser either
53
+ * redirects to HubSpot's authorize endpoint or opens it in a popup
54
+ * (when embedded in an iframe or when `oauthConnectMode` is
55
+ * `'popup'`). The promise resolves when the popup flow completes, or
56
+ * just before a full-page redirect begins.
42
57
  */
43
58
  connectToHubSpot: () => Promise<void>;
44
59
  /**
@@ -75,5 +90,5 @@ interface AppConnectController {
75
90
  start: () => void;
76
91
  }
77
92
  //#endregion
78
- export { AppConnectStatus as i, AppConnectController as n, AppConnectState as r, AppConnectBrowserConfig as t };
79
- //# sourceMappingURL=types-rTQw6A54.d.ts.map
93
+ export { OAuthConnectMode as a, AppConnectStatus as i, AppConnectController as n, AppConnectState as r, AppConnectBrowserConfig as t };
94
+ //# sourceMappingURL=types-DkAmHcZt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","names":[],"sources":["../../../src/shared/constants.ts"],"sourcesContent":["/**\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"],"mappings":";;;;;;;;;;;;;;;AA4BA,MAAa,sBAAsB;;;;;AAMnC,MAAa,2BAA2B;;;;;AAMxC,MAAa,4BAA4B"}
1
+ {"version":3,"file":"constants.js","names":[],"sources":["../../../src/shared/constants.ts"],"sourcesContent":["/**\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 * after a successful `auth/complete` token exchange.\n */\nexport const OAUTH_POPUP_MESSAGE_TYPE = 'hubspot-app-connect:oauth-complete';\n"],"mappings":";;;;;;;;;;;;;;;AA4BA,MAAa,sBAAsB;;;;;AAMnC,MAAa,2BAA2B;;;;;AAMxC,MAAa,4BAA4B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/app-connect-sdk",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.14",
4
4
  "description": "HubSpot App Connect SDK (alpha release). Documentation and integration guidance forthcoming.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,9 +44,9 @@
44
44
  "tsdown": "0.22.0",
45
45
  "typescript": "6.0.3",
46
46
  "vitest": "4.1.6",
47
+ "@private/prettier-config": "0.1.0",
47
48
  "@private/eslint-config": "0.1.0",
48
- "@private/tsconfig": "0.1.0",
49
- "@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",
@@ -57,8 +57,10 @@ flowchart TD
57
57
  ## Module map
58
58
 
59
59
  - [create.ts](./create.ts) — factory; the only file [`../index.ts`](../index.ts) imports from. Wires the context, defines `connectToHubSpot` / `disconnectFromHubSpot`, and applies `memoizeLast` to `getSnapshot`.
60
- - [init.ts](./init.ts) — runs once on `start()`. Reads `?__hs_expires_at=…` from `window.location`, persists it via [`utils/session-utils.ts`](./utils/session-utils.ts), and scrubs the parameter from the address bar with `history.replaceState`.
61
- - [connect-start.ts](./connect-start.ts) — `GET`s the SDK's `/auth/init-session` route, then full-page redirects to HubSpot's `authorize` URL. The `return_path` is the current path + query so the user lands back where they started.
60
+ - [init.ts](./init.ts) — runs once on `start()`. On the frontend OAuth callback path, POSTs `code` + `state` to `/auth/complete`. Redirect flow: persists `expires_at` and `history.replaceState`s to `return_path`. Popup flow (`window.opener`): `postMessage`s the opener and closes.
61
+ - [connect-start.ts](./connect-start.ts) — `GET`s `/auth/init-session`, then redirects or opens a popup per `config.oauthConnectMode` (`auto` uses a popup when embedded in an iframe). See [oauth-popup.ts](./oauth-popup.ts).
62
+ - [oauth-popup.ts](./oauth-popup.ts) — opener waits for popup `postMessage`; popup callback notifies opener after `auth/complete`.
63
+ - [utils/resolve-oauth-connect-mode.ts](./utils/resolve-oauth-connect-mode.ts) / [utils/is-app-embedded-in-iframe.ts](./utils/is-app-embedded-in-iframe.ts) — map `oauthConnectMode` + iframe detection to redirect vs popup.
62
64
  - [disconnect.ts](./disconnect.ts) — `POST`s `/auth/logout`, clears local session storage, and redirects to the server-supplied `redirect_to`. Errors are caught and surfaced via `state.error`.
63
65
  - [refresh.ts](./refresh.ts) — subscribes to the store and (re)schedules a `/auth/refresh` call whenever `expiresAt` changes. Exposes `RefreshSchedulerHandle.stop()` for teardown.
64
66
  - [view-state.ts](./view-state.ts) — `getDerivedStatus` and `SERVER_VIEW` (the SSR snapshot returned by `getServerSnapshot`).