@hubspot/app-connect-sdk 1.0.0-alpha.2 → 1.0.0-alpha.3
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.
- package/.turbo/turbo-format$colon$check.log +1 -1
- package/.turbo/turbo-test.log +60 -58
- package/.turbo/turbo-tsdown.log +55 -52
- package/build/tsconfig.browser.tsbuildinfo +1 -1
- package/build/tsconfig.server.tsbuildinfo +1 -1
- package/dist/browser/{HubSpotAppConnect-BW45gyDs.js → HubSpotAppConnect-COQgPrFn.js} +5 -3
- package/dist/browser/HubSpotAppConnect-COQgPrFn.js.map +1 -0
- package/dist/browser/{create-vctOhpX9.js → create-crdncXsh.js} +53 -24
- package/dist/browser/create-crdncXsh.js.map +1 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/react/lovable.js +2 -2
- package/dist/browser/react.js +1 -1
- package/dist/server/api-client-core/plugins/fetch-transport.js +5 -1
- package/dist/server/api-client-core/plugins/fetch-transport.js.map +1 -1
- package/dist/server/constants.js +33 -6
- package/dist/server/constants.js.map +1 -1
- package/dist/server/hono/hono-request-handler.js +18 -13
- package/dist/server/hono/hono-request-handler.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-complete.js +154 -0
- package/dist/server/hono/hubspot-connect-routes/auth-complete.js.map +1 -0
- package/dist/server/hono/hubspot-connect-routes/auth-init-session.js +22 -11
- package/dist/server/hono/hubspot-connect-routes/auth-init-session.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-logout.js +18 -1
- package/dist/server/hono/hubspot-connect-routes/auth-logout.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-refresh.js +6 -0
- package/dist/server/hono/hubspot-connect-routes/auth-refresh.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/hubspot-connect-routes.js +4 -2
- package/dist/server/hono/hubspot-connect-routes/hubspot-connect-routes.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/utils.js +50 -3
- package/dist/server/hono/hubspot-connect-routes/utils.js.map +1 -1
- package/dist/server/hono/types.d.ts +13 -9
- package/dist/server/hono/utils/cookie-utils.js +2 -1
- package/dist/server/hono/utils/cookie-utils.js.map +1 -1
- package/dist/server/hono/utils/cors-middleware.js +85 -0
- package/dist/server/hono/utils/cors-middleware.js.map +1 -0
- package/dist/server/sanitize-request.js +24 -10
- package/dist/server/sanitize-request.js.map +1 -1
- package/dist/server/shared/constants.js +22 -9
- package/dist/server/shared/constants.js.map +1 -1
- package/package.json +3 -3
- package/src/browser/app-connect-controller/init.test.ts +167 -0
- package/src/browser/app-connect-controller/init.ts +70 -19
- package/src/browser/react/components/AppConnectHeader/AppConnectHeader.tsx +3 -5
- package/src/browser/react/components/ConnectButton/ConnectButton.tsx +2 -1
- package/src/server/api-client-core/plugins/fetch-transport.ts +5 -1
- package/src/server/constants.ts +29 -4
- package/src/server/hono/hono-request-handler.ts +42 -15
- package/src/server/hono/hubspot-connect-routes/auth-complete.test.ts +285 -0
- package/src/server/hono/hubspot-connect-routes/{auth-callback.ts → auth-complete.ts} +73 -30
- package/src/server/hono/hubspot-connect-routes/auth-init-session.test.ts +114 -30
- package/src/server/hono/hubspot-connect-routes/auth-init-session.ts +33 -10
- package/src/server/hono/hubspot-connect-routes/auth-logout.test.ts +13 -0
- package/src/server/hono/hubspot-connect-routes/auth-logout.ts +18 -0
- package/src/server/hono/hubspot-connect-routes/auth-refresh.test.ts +6 -0
- package/src/server/hono/hubspot-connect-routes/auth-refresh.ts +6 -0
- package/src/server/hono/hubspot-connect-routes/hubspot-connect-routes.ts +9 -2
- package/src/server/hono/hubspot-connect-routes/utils.ts +57 -1
- package/src/server/hono/types.ts +15 -9
- package/src/server/hono/utils/cookie-utils.ts +27 -2
- package/src/server/hono/utils/cors-middleware.test.ts +79 -0
- package/src/server/hono/utils/cors-middleware.ts +95 -0
- package/src/server/sanitize-request.ts +25 -11
- package/src/server/types.ts +2 -2
- package/src/shared/constants.ts +31 -3
- package/src/shared/wire-types.ts +19 -0
- package/dist/browser/HubSpotAppConnect-BW45gyDs.js.map +0 -1
- package/dist/browser/create-vctOhpX9.js.map +0 -1
- package/dist/server/hono/hubspot-connect-routes/auth-callback.js +0 -125
- package/dist/server/hono/hubspot-connect-routes/auth-callback.js.map +0 -1
- package/src/server/hono/hubspot-connect-routes/auth-callback.test.ts +0 -225
|
@@ -75,7 +75,8 @@ var spinner = "ConnectButton_spinner__db1gqc6";
|
|
|
75
75
|
//#region src/browser/react/components/ConnectButton/ConnectButton.tsx
|
|
76
76
|
function ConnectButton({ variant = "primary", size = "md", className }) {
|
|
77
77
|
const { status, connectToHubSpot } = useHubSpotAppConnect();
|
|
78
|
-
|
|
78
|
+
console.log("status", status);
|
|
79
|
+
const isConnecting = status === "connecting" || status === "initializing";
|
|
79
80
|
return /* @__PURE__ */ jsxs(Button, {
|
|
80
81
|
variant,
|
|
81
82
|
size,
|
|
@@ -302,6 +303,7 @@ function getFullName(user) {
|
|
|
302
303
|
}
|
|
303
304
|
function AppConnectHeader({ title }) {
|
|
304
305
|
const { status } = useHubSpotAppConnect();
|
|
306
|
+
const connectButton = status === "initializing" ? null : /* @__PURE__ */ jsx(ConnectButton, { variant: "secondary" });
|
|
305
307
|
return /* @__PURE__ */ jsxs("header", {
|
|
306
308
|
className: styles$3.header,
|
|
307
309
|
children: [/* @__PURE__ */ jsx("div", {
|
|
@@ -316,7 +318,7 @@ function AppConnectHeader({ title }) {
|
|
|
316
318
|
}), /* @__PURE__ */ jsx(ShareButton, { pageTitle: title })]
|
|
317
319
|
}), status === "connected" ? /* @__PURE__ */ jsx(ViewingHubSpotContextRow, {}) : null]
|
|
318
320
|
})
|
|
319
|
-
}), status === "connected" ? /* @__PURE__ */ jsx(UserMenu, {}) :
|
|
321
|
+
}), status === "connected" ? /* @__PURE__ */ jsx(UserMenu, {}) : connectButton]
|
|
320
322
|
});
|
|
321
323
|
}
|
|
322
324
|
function ViewingHubSpotContextRow() {
|
|
@@ -486,4 +488,4 @@ function HubSpotAppConnectContent({ connected, disconnectedMessage }) {
|
|
|
486
488
|
//#endregion
|
|
487
489
|
export { useHubSpotAppConnect as n, HubSpotAppConnect as t };
|
|
488
490
|
|
|
489
|
-
//# sourceMappingURL=HubSpotAppConnect-
|
|
491
|
+
//# sourceMappingURL=HubSpotAppConnect-COQgPrFn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HubSpotAppConnect-COQgPrFn.js","names":["styles.root","styles.variant","variant","styles.size","size","styles","styles","styles","styles"],"sources":["../../src/browser/react/context.ts","../../src/browser/react/hooks.ts","../../src/browser/react/components/Button/Button.css.ts","../../src/browser/react/components/Button/Button.tsx","../../src/browser/react/components/ConnectButton/ConnectButton.css.ts","../../src/browser/react/components/ConnectButton/ConnectButton.tsx","../../src/browser/react/components/icons/ChevronDownIcon.tsx","../../src/browser/react/components/icons/ExternalLinkIcon.tsx","../../src/browser/react/components/icons/HubSpotDataSourceIcon.tsx","../../src/browser/react/components/icons/LogoutIcon.tsx","../../src/browser/react/components/icons/ShareIcon.tsx","../../src/browser/react/components/ShareButton/ShareButton.css.ts","../../src/browser/react/components/ShareButton/ShareButton.tsx","../../src/browser/react/components/AppConnectHeader/AppConnectHeader.css.ts","../../src/browser/react/components/AppConnectHeader/AppConnectHeader.tsx","../../src/browser/react/components/DisconnectedBody/DisconnectedBody.css.ts","../../src/browser/react/components/DisconnectedBody/DisconnectedBody.tsx","../../src/browser/react/components/LoadingIndicator/LoadingIndicator.css.ts","../../src/browser/react/components/LoadingIndicator/LoadingIndicator.tsx","../../src/browser/react/components/HubSpotAppConnect/HubSpotAppConnect.css.ts","../../src/browser/react/components/HubSpotAppConnect/HubSpotAppConnect.tsx"],"sourcesContent":["import { createContext } from 'react';\n\nimport type { AppConnectController } from '../types.ts';\n\n/**\n * React context that carries the `AppConnectController` from the\n * `HubSpotAppConnect` provider down to consumers of\n * `useHubSpotAppConnect`. `null` indicates the hook is being used\n * outside a provider.\n */\nexport const HubSpotAppConnectControllerContext =\n createContext<AppConnectController | null>(null);\n","import { useContext, useSyncExternalStore } from 'react';\n\nimport type { AppConnectState } from '../types.ts';\nimport { HubSpotAppConnectControllerContext } from './context.ts';\n\nexport type UseHubSpotAppConnectResult = AppConnectState;\n\n/**\n * React hook that returns the current `AppConnectState`. Must be\n * called inside a {@link HubSpotAppConnect} provider — throws when\n * no controller is available.\n *\n * The hook subscribes to the controller via `useSyncExternalStore`\n * so React 18+ batched updates and SSR work correctly.\n */\nexport function useHubSpotAppConnect(): UseHubSpotAppConnectResult {\n const controller = useContext(HubSpotAppConnectControllerContext);\n if (controller == null) {\n throw new Error(\n 'useHubSpotAppConnect must be used within HubSpotAppConnect'\n );\n }\n return useSyncExternalStore(\n controller.subscribe,\n controller.getSnapshot,\n controller.getServerSnapshot\n );\n}\n","import { style, styleVariants } from '@vanilla-extract/css';\n\nimport { themeVars } from '../../../theme.css.ts';\n\nexport const root = style({\n appearance: 'none',\n WebkitAppearance: 'none',\n margin: 0,\n font: 'inherit',\n fontWeight: 500,\n lineHeight: 'inherit',\n textAlign: 'center',\n cursor: 'pointer',\n borderWidth: 1,\n borderStyle: 'solid',\n borderRadius: themeVars.borderRadius[100],\n transition:\n 'background-color 120ms ease, border-color 120ms ease, color 120ms ease, opacity 120ms ease',\n selectors: {\n '&:disabled': {\n cursor: 'wait',\n opacity: 0.55,\n },\n },\n});\n\nexport const variant = styleVariants({\n primary: {\n backgroundColor: themeVars.fill.primary.default,\n color: themeVars.text.primary.default,\n borderColor: themeVars.border.primary.default,\n selectors: {\n '&:hover:not(:disabled)': {\n filter: 'brightness(0.95)',\n },\n },\n },\n secondary: {\n backgroundColor: themeVars.fill.surface.default.default,\n color: themeVars.text.core.default,\n borderColor: themeVars.border.core.default,\n selectors: {\n '&:hover:not(:disabled)': {\n backgroundColor: '#f7f7f7',\n },\n },\n },\n});\n\nexport const size = styleVariants({\n md: {\n padding: `${themeVars.space[200]} ${themeVars.space[300]}`,\n fontSize: 14,\n },\n lg: {\n padding: `${themeVars.space[300]} ${themeVars.space[500]}`,\n fontSize: 16,\n },\n});\n","import type { ButtonHTMLAttributes, ReactNode } from 'react';\n\nimport * as styles from './Button.css.ts';\n\nexport type ButtonVariant = 'primary' | 'secondary';\nexport type ButtonSize = 'md' | 'lg';\n\nexport interface ButtonProps extends Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n 'children'\n> {\n children: ReactNode;\n variant?: ButtonVariant;\n size?: ButtonSize;\n}\n\nexport function Button({\n children,\n className,\n variant = 'primary',\n size = 'md',\n type = 'button',\n ...rest\n}: ButtonProps) {\n const composedClassName = [\n styles.root,\n styles.variant[variant],\n styles.size[size],\n className,\n ]\n .filter(Boolean)\n .join(' ');\n return (\n <button {...rest} type={type} className={composedClassName}>\n {children}\n </button>\n );\n}\n","import { keyframes, style, styleVariants } from '@vanilla-extract/css';\n\nimport { themeVars } from '../../../theme.css.ts';\n\nconst spin = keyframes({\n from: { transform: 'rotate(0deg)' },\n to: { transform: 'rotate(360deg)' },\n});\n\nexport const root = style({\n position: 'relative',\n});\n\nexport const label = style({\n position: 'relative',\n zIndex: 0,\n});\n\nexport const labelMuted = style({\n opacity: 0.22,\n});\n\nexport const loadingBackdrop = styleVariants({\n primary: {\n position: 'absolute',\n left: '50%',\n top: '50%',\n zIndex: 1,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: 36,\n height: 36,\n marginLeft: -18,\n marginTop: -18,\n borderRadius: '50%',\n backgroundColor: 'rgba(255, 255, 255, 0.88)',\n boxShadow: '0 1px 6px rgba(0, 0, 0, 0.14)',\n pointerEvents: 'none',\n },\n secondary: {\n position: 'absolute',\n left: '50%',\n top: '50%',\n zIndex: 1,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: 36,\n height: 36,\n marginLeft: -18,\n marginTop: -18,\n borderRadius: '50%',\n backgroundColor: 'rgba(255, 255, 255, 0.82)',\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: themeVars.border.core.subtle.default,\n boxShadow: '0 1px 4px rgba(0, 0, 0, 0.08)',\n pointerEvents: 'none',\n },\n});\n\nexport const spinner = style({\n display: 'block',\n width: 14,\n height: 14,\n borderWidth: 2,\n borderStyle: 'solid',\n borderColor: themeVars.border.core.subtle.default,\n borderTopColor: themeVars.fill.primary.default,\n borderRadius: '50%',\n animation: `${spin} 0.8s linear infinite`,\n});\n","import { useHubSpotAppConnect } from '../../hooks.ts';\nimport type { ButtonProps } from '../Button/Button.tsx';\nimport { Button } from '../Button/Button.tsx';\nimport {\n label,\n labelMuted,\n loadingBackdrop,\n root,\n spinner,\n} from './ConnectButton.css.ts';\n\nexport interface ConnectButtonProps extends Pick<\n ButtonProps,\n 'variant' | 'size' | 'className'\n> {}\n\nexport function ConnectButton({\n variant = 'primary',\n size = 'md',\n className,\n}: ConnectButtonProps) {\n const { status, connectToHubSpot } = useHubSpotAppConnect();\n console.log('status', status);\n const isConnecting = status === 'connecting' || status === 'initializing';\n const composedClassName = [root, className].filter(Boolean).join(' ');\n const labelClassName = isConnecting ? `${label} ${labelMuted}` : label;\n return (\n <Button\n variant={variant}\n size={size}\n className={composedClassName}\n aria-busy={isConnecting}\n onClick={() => void connectToHubSpot()}\n disabled={isConnecting}\n >\n <span className={labelClassName}>Connect to HubSpot</span>\n {isConnecting ? (\n <span className={loadingBackdrop[variant]} aria-hidden=\"true\">\n <span className={spinner} />\n </span>\n ) : null}\n </Button>\n );\n}\n","export interface ChevronDownIconProps {\n className?: string;\n}\n\nexport function ChevronDownIcon({ className }: ChevronDownIconProps) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 6l4 4 4-4\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n","export interface ExternalLinkIconProps {\n className?: string;\n}\n\nexport function ExternalLinkIcon({ className }: ExternalLinkIconProps) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M6.5 3.5H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2.5M10 2.5h3.5V6M9 7l5-5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.25\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n","export interface HubSpotDataSourceIconProps {\n className?: string;\n}\n\nexport function HubSpotDataSourceIcon({\n className,\n}: HubSpotDataSourceIconProps) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <rect\n x=\"2.5\"\n y=\"2.5\"\n width=\"11\"\n height=\"11\"\n rx=\"1.5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.25\"\n />\n <path\n d=\"M5 6h6M5 8.25h6M5 10.5h4\"\n stroke=\"currentColor\"\n strokeWidth=\"1.25\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n","export interface LogoutIconProps {\n className?: string;\n}\n\nexport function LogoutIcon({ className }: LogoutIconProps) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M6.5 2.5h-3a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h3\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M10.5 11l3-3-3-3\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M13.5 8h-7\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n","export interface ShareIconProps {\n className?: string;\n}\n\nexport function ShareIcon({ className }: ShareIconProps) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <circle cx=\"18\" cy=\"5\" r=\"2.75\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n <circle cx=\"6\" cy=\"12\" r=\"2.75\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n <circle\n cx=\"18\"\n cy=\"19\"\n r=\"2.75\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n />\n <path\n d=\"M8.6 10.5L15.4 6.5M8.6 13.5L15.4 17.5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n","import { style } from '@vanilla-extract/css';\n\nimport { themeVars } from '../../../theme.css.ts';\n\nconst shareBorder = '#cbd6e2';\nconst shareText = '#33475b';\n\nexport const styles = {\n shareButton: style({\n flexShrink: 0,\n appearance: 'none',\n WebkitAppearance: 'none',\n margin: 0,\n fontFamily: 'inherit',\n fontSize: 14,\n fontWeight: 500,\n lineHeight: 1,\n cursor: 'pointer',\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n gap: themeVars.space[200],\n padding: `8px ${themeVars.space[300]}`,\n color: shareText,\n backgroundColor: themeVars.fill.surface.default.default,\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: shareBorder,\n borderRadius: 100,\n transition:\n 'background-color 120ms ease, border-color 120ms ease, color 120ms ease',\n selectors: {\n '&:hover:not(:disabled)': {\n backgroundColor: '#f5f8fa',\n },\n '&:focus-visible': {\n outline: `2px solid ${themeVars.border.tertiary.default}`,\n outlineOffset: 2,\n },\n '&:disabled': {\n cursor: 'not-allowed',\n opacity: 0.55,\n },\n },\n }),\n shareIcon: style({\n width: 16,\n height: 16,\n flexShrink: 0,\n display: 'block',\n }),\n} as const;\n","import { ShareIcon } from '../icons/ShareIcon.tsx';\nimport { styles } from './ShareButton.css.ts';\n\ninterface ShareAppConnectPageOptions {\n pageTitle: string;\n}\n\nasync function shareAppConnectPage(\n options: ShareAppConnectPageOptions\n): Promise<void> {\n const { pageTitle } = options;\n const url = window.location.href;\n if (typeof navigator.share === 'function') {\n try {\n await navigator.share({ title: pageTitle, text: pageTitle, url });\n return;\n } catch (error) {\n if (error instanceof DOMException && error.name === 'AbortError') {\n return;\n }\n }\n }\n if (typeof navigator.clipboard?.writeText === 'function') {\n await navigator.clipboard.writeText(url);\n }\n}\n\nexport interface ShareButtonProps {\n pageTitle: string;\n}\n\nexport function ShareButton({ pageTitle }: ShareButtonProps) {\n return (\n <button\n type=\"button\"\n className={styles.shareButton}\n onClick={() => void shareAppConnectPage({ pageTitle })}\n >\n <ShareIcon className={styles.shareIcon} />\n <span>Share</span>\n </button>\n );\n}\n","import { style } from '@vanilla-extract/css';\n\nimport { themeVars } from '../../../theme.css.ts';\n\nconst avatarOrange = themeVars.fill.brand.default;\nconst avatarTextColor = themeVars.text.primary.default;\n\nexport const styles = {\n header: style({\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: themeVars.space[300],\n paddingBottom: themeVars.space[300],\n borderBottomWidth: 1,\n borderBottomStyle: 'solid',\n borderBottomColor: themeVars.border.core.subtle.default,\n }),\n titleRow: style({\n display: 'flex',\n alignItems: 'center',\n flex: 1,\n minWidth: 0,\n }),\n leftStack: style({\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-start',\n gap: themeVars.space[200],\n minWidth: 0,\n flex: 1,\n }),\n contextRow: style({\n display: 'flex',\n alignItems: 'center',\n gap: 6,\n fontSize: 14,\n lineHeight: 1.35,\n color: themeVars.text.core.subtle,\n minWidth: 0,\n }),\n contextIcon: style({\n width: 16,\n height: 16,\n flexShrink: 0,\n color: themeVars.text.core.subtle,\n }),\n contextPrefix: style({\n flexShrink: 0,\n }),\n contextLink: style({\n display: 'inline-flex',\n alignItems: 'center',\n gap: 4,\n color: themeVars.fill.brand.default,\n textDecoration: 'none',\n fontWeight: 500,\n selectors: {\n '&:hover': {\n textDecoration: 'underline',\n },\n },\n }),\n contextExternalIcon: style({\n width: 14,\n height: 14,\n flexShrink: 0,\n }),\n titleCluster: style({\n display: 'flex',\n alignItems: 'center',\n gap: themeVars.space[200],\n width: 'max-content',\n maxWidth: '100%',\n minWidth: 0,\n }),\n title: style({\n flex: '0 1 auto',\n fontSize: 20,\n fontWeight: 600,\n margin: 0,\n color: themeVars.text.core.default,\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n }),\n userTrigger: style({\n appearance: 'none',\n WebkitAppearance: 'none',\n margin: 0,\n font: 'inherit',\n lineHeight: 1,\n cursor: 'pointer',\n display: 'inline-flex',\n alignItems: 'center',\n gap: themeVars.space[200],\n padding: `6px ${themeVars.space[300]} 6px 6px`,\n backgroundColor: themeVars.fill.surface.default.default,\n color: themeVars.text.core.default,\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: themeVars.border.core.default,\n borderRadius: 999,\n transition: 'background-color 120ms ease, border-color 120ms ease',\n selectors: {\n '&:hover': {\n backgroundColor: '#f7f7f7',\n },\n '&[data-popup-open]': {\n backgroundColor: '#f7f7f7',\n },\n },\n }),\n triggerName: style({\n fontWeight: 500,\n fontSize: 14,\n }),\n chevron: style({\n width: 14,\n height: 14,\n color: themeVars.text.core.subtle,\n transition: 'transform 150ms ease',\n selectors: {\n '[data-popup-open] &': {\n transform: 'rotate(180deg)',\n },\n },\n }),\n avatar: style({\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: avatarOrange,\n color: avatarTextColor,\n fontWeight: 600,\n flexShrink: 0,\n borderRadius: '50%',\n userSelect: 'none',\n }),\n avatarSm: style({\n width: 28,\n height: 28,\n fontSize: 11,\n }),\n avatarLg: style({\n width: 40,\n height: 40,\n fontSize: 14,\n }),\n popup: style({\n minWidth: 240,\n backgroundColor: themeVars.fill.surface.default.default,\n color: themeVars.text.core.default,\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: themeVars.border.core.subtle.default,\n borderRadius: themeVars.borderRadius[300],\n boxShadow:\n '0 6px 16px rgba(17, 17, 17, 0.08), 0 2px 6px rgba(17, 17, 17, 0.04)',\n padding: `${themeVars.space[200]} 0`,\n outline: 'none',\n transformOrigin: 'top right',\n opacity: 0,\n transform: 'scale(0.96)',\n transition: 'opacity 120ms ease, transform 120ms ease',\n selectors: {\n '&[data-starting-style]': {\n opacity: 0,\n transform: 'scale(0.96)',\n },\n '&[data-open]': {\n opacity: 1,\n transform: 'scale(1)',\n },\n },\n }),\n userInfo: style({\n display: 'flex',\n alignItems: 'center',\n gap: themeVars.space[300],\n padding: `${themeVars.space[200]} ${themeVars.space[300]}`,\n }),\n userInfoText: style({\n display: 'flex',\n flexDirection: 'column',\n minWidth: 0,\n }),\n userInfoName: style({\n fontWeight: 600,\n fontSize: 14,\n color: themeVars.text.core.default,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n }),\n userInfoEmail: style({\n fontSize: 13,\n color: themeVars.text.core.subtle,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n }),\n separator: style({\n height: 1,\n margin: `${themeVars.space[200]} 0`,\n backgroundColor: themeVars.border.core.subtle.default,\n border: 'none',\n }),\n disconnectItem: style({\n display: 'flex',\n alignItems: 'center',\n gap: themeVars.space[200],\n padding: `${themeVars.space[200]} ${themeVars.space[300]}`,\n color: themeVars.text.alert.default,\n fontSize: 14,\n fontWeight: 500,\n cursor: 'pointer',\n outline: 'none',\n userSelect: 'none',\n selectors: {\n '&[data-highlighted]': {\n backgroundColor: themeVars.fill.accent.red.subtle.default,\n },\n },\n }),\n disconnectIcon: style({\n width: 16,\n height: 16,\n }),\n} as const;\n","import { Menu } from '@base-ui/react/menu';\n\nimport { useHubSpotAppConnect } from '../../hooks.ts';\nimport { ConnectButton } from '../ConnectButton/ConnectButton.tsx';\nimport { ChevronDownIcon } from '../icons/ChevronDownIcon.tsx';\nimport { ExternalLinkIcon } from '../icons/ExternalLinkIcon.tsx';\nimport { HubSpotDataSourceIcon } from '../icons/HubSpotDataSourceIcon.tsx';\nimport { LogoutIcon } from '../icons/LogoutIcon.tsx';\nimport { ShareButton } from '../ShareButton/ShareButton.tsx';\nimport { styles } from './AppConnectHeader.css.ts';\n\ninterface FakeUser {\n firstName: string;\n lastName: string;\n email: string;\n}\n\nconst FAKE_USER: FakeUser = {\n firstName: 'Gabby',\n lastName: 'Martinez',\n email: 'gabby.martinez@acmecorp.com',\n};\n\nfunction getUserInitials(user: FakeUser): string {\n const first = user.firstName.charAt(0).toUpperCase();\n const last = user.lastName.charAt(0).toUpperCase();\n return `${first}${last}`;\n}\n\nfunction getFullName(user: FakeUser): string {\n return `${user.firstName} ${user.lastName}`;\n}\n\ninterface AppConnectHeaderProps {\n title: string;\n}\n\nexport function AppConnectHeader({ title }: AppConnectHeaderProps) {\n const { status } = useHubSpotAppConnect();\n const connectButton =\n status === 'initializing' ? null : <ConnectButton variant=\"secondary\" />;\n return (\n <header className={styles.header}>\n <div className={styles.titleRow}>\n <div className={styles.leftStack}>\n <div className={styles.titleCluster}>\n <h1 className={styles.title}>{title}</h1>\n <ShareButton pageTitle={title} />\n </div>\n {status === 'connected' ? <ViewingHubSpotContextRow /> : null}\n </div>\n </div>\n {status === 'connected' ? <UserMenu /> : connectButton}\n </header>\n );\n}\n\nfunction ViewingHubSpotContextRow() {\n return (\n <div className={styles.contextRow}>\n <HubSpotDataSourceIcon className={styles.contextIcon} />\n <span className={styles.contextPrefix}>Viewing HubSpot data from </span>\n <a\n className={styles.contextLink}\n href=\"#\"\n onClick={(event) => {\n event.preventDefault();\n }}\n >\n Acme Corp · HubSpot\n <ExternalLinkIcon className={styles.contextExternalIcon} />\n </a>\n </div>\n );\n}\n\nfunction UserMenu() {\n const { disconnectFromHubSpot } = useHubSpotAppConnect();\n const initials = getUserInitials(FAKE_USER);\n const fullName = getFullName(FAKE_USER);\n\n return (\n <Menu.Root modal={false}>\n <Menu.Trigger className={styles.userTrigger}>\n <span\n className={`${styles.avatar} ${styles.avatarSm}`}\n aria-hidden=\"true\"\n >\n {initials}\n </span>\n <span className={styles.triggerName}>{fullName}</span>\n <ChevronDownIcon className={styles.chevron} />\n </Menu.Trigger>\n <Menu.Portal>\n <Menu.Positioner sideOffset={8} align=\"end\">\n <Menu.Popup className={styles.popup}>\n <div className={styles.userInfo}>\n <span\n className={`${styles.avatar} ${styles.avatarLg}`}\n aria-hidden=\"true\"\n >\n {initials}\n </span>\n <div className={styles.userInfoText}>\n <span className={styles.userInfoName}>{fullName}</span>\n <span className={styles.userInfoEmail}>{FAKE_USER.email}</span>\n </div>\n </div>\n <Menu.Separator className={styles.separator} />\n <Menu.Item\n className={styles.disconnectItem}\n onClick={() => void disconnectFromHubSpot()}\n >\n <LogoutIcon className={styles.disconnectIcon} />\n Disconnect\n </Menu.Item>\n </Menu.Popup>\n </Menu.Positioner>\n </Menu.Portal>\n </Menu.Root>\n );\n}\n","import { style } from '@vanilla-extract/css';\n\nimport { themeVars } from '../../../theme.css.ts';\n\nexport const styles = {\n card: style({\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: themeVars.border.core.default,\n borderRadius: themeVars.borderRadius[400],\n padding: themeVars.space[500],\n textAlign: 'center',\n backgroundColor: themeVars.fill.surface.default.default,\n color: themeVars.text.core.default,\n }),\n message: style({\n marginTop: 0,\n marginBottom: themeVars.space[400],\n }),\n errorText: style({\n marginTop: themeVars.space[300],\n marginBottom: 0,\n color: themeVars.text.alert.default,\n }),\n} as const;\n","import type { ReactNode } from 'react';\n\nimport { useHubSpotAppConnect } from '../../hooks.ts';\nimport { ConnectButton } from '../ConnectButton/ConnectButton.tsx';\nimport { styles } from './DisconnectedBody.css.ts';\n\ninterface DisconnectedBodyProps {\n message: ReactNode;\n}\n\nexport function DisconnectedBody({ message }: DisconnectedBodyProps) {\n const { error } = useHubSpotAppConnect();\n\n return (\n <div className={styles.card}>\n <p className={styles.message}>{message}</p>\n <ConnectButton size=\"lg\" />\n {error && (\n <p className={styles.errorText}>\n Failed to connect to HubSpot. {error}\n </p>\n )}\n </div>\n );\n}\n","import { keyframes, style } from '@vanilla-extract/css';\n\nimport { themeVars } from '../../../theme.css.ts';\n\nconst spin = keyframes({\n from: { transform: 'rotate(0deg)' },\n to: { transform: 'rotate(360deg)' },\n});\n\nexport const styles = {\n wrapper: style({\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n minHeight: 200,\n padding: themeVars.space[600],\n }),\n spinner: style({\n width: 40,\n height: 40,\n borderWidth: 3,\n borderStyle: 'solid',\n borderColor: themeVars.border.core.subtle.default,\n borderTopColor: themeVars.fill.primary.default,\n borderRadius: '50%',\n animation: `${spin} 0.8s linear infinite`,\n }),\n} as const;\n","import { styles } from './LoadingIndicator.css.ts';\n\nexport function LoadingIndicator() {\n return (\n <div className={styles.wrapper} role=\"status\" aria-label=\"Loading\">\n <div className={styles.spinner} />\n </div>\n );\n}\n","import { style } from '@vanilla-extract/css';\n\nimport { themeVars } from '../../../theme.css.ts';\n\nexport const styles = {\n shell: style({\n width: '100%',\n padding: `${themeVars.space[400]} ${themeVars.space[500]}`,\n }),\n content: style({\n marginTop: themeVars.space[500],\n }),\n connectedErrorBanner: style({\n backgroundColor: themeVars.fill.alert.subtle,\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: themeVars.border.alert.default,\n borderRadius: themeVars.borderRadius[300],\n padding: themeVars.space[300],\n marginBottom: themeVars.space[300],\n color: themeVars.text.alert.default,\n }),\n} as const;\n","import { useEffect, type ReactNode } from 'react';\n\nimport { themeClass } from '../../../theme.css.ts';\nimport type { AppConnectController } from '../../../types.ts';\nimport { HubSpotAppConnectControllerContext } from '../../context.ts';\nimport { useHubSpotAppConnect } from '../../hooks.ts';\nimport { AppConnectHeader } from '../AppConnectHeader/AppConnectHeader.tsx';\nimport { DisconnectedBody } from '../DisconnectedBody/DisconnectedBody.tsx';\nimport { LoadingIndicator } from '../LoadingIndicator/LoadingIndicator.tsx';\nimport { styles } from './HubSpotAppConnect.css.ts';\n\n/**\n * Props accepted by {@link HubSpotAppConnect}.\n */\nexport interface HubSpotAppConnectProps {\n /** Title text rendered in the standard SDK header. */\n title: string;\n /** Controller produced by `createAppConnectController`. */\n controller: AppConnectController;\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 * Layout component that exposes `controller` to {@link useHubSpotAppConnect},\n * starts it once on mount, and renders a standard header plus the content\n * slot that matches the current connection status.\n */\nexport function HubSpotAppConnect({\n title,\n controller,\n connected,\n disconnectedMessage,\n}: HubSpotAppConnectProps) {\n useEffect(() => {\n controller.start();\n }, [controller]);\n useEffect(() => {\n document.documentElement.classList.add(themeClass);\n return () => {\n document.documentElement.classList.remove(themeClass);\n };\n }, []);\n return (\n <HubSpotAppConnectControllerContext.Provider value={controller}>\n <div className={styles.shell}>\n <AppConnectHeader title={title} />\n <div className={styles.content}>\n <HubSpotAppConnectContent\n connected={connected}\n disconnectedMessage={disconnectedMessage}\n />\n </div>\n </div>\n </HubSpotAppConnectControllerContext.Provider>\n );\n}\n\ninterface HubSpotAppConnectContentProps {\n connected: ReactNode;\n disconnectedMessage: ReactNode;\n}\n\nfunction HubSpotAppConnectContent({\n connected,\n disconnectedMessage,\n}: HubSpotAppConnectContentProps) {\n const { status, error } = useHubSpotAppConnect();\n if (status === 'initializing') {\n return <LoadingIndicator />;\n }\n if (status === 'connected') {\n return (\n <>\n {error ? (\n <div className={styles.connectedErrorBanner} role=\"alert\">\n {error}\n </div>\n ) : null}\n {connected}\n </>\n );\n }\n return <DisconnectedBody message={disconnectedMessage} />;\n}\n"],"mappings":"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"}
|
|
@@ -92,20 +92,16 @@ function createDefaultSessionStorage() {
|
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
//#endregion
|
|
96
|
-
//#region src/shared/constants.ts
|
|
97
95
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* import from this module so the wire format stays in sync.
|
|
96
|
+
* Query parameter on the `auth/complete` POST request carrying the
|
|
97
|
+
* authorization `code` HubSpot returned to the frontend callback.
|
|
101
98
|
*/
|
|
99
|
+
const AUTH_COMPLETE_CODE_PARAM = "code";
|
|
102
100
|
/**
|
|
103
|
-
* Query parameter on the
|
|
104
|
-
*
|
|
105
|
-
* the `auth/callback` redirect; the browser parses it during `initSdk`
|
|
106
|
-
* and then strips it from the URL via `history.replaceState`.
|
|
101
|
+
* Query parameter on the `auth/complete` POST request carrying the
|
|
102
|
+
* OAuth `state` HubSpot echoed back to the frontend callback.
|
|
107
103
|
*/
|
|
108
|
-
const
|
|
104
|
+
const AUTH_COMPLETE_STATE_PARAM = "state";
|
|
109
105
|
//#endregion
|
|
110
106
|
//#region src/browser/app-connect-controller/constants.ts
|
|
111
107
|
/**
|
|
@@ -215,25 +211,58 @@ async function disconnectFromHubSpot(context) {
|
|
|
215
211
|
//#endregion
|
|
216
212
|
//#region src/browser/app-connect-controller/init.ts
|
|
217
213
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* the
|
|
214
|
+
* On `controller.start()`:
|
|
215
|
+
*
|
|
216
|
+
* 1. If the browser has been redirected back to the SDK's frontend
|
|
217
|
+
* OAuth callback path (`HUBSPOT_FRONTEND_CALLBACK_PATH`) with
|
|
218
|
+
* `?code` + `?state`, POST those values to the SDK's
|
|
219
|
+
* `auth/complete` endpoint to finish the token exchange. The
|
|
220
|
+
* server sets the durable session cookies on the response (in the
|
|
221
|
+
* same `(frontend, edge)` CHIPS partition the SDK reads them from
|
|
222
|
+
* on subsequent API fetches) and returns `{ expires_at,
|
|
223
|
+
* return_path }`. Replace the URL with `${return_path}?
|
|
224
|
+
* ${EXPIRES_AT_URL_PARAM}=${expires_at}` so the rest of init
|
|
225
|
+
* runs against the page the user actually started the connect
|
|
226
|
+
* flow from.
|
|
227
|
+
* 2. Pick up `?__hs_expires_at` from `window.location` (placed there
|
|
228
|
+
* by step 1, or by an in-progress refresh hop), persist it to the
|
|
229
|
+
* controller's store + sessionStorage, and strip it from the
|
|
230
|
+
* address bar so it isn't logged or bookmarked.
|
|
221
231
|
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* trip).
|
|
232
|
+
* A no-op when neither set of parameters is present (every page
|
|
233
|
+
* load other than the OAuth return trip).
|
|
225
234
|
*/
|
|
226
235
|
async function initAppConnect(context) {
|
|
236
|
+
await consumeOAuthCallback(context);
|
|
237
|
+
}
|
|
238
|
+
async function consumeOAuthCallback(context) {
|
|
239
|
+
if (window.location.pathname !== "/__hubspot_oauth_callback") return;
|
|
227
240
|
const params = new URLSearchParams(window.location.search);
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
241
|
+
const code = params.get(AUTH_COMPLETE_CODE_PARAM);
|
|
242
|
+
const state = params.get(AUTH_COMPLETE_STATE_PARAM);
|
|
243
|
+
if (!code || !state) return;
|
|
244
|
+
const completeUrl = new URL(`${context.config.hubSpotConnectBaseUrl}/auth/complete`, window.location.origin);
|
|
245
|
+
completeUrl.searchParams.set(AUTH_COMPLETE_CODE_PARAM, code);
|
|
246
|
+
completeUrl.searchParams.set(AUTH_COMPLETE_STATE_PARAM, state);
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
response = await fetch(completeUrl.toString(), {
|
|
250
|
+
method: "POST",
|
|
251
|
+
credentials: "include"
|
|
252
|
+
});
|
|
253
|
+
} catch (err) {
|
|
254
|
+
clearSessionStorage(context);
|
|
255
|
+
throw new Error(`Failed to complete HubSpot OAuth: ${err instanceof Error ? err.message : String(err)}`);
|
|
256
|
+
}
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
clearSessionStorage(context);
|
|
259
|
+
throw new Error(`Failed to complete HubSpot OAuth: ${response.status} ${response.statusText}`);
|
|
260
|
+
}
|
|
261
|
+
const { expires_at: expiresAt, return_path: returnPath } = await response.json();
|
|
235
262
|
context.store.setState({ expiresAt });
|
|
236
263
|
context.sessionStorage.setItem(EXPIRES_AT_KEY, String(expiresAt));
|
|
264
|
+
const targetUrl = new URL(returnPath, window.location.origin);
|
|
265
|
+
history.replaceState(null, "", `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`);
|
|
237
266
|
}
|
|
238
267
|
//#endregion
|
|
239
268
|
//#region src/browser/app-connect-controller/refresh.ts
|
|
@@ -514,4 +543,4 @@ function createAppConnectController(options) {
|
|
|
514
543
|
//#endregion
|
|
515
544
|
export { createLogger as n, createAppConnectController as t };
|
|
516
545
|
|
|
517
|
-
//# sourceMappingURL=create-
|
|
546
|
+
//# sourceMappingURL=create-crdncXsh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-crdncXsh.js","names":["disconnectFromHubSpot","runDisconnectFromHubSpot"],"sources":["../../src/shared/logger.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/shared/constants.ts","../../src/browser/app-connect-controller/constants.ts","../../src/browser/app-connect-controller/utils/session-utils.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","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 type { AppConnectContext } from './types.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 the browser to that URL (full-page redirect).\n *\n * The `return_path` is the current path + query so the user lands\n * back where they started after authorizing.\n *\n * Throws when the init call fails. Does not return after the 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 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","/**\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 HUBSPOT_FRONTEND_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","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 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 HUBSPOT_FRONTEND_CALLBACK_PATH,\n} from '../../shared/constants.ts';\nimport type { AuthCompleteResponse } from '../../shared/wire-types.ts';\nimport { EXPIRES_AT_KEY } from './constants.ts';\nimport type { AppConnectContext } from './types.ts';\nimport { clearSessionStorage } 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 !== HUBSPOT_FRONTEND_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 context.store.setState({ expiresAt });\n context.sessionStorage.setItem(EXPIRES_AT_KEY, String(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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"}
|
package/dist/browser/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as createLogger, t as createAppConnectController } from "./create-
|
|
1
|
+
import { n as createLogger, t as createAppConnectController } from "./create-crdncXsh.js";
|
|
2
2
|
import { n as themeVars, t as themeClass } from "./theme.css-CJbxi5hC.js";
|
|
3
3
|
export { createAppConnectController, createLogger, themeClass, themeVars };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as createAppConnectController } from "../create-
|
|
2
|
-
import { t as HubSpotAppConnect } from "../HubSpotAppConnect-
|
|
1
|
+
import { t as createAppConnectController } from "../create-crdncXsh.js";
|
|
2
|
+
import { t as HubSpotAppConnect } from "../HubSpotAppConnect-COQgPrFn.js";
|
|
3
3
|
import { useRef } from "react";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/browser/react/lovable/LovableHubSpotAppConnect.tsx
|
package/dist/browser/react.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as useHubSpotAppConnect, t as HubSpotAppConnect } from "./HubSpotAppConnect-
|
|
1
|
+
import { n as useHubSpotAppConnect, t as HubSpotAppConnect } from "./HubSpotAppConnect-COQgPrFn.js";
|
|
2
2
|
export { HubSpotAppConnect, useHubSpotAppConnect };
|
|
@@ -57,10 +57,14 @@ function fetchTransportPlugin(options) {
|
|
|
57
57
|
if (operation.body) init.body = JSON.stringify(operation.body);
|
|
58
58
|
}
|
|
59
59
|
const response = await fetch(url, init);
|
|
60
|
+
const responseHeaders = Object.create(null);
|
|
61
|
+
response.headers.forEach((value, key) => {
|
|
62
|
+
responseHeaders[key] = value;
|
|
63
|
+
});
|
|
60
64
|
return {
|
|
61
65
|
status: response.status,
|
|
62
66
|
statusText: response.statusText,
|
|
63
|
-
headers:
|
|
67
|
+
headers: responseHeaders,
|
|
64
68
|
bodyJson: response.status === 204 ? void 0 : await response.json()
|
|
65
69
|
};
|
|
66
70
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-transport.js","names":[],"sources":["../../../../src/server/api-client-core/plugins/fetch-transport.ts"],"sourcesContent":["import { isBinaryData } from '../binary-data.ts';\nimport type { Plugin } from '../types.ts';\n\nconst BASE_URL = 'https://api.hubapi.com';\n\nexport interface FetchTransportPluginOptions {\n getAccessToken: () => string;\n}\n\nfunction appendRecordToUrlSearchParams(\n params: URLSearchParams,\n record: Record<string, unknown>\n): void {\n for (const [key, value] of Object.entries(record)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n params.append(key, String(item as string));\n }\n } else {\n params.append(key, String(value));\n }\n }\n}\n\n/**\n * Transport plugin that executes HTTP requests using the Fetch API.\n *\n * This is the terminal middleware in the chain — it builds the actual HTTP\n * request from the operation descriptor and returns a normalized response.\n * It handles path-parameter interpolation, query-string serialization,\n * Bearer-token auth, and content-type negotiation (JSON, form-urlencoded, multipart).\n */\nexport function fetchTransportPlugin(\n options: FetchTransportPluginOptions\n): Plugin {\n return {\n activate(api) {\n api.addMiddleware(async (ctx) => {\n const { operation } = ctx;\n\n // Interpolate path parameters (e.g. \"/contacts/{contactId}\" → \"/contacts/123\")\n let url = `${BASE_URL}${operation.path}`;\n if (operation.pathParams) {\n for (const [key, value] of Object.entries(operation.pathParams)) {\n url = url.replace(`{${key}}`, encodeURIComponent(String(value)));\n }\n }\n\n // Serialize query parameters, supporting repeated keys for array values\n if (operation.queryParams) {\n const params = new URLSearchParams();\n appendRecordToUrlSearchParams(params, operation.queryParams);\n const qs = params.toString();\n if (qs) url += `?${qs}`;\n }\n\n const headers: Record<string, string> = {\n ...(operation.headers ?? {}),\n Authorization: `Bearer ${options.getAccessToken()}`,\n };\n\n const init: RequestInit = {\n method: operation.method.toUpperCase(),\n headers,\n };\n\n // For file uploads the API expects multipart/form-data; BinaryData\n // values are appended as Blobs while everything else is stringified.\n // Form bodies use application/x-www-form-urlencoded. Otherwise JSON.\n if (operation.contentType === 'multipart/form-data' && operation.body) {\n const formData = new FormData();\n for (const [key, value] of Object.entries(\n operation.body as Record<string, unknown>\n )) {\n if (value == null) continue;\n if (isBinaryData(value)) {\n formData.append(key, value.source as Blob);\n } else {\n formData.append(key, String(value));\n }\n }\n init.body = formData;\n } else if (\n operation.contentType === 'application/x-www-form-urlencoded'\n ) {\n headers['Content-Type'] = 'application/x-www-form-urlencoded';\n if (operation.body) {\n const params = new URLSearchParams();\n appendRecordToUrlSearchParams(\n params,\n operation.body as Record<string, unknown>\n );\n const encoded = params.toString();\n if (encoded) init.body = encoded;\n }\n } else {\n headers['Content-Type'] = 'application/json';\n if (operation.body) {\n init.body = JSON.stringify(operation.body);\n }\n }\n const response = await fetch(url, init);\n\n return {\n status: response.status,\n statusText: response.statusText,\n headers:
|
|
1
|
+
{"version":3,"file":"fetch-transport.js","names":[],"sources":["../../../../src/server/api-client-core/plugins/fetch-transport.ts"],"sourcesContent":["import { isBinaryData } from '../binary-data.ts';\nimport type { Plugin } from '../types.ts';\n\nconst BASE_URL = 'https://api.hubapi.com';\n\nexport interface FetchTransportPluginOptions {\n getAccessToken: () => string;\n}\n\nfunction appendRecordToUrlSearchParams(\n params: URLSearchParams,\n record: Record<string, unknown>\n): void {\n for (const [key, value] of Object.entries(record)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n params.append(key, String(item as string));\n }\n } else {\n params.append(key, String(value));\n }\n }\n}\n\n/**\n * Transport plugin that executes HTTP requests using the Fetch API.\n *\n * This is the terminal middleware in the chain — it builds the actual HTTP\n * request from the operation descriptor and returns a normalized response.\n * It handles path-parameter interpolation, query-string serialization,\n * Bearer-token auth, and content-type negotiation (JSON, form-urlencoded, multipart).\n */\nexport function fetchTransportPlugin(\n options: FetchTransportPluginOptions\n): Plugin {\n return {\n activate(api) {\n api.addMiddleware(async (ctx) => {\n const { operation } = ctx;\n\n // Interpolate path parameters (e.g. \"/contacts/{contactId}\" → \"/contacts/123\")\n let url = `${BASE_URL}${operation.path}`;\n if (operation.pathParams) {\n for (const [key, value] of Object.entries(operation.pathParams)) {\n url = url.replace(`{${key}}`, encodeURIComponent(String(value)));\n }\n }\n\n // Serialize query parameters, supporting repeated keys for array values\n if (operation.queryParams) {\n const params = new URLSearchParams();\n appendRecordToUrlSearchParams(params, operation.queryParams);\n const qs = params.toString();\n if (qs) url += `?${qs}`;\n }\n\n const headers: Record<string, string> = {\n ...(operation.headers ?? {}),\n Authorization: `Bearer ${options.getAccessToken()}`,\n };\n\n const init: RequestInit = {\n method: operation.method.toUpperCase(),\n headers,\n };\n\n // For file uploads the API expects multipart/form-data; BinaryData\n // values are appended as Blobs while everything else is stringified.\n // Form bodies use application/x-www-form-urlencoded. Otherwise JSON.\n if (operation.contentType === 'multipart/form-data' && operation.body) {\n const formData = new FormData();\n for (const [key, value] of Object.entries(\n operation.body as Record<string, unknown>\n )) {\n if (value == null) continue;\n if (isBinaryData(value)) {\n formData.append(key, value.source as Blob);\n } else {\n formData.append(key, String(value));\n }\n }\n init.body = formData;\n } else if (\n operation.contentType === 'application/x-www-form-urlencoded'\n ) {\n headers['Content-Type'] = 'application/x-www-form-urlencoded';\n if (operation.body) {\n const params = new URLSearchParams();\n appendRecordToUrlSearchParams(\n params,\n operation.body as Record<string, unknown>\n );\n const encoded = params.toString();\n if (encoded) init.body = encoded;\n }\n } else {\n headers['Content-Type'] = 'application/json';\n if (operation.body) {\n init.body = JSON.stringify(operation.body);\n }\n }\n const response = await fetch(url, init);\n const responseHeaders: Record<string, string> = Object.create(null);\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n return {\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n // 204 No Content responses have no body to parse\n bodyJson: response.status === 204 ? undefined : await response.json(),\n };\n });\n },\n };\n}\n"],"mappings":";;AAGA,MAAM,WAAW;AAMjB,SAAS,8BACP,QACA,QACM;CACN,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,EAAE;EACjD,IAAI,SAAS,MAAM;EACnB,IAAI,MAAM,QAAQ,MAAM,EACtB,KAAK,MAAM,QAAQ,OACjB,OAAO,OAAO,KAAK,OAAO,KAAe,CAAC;OAG5C,OAAO,OAAO,KAAK,OAAO,MAAM,CAAC;;;;;;;;;;;AAavC,SAAgB,qBACd,SACQ;CACR,OAAO,EACL,SAAS,KAAK;EACZ,IAAI,cAAc,OAAO,QAAQ;GAC/B,MAAM,EAAE,cAAc;GAGtB,IAAI,MAAM,GAAG,WAAW,UAAU;GAClC,IAAI,UAAU,YACZ,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,WAAW,EAC7D,MAAM,IAAI,QAAQ,IAAI,IAAI,IAAI,mBAAmB,OAAO,MAAM,CAAC,CAAC;GAKpE,IAAI,UAAU,aAAa;IACzB,MAAM,SAAS,IAAI,iBAAiB;IACpC,8BAA8B,QAAQ,UAAU,YAAY;IAC5D,MAAM,KAAK,OAAO,UAAU;IAC5B,IAAI,IAAI,OAAO,IAAI;;GAGrB,MAAM,UAAkC;IACtC,GAAI,UAAU,WAAW,EAAE;IAC3B,eAAe,UAAU,QAAQ,gBAAgB;IAClD;GAED,MAAM,OAAoB;IACxB,QAAQ,UAAU,OAAO,aAAa;IACtC;IACD;GAKD,IAAI,UAAU,gBAAgB,yBAAyB,UAAU,MAAM;IACrE,MAAM,WAAW,IAAI,UAAU;IAC/B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAChC,UAAU,KACX,EAAE;KACD,IAAI,SAAS,MAAM;KACnB,IAAI,aAAa,MAAM,EACrB,SAAS,OAAO,KAAK,MAAM,OAAe;UAE1C,SAAS,OAAO,KAAK,OAAO,MAAM,CAAC;;IAGvC,KAAK,OAAO;UACP,IACL,UAAU,gBAAgB,qCAC1B;IACA,QAAQ,kBAAkB;IAC1B,IAAI,UAAU,MAAM;KAClB,MAAM,SAAS,IAAI,iBAAiB;KACpC,8BACE,QACA,UAAU,KACX;KACD,MAAM,UAAU,OAAO,UAAU;KACjC,IAAI,SAAS,KAAK,OAAO;;UAEtB;IACL,QAAQ,kBAAkB;IAC1B,IAAI,UAAU,MACZ,KAAK,OAAO,KAAK,UAAU,UAAU,KAAK;;GAG9C,MAAM,WAAW,MAAM,MAAM,KAAK,KAAK;GACvC,MAAM,kBAA0C,OAAO,OAAO,KAAK;GACnE,SAAS,QAAQ,SAAS,OAAO,QAAQ;IACvC,gBAAgB,OAAO;KACvB;GAEF,OAAO;IACL,QAAQ,SAAS;IACjB,YAAY,SAAS;IACrB,SAAS;IAET,UAAU,SAAS,WAAW,MAAM,KAAA,IAAY,MAAM,SAAS,MAAM;IACtE;IACD;IAEL"}
|
package/dist/server/constants.js
CHANGED
|
@@ -12,12 +12,34 @@ const HUBSPOT_ACCESS_TOKEN_COOKIE_NAME = "__Host-hs_access_token";
|
|
|
12
12
|
*/
|
|
13
13
|
const HUBSPOT_APP_SID_COOKIE_NAME = "__Host-hs_app_sid";
|
|
14
14
|
/**
|
|
15
|
+
* Cookie pinning the browser-facing app origin (e.g.
|
|
16
|
+
* `https://app.example.com`) for the lifetime of an app session.
|
|
17
|
+
* Set by `auth/init-session` from the request's `Origin` header and
|
|
18
|
+
* read by:
|
|
19
|
+
*
|
|
20
|
+
* - The CORS middleware to emit a credentialed
|
|
21
|
+
* `Access-Control-Allow-Origin` value (`*` is forbidden when
|
|
22
|
+
* `Access-Control-Allow-Credentials: true`).
|
|
23
|
+
* - `auth/complete` to rebuild the OAuth `redirect_uri` it sent to
|
|
24
|
+
* HubSpot during `init-session` (the token endpoint validates that
|
|
25
|
+
* the two values match).
|
|
26
|
+
*
|
|
27
|
+
* `__Host-` prefixed (Path=/, Secure, no Domain), so the same
|
|
28
|
+
* function host can serve both `hubspot-connect` and `api` routes
|
|
29
|
+
* and read the cookie from either.
|
|
30
|
+
*/
|
|
31
|
+
const HUBSPOT_APP_ORIGIN_COOKIE_NAME = "__Host-hs_app_origin";
|
|
32
|
+
/**
|
|
15
33
|
* Prefix used for refresh-token cookies. Each session gets its own
|
|
16
34
|
* cookie name (`hs_refresh_<sidHash>`) so multiple devices/tabs can
|
|
17
35
|
* coexist without overwriting each other's refresh tokens.
|
|
18
36
|
*/
|
|
19
37
|
const HUBSPOT_REFRESH_COOKIE_PREFIX = "hs_refresh_";
|
|
20
|
-
const PROTECTED_COOKIE_NAMES = new Set([
|
|
38
|
+
const PROTECTED_COOKIE_NAMES = new Set([
|
|
39
|
+
HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,
|
|
40
|
+
HUBSPOT_APP_SID_COOKIE_NAME,
|
|
41
|
+
HUBSPOT_APP_ORIGIN_COOKIE_NAME
|
|
42
|
+
]);
|
|
21
43
|
/**
|
|
22
44
|
* Returns `true` for cookies the SDK manages internally
|
|
23
45
|
* (access token, session ID, any refresh-token cookie). The
|
|
@@ -30,17 +52,22 @@ function isProtectedCookieName(cookieName) {
|
|
|
30
52
|
}
|
|
31
53
|
/**
|
|
32
54
|
* Cookie carrying the PKCE code verifier between `init-session` and
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
55
|
+
* `auth/complete`. Set with `SameSite=None; Secure; Partitioned` so the
|
|
56
|
+
* frontend's credentialed cross-site `POST /auth/complete` (made from
|
|
57
|
+
* the OAuth callback page on the app origin to the SDK's edge function
|
|
58
|
+
* origin) carries it through. `Lax` would silently drop it on that
|
|
59
|
+
* fetch, breaking every successful HubSpot redirect.
|
|
35
60
|
*/
|
|
36
61
|
const TEMP_COOKIE_PKCE_VERIFIER = "__hs_pkce_verifier";
|
|
37
62
|
/**
|
|
38
63
|
* Cookie carrying the OAuth `state` value between `init-session` and
|
|
39
|
-
* `
|
|
40
|
-
* primary CSRF defense.
|
|
64
|
+
* `auth/complete`. Compared against the `state` query parameter as the
|
|
65
|
+
* primary CSRF defense. Same `SameSite=None; Secure; Partitioned`
|
|
66
|
+
* attributes as `TEMP_COOKIE_PKCE_VERIFIER` for the same cross-site
|
|
67
|
+
* `POST /auth/complete` reason.
|
|
41
68
|
*/
|
|
42
69
|
const TEMP_COOKIE_OAUTH_STATE = "__hs_oauth_state";
|
|
43
70
|
//#endregion
|
|
44
|
-
export { HUBSPOT_ACCESS_TOKEN_COOKIE_NAME, HUBSPOT_APP_SID_COOKIE_NAME, HUBSPOT_REFRESH_COOKIE_PREFIX, TEMP_COOKIE_OAUTH_STATE, TEMP_COOKIE_PKCE_VERIFIER, isProtectedCookieName };
|
|
71
|
+
export { HUBSPOT_ACCESS_TOKEN_COOKIE_NAME, HUBSPOT_APP_ORIGIN_COOKIE_NAME, HUBSPOT_APP_SID_COOKIE_NAME, HUBSPOT_REFRESH_COOKIE_PREFIX, TEMP_COOKIE_OAUTH_STATE, TEMP_COOKIE_PKCE_VERIFIER, isProtectedCookieName };
|
|
45
72
|
|
|
46
73
|
//# sourceMappingURL=constants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":[],"sources":["../../src/server/constants.ts"],"sourcesContent":["/**\n * Cookie name carrying the DPoP-bound access token. Uses the\n * `__Host-` prefix so the browser only accepts it from a `Secure`\n * response with `Path=/` — a defense-in-depth against subdomain\n * cookie injection.\n */\nexport const HUBSPOT_ACCESS_TOKEN_COOKIE_NAME = '__Host-hs_access_token';\n\n/**\n * Cookie carrying the opaque app-session ID. Hashed before being put\n * on the wire as a DPoP `sid` claim.\n */\nexport const HUBSPOT_APP_SID_COOKIE_NAME = '__Host-hs_app_sid';\n\n/**\n * Prefix used for refresh-token cookies. Each session gets its own\n * cookie name (`hs_refresh_<sidHash>`) so multiple devices/tabs can\n * coexist without overwriting each other's refresh tokens.\n */\nexport const HUBSPOT_REFRESH_COOKIE_PREFIX = 'hs_refresh_';\n\nconst PROTECTED_COOKIE_NAMES = new Set([\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n]);\n\n/**\n * Returns `true` for cookies the SDK manages internally\n * (access token, session ID, any refresh-token cookie). The\n * `sanitizeRequest` helper uses this to strip these cookies from the\n * request before user code sees it, ensuring user route handlers\n * cannot accidentally leak them in logs or proxy them upstream.\n */\nexport function isProtectedCookieName(cookieName: string): boolean {\n return (\n PROTECTED_COOKIE_NAMES.has(cookieName) ||\n cookieName.startsWith(HUBSPOT_REFRESH_COOKIE_PREFIX)\n );\n}\n\n/**\n * Cookie carrying the PKCE code verifier between `init-session` and\n * `
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../src/server/constants.ts"],"sourcesContent":["/**\n * Cookie name carrying the DPoP-bound access token. Uses the\n * `__Host-` prefix so the browser only accepts it from a `Secure`\n * response with `Path=/` — a defense-in-depth against subdomain\n * cookie injection.\n */\nexport const HUBSPOT_ACCESS_TOKEN_COOKIE_NAME = '__Host-hs_access_token';\n\n/**\n * Cookie carrying the opaque app-session ID. Hashed before being put\n * on the wire as a DPoP `sid` claim.\n */\nexport const HUBSPOT_APP_SID_COOKIE_NAME = '__Host-hs_app_sid';\n\n/**\n * Cookie pinning the browser-facing app origin (e.g.\n * `https://app.example.com`) for the lifetime of an app session.\n * Set by `auth/init-session` from the request's `Origin` header and\n * read by:\n *\n * - The CORS middleware to emit a credentialed\n * `Access-Control-Allow-Origin` value (`*` is forbidden when\n * `Access-Control-Allow-Credentials: true`).\n * - `auth/complete` to rebuild the OAuth `redirect_uri` it sent to\n * HubSpot during `init-session` (the token endpoint validates that\n * the two values match).\n *\n * `__Host-` prefixed (Path=/, Secure, no Domain), so the same\n * function host can serve both `hubspot-connect` and `api` routes\n * and read the cookie from either.\n */\nexport const HUBSPOT_APP_ORIGIN_COOKIE_NAME = '__Host-hs_app_origin';\n\n/**\n * Prefix used for refresh-token cookies. Each session gets its own\n * cookie name (`hs_refresh_<sidHash>`) so multiple devices/tabs can\n * coexist without overwriting each other's refresh tokens.\n */\nexport const HUBSPOT_REFRESH_COOKIE_PREFIX = 'hs_refresh_';\n\nconst PROTECTED_COOKIE_NAMES = new Set([\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n HUBSPOT_APP_ORIGIN_COOKIE_NAME,\n]);\n\n/**\n * Returns `true` for cookies the SDK manages internally\n * (access token, session ID, any refresh-token cookie). The\n * `sanitizeRequest` helper uses this to strip these cookies from the\n * request before user code sees it, ensuring user route handlers\n * cannot accidentally leak them in logs or proxy them upstream.\n */\nexport function isProtectedCookieName(cookieName: string): boolean {\n return (\n PROTECTED_COOKIE_NAMES.has(cookieName) ||\n cookieName.startsWith(HUBSPOT_REFRESH_COOKIE_PREFIX)\n );\n}\n\n/**\n * Cookie carrying the PKCE code verifier between `init-session` and\n * `auth/complete`. Set with `SameSite=None; Secure; Partitioned` so the\n * frontend's credentialed cross-site `POST /auth/complete` (made from\n * the OAuth callback page on the app origin to the SDK's edge function\n * origin) carries it through. `Lax` would silently drop it on that\n * fetch, breaking every successful HubSpot redirect.\n */\nexport const TEMP_COOKIE_PKCE_VERIFIER = '__hs_pkce_verifier';\n\n/**\n * Cookie carrying the OAuth `state` value between `init-session` and\n * `auth/complete`. Compared against the `state` query parameter as the\n * primary CSRF defense. Same `SameSite=None; Secure; Partitioned`\n * attributes as `TEMP_COOKIE_PKCE_VERIFIER` for the same cross-site\n * `POST /auth/complete` reason.\n */\nexport const TEMP_COOKIE_OAUTH_STATE = '__hs_oauth_state';\n"],"mappings":";;;;;;;AAMA,MAAa,mCAAmC;;;;;AAMhD,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;AAmB3C,MAAa,iCAAiC;;;;;;AAO9C,MAAa,gCAAgC;AAE7C,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACD,CAAC;;;;;;;;AASF,SAAgB,sBAAsB,YAA6B;CACjE,OACE,uBAAuB,IAAI,WAAW,IACtC,WAAW,WAAA,cAAyC;;;;;;;;;;AAYxD,MAAa,4BAA4B;;;;;;;;AASzC,MAAa,0BAA0B"}
|
|
@@ -5,6 +5,7 @@ import { HUBSPOT_ACCESS_TOKEN_COOKIE_NAME, HUBSPOT_APP_SID_COOKIE_NAME } from ".
|
|
|
5
5
|
import { createHubSpotProxy } from "../proxy.js";
|
|
6
6
|
import { parseCookies } from "../utils/cookie-utils.js";
|
|
7
7
|
import { sanitizeRequest } from "../sanitize-request.js";
|
|
8
|
+
import { corsMiddleware } from "./utils/cors-middleware.js";
|
|
8
9
|
import { Hono } from "hono";
|
|
9
10
|
//#region src/server/hono/hono-request-handler.ts
|
|
10
11
|
/**
|
|
@@ -19,32 +20,36 @@ import { Hono } from "hono";
|
|
|
19
20
|
function createAppConnectRequestHandler(options) {
|
|
20
21
|
const { registerRoutes, appKeys, logger = noopLogger } = options;
|
|
21
22
|
const app = new Hono();
|
|
23
|
+
app.use("*", corsMiddleware());
|
|
24
|
+
app.use("*", async (c, next) => {
|
|
25
|
+
const { authenticated } = c.env.hubSpot;
|
|
26
|
+
if (!authenticated) return c.json({ error: "Unauthorized" }, 401);
|
|
27
|
+
await next();
|
|
28
|
+
});
|
|
22
29
|
registerRoutes(app);
|
|
23
30
|
return (request) => {
|
|
24
31
|
const cookie = request.headers.get("Cookie");
|
|
25
32
|
if (!cookie) throw new Error("Missing auth cookies");
|
|
26
33
|
const cookies = parseCookies(cookie);
|
|
27
34
|
const accessToken = cookies[HUBSPOT_ACCESS_TOKEN_COOKIE_NAME];
|
|
28
|
-
const
|
|
29
|
-
if (!accessToken || !sessionId) return new Response(null, {
|
|
30
|
-
status: 401,
|
|
31
|
-
statusText: "Unauthorized",
|
|
32
|
-
headers: { "Content-Type": "application/json" }
|
|
33
|
-
});
|
|
34
|
-
const hubSpotProxy = createHubSpotProxy({
|
|
35
|
+
const proxy = createHubSpotProxy({
|
|
35
36
|
userCredentials: {
|
|
36
37
|
accessToken,
|
|
37
|
-
sessionId
|
|
38
|
+
sessionId: cookies[HUBSPOT_APP_SID_COOKIE_NAME]
|
|
38
39
|
},
|
|
39
40
|
appKeys,
|
|
40
41
|
logger
|
|
41
42
|
});
|
|
42
|
-
const
|
|
43
|
+
const client = createHubSpotClient({ plugins: [fetchTransportPlugin({ getAccessToken: () => {
|
|
44
|
+
if (!accessToken) throw new Error("Missing access token");
|
|
45
|
+
return accessToken;
|
|
46
|
+
} })] });
|
|
43
47
|
const sanitizedRequest = sanitizeRequest(request);
|
|
44
|
-
const honoBindings = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
const honoBindings = { hubSpot: {
|
|
49
|
+
proxy,
|
|
50
|
+
client,
|
|
51
|
+
authenticated: proxy.authenticated
|
|
52
|
+
} };
|
|
48
53
|
return app.fetch(sanitizedRequest, honoBindings);
|
|
49
54
|
};
|
|
50
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono-request-handler.js","names":[],"sources":["../../../src/server/hono/hono-request-handler.ts"],"sourcesContent":["import { Hono } from 'hono';\n\nimport { noopLogger, type Logger } from '../../shared/logger.ts';\nimport { createHubSpotClient } from '../api-client-core/client.ts';\nimport { fetchTransportPlugin } from '../api-client-core/plugins/fetch-transport.ts';\nimport {\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n} from '../constants.ts';\nimport { createHubSpotProxy } from '../proxy.ts';\nimport { sanitizeRequest } from '../sanitize-request.ts';\nimport type { AppKeys, UserCredentials } from '../types.ts';\nimport { parseCookies } from '../utils/cookie-utils.ts';\nimport type { AppConnectHonoBindings, AppConnectHonoEnv } from './types.ts';\n\n/**\n * Web-standard fetch handler signature returned by\n * {@link createAppConnectRequestHandler}.\n */\nexport type AppConnectFetchHandler = (\n request: Request\n) => Response | Promise<Response>;\n\n/**\n * Callback used to attach application routes to the SDK-owned Hono\n * app instance.\n */\nexport type RegisterAppConnectRoutesFunction = (\n app: Hono<AppConnectHonoEnv>\n) => void;\n\n/**\n * Options accepted by {@link createAppConnectRequestHandler}.\n */\nexport interface CreateAppConnectRequestHandlerOptions {\n /** Registers application routes on the SDK-owned Hono app. */\n registerRoutes: RegisterAppConnectRoutesFunction;\n /**\n * Imported app keys from `secureStart`, or `null` when CIMD and DPoP\n * are both disabled.\n */\n appKeys: AppKeys | null;\n /**\n * Optional logger. When omitted the SDK uses a no-op logger so\n * server-side state never leaks into the host application's\n * console.\n */\n logger?: Logger;\n}\n\n/**\n * Wraps a Hono app so its `fetch` handler additionally:\n *\n * - Strips SDK-managed cookies (access token, refresh, sid) from the\n * request before the app sees them, via `sanitizeRequest`.\n * - Exposes a `hubSpotProxy` on the Hono context so route handlers\n * can issue authenticated calls to HubSpot's API on behalf of the\n * browser session.\n */\nexport function createAppConnectRequestHandler(\n options: CreateAppConnectRequestHandlerOptions\n): AppConnectFetchHandler {\n const { registerRoutes, appKeys, logger = noopLogger } = options;\n const app = new Hono<AppConnectHonoEnv>();\n registerRoutes(app);\n\n return (request: Request) => {\n const cookie = request.headers.get('Cookie');\n if (!cookie) {\n throw new Error('Missing auth cookies');\n }\n const cookies = parseCookies(cookie);\n const accessToken = cookies[HUBSPOT_ACCESS_TOKEN_COOKIE_NAME];\n const sessionId = cookies[HUBSPOT_APP_SID_COOKIE_NAME];\n
|
|
1
|
+
{"version":3,"file":"hono-request-handler.js","names":[],"sources":["../../../src/server/hono/hono-request-handler.ts"],"sourcesContent":["import { Hono } from 'hono';\n\nimport { noopLogger, type Logger } from '../../shared/logger.ts';\nimport { createHubSpotClient } from '../api-client-core/client.ts';\nimport { fetchTransportPlugin } from '../api-client-core/plugins/fetch-transport.ts';\nimport {\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n} from '../constants.ts';\nimport { createHubSpotProxy } from '../proxy.ts';\nimport { sanitizeRequest } from '../sanitize-request.ts';\nimport type { AppKeys, UserCredentials } from '../types.ts';\nimport { parseCookies } from '../utils/cookie-utils.ts';\nimport type { AppConnectHonoBindings, AppConnectHonoEnv } from './types.ts';\nimport { corsMiddleware } from './utils/cors-middleware.ts';\n\n/**\n * Web-standard fetch handler signature returned by\n * {@link createAppConnectRequestHandler}.\n */\nexport type AppConnectFetchHandler = (\n request: Request\n) => Response | Promise<Response>;\n\n/**\n * Callback used to attach application routes to the SDK-owned Hono\n * app instance.\n */\nexport type RegisterAppConnectRoutesFunction = (\n app: Hono<AppConnectHonoEnv>\n) => void;\n\n/**\n * Options accepted by {@link createAppConnectRequestHandler}.\n */\nexport interface CreateAppConnectRequestHandlerOptions {\n /** Registers application routes on the SDK-owned Hono app. */\n registerRoutes: RegisterAppConnectRoutesFunction;\n /**\n * Imported app keys from `secureStart`, or `null` when CIMD and DPoP\n * are both disabled.\n */\n appKeys: AppKeys | null;\n /**\n * Optional logger. When omitted the SDK uses a no-op logger so\n * server-side state never leaks into the host application's\n * console.\n */\n logger?: Logger;\n}\n\n/**\n * Wraps a Hono app so its `fetch` handler additionally:\n *\n * - Strips SDK-managed cookies (access token, refresh, sid) from the\n * request before the app sees them, via `sanitizeRequest`.\n * - Exposes a `hubSpotProxy` on the Hono context so route handlers\n * can issue authenticated calls to HubSpot's API on behalf of the\n * browser session.\n */\nexport function createAppConnectRequestHandler(\n options: CreateAppConnectRequestHandlerOptions\n): AppConnectFetchHandler {\n const { registerRoutes, appKeys, logger = noopLogger } = options;\n const app = new Hono<AppConnectHonoEnv>();\n // Credentialed CORS first: preflights short-circuit with 204\n // before the auth check runs, and 401 responses still carry\n // `Access-Control-Allow-*` headers (the browser drops responses\n // without them on credentialed cross-site fetches).\n app.use('*', corsMiddleware());\n\n // Auth gate: every non-OPTIONS request must arrive with the\n // SDK-managed access-token + session-id cookies. The CORS\n // middleware above short-circuits OPTIONS, so this only runs on\n // real requests; missing cookies now surface as a normal 401 with\n // CORS headers attached on the way back out (the previous\n // implementation threw on a missing `Cookie` header before any\n // middleware ran, which broke browser preflights).\n app.use('*', async (c, next) => {\n const { authenticated } = c.env.hubSpot;\n if (!authenticated) {\n return c.json({ error: 'Unauthorized' }, 401);\n }\n\n await next();\n return;\n });\n\n registerRoutes(app);\n\n return (request: Request) => {\n const cookie = request.headers.get('Cookie');\n if (!cookie) {\n throw new Error('Missing auth cookies');\n }\n const cookies = parseCookies(cookie);\n const accessToken = cookies[HUBSPOT_ACCESS_TOKEN_COOKIE_NAME];\n const sessionId = cookies[HUBSPOT_APP_SID_COOKIE_NAME];\n\n const userCredentials: UserCredentials = { accessToken, sessionId };\n\n const proxy = createHubSpotProxy({\n userCredentials,\n appKeys,\n logger,\n });\n\n const client = createHubSpotClient({\n plugins: [\n fetchTransportPlugin({\n getAccessToken: () => {\n if (!accessToken) {\n throw new Error('Missing access token');\n }\n return accessToken;\n },\n }),\n ],\n });\n\n const sanitizedRequest = sanitizeRequest(request);\n\n const honoBindings: AppConnectHonoBindings = {\n hubSpot: {\n proxy,\n client,\n authenticated: proxy.authenticated,\n },\n };\n\n return app.fetch(sanitizedRequest, honoBindings);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA4DA,SAAgB,+BACd,SACwB;CACxB,MAAM,EAAE,gBAAgB,SAAS,SAAS,eAAe;CACzD,MAAM,MAAM,IAAI,MAAyB;CAKzC,IAAI,IAAI,KAAK,gBAAgB,CAAC;CAS9B,IAAI,IAAI,KAAK,OAAO,GAAG,SAAS;EAC9B,MAAM,EAAE,kBAAkB,EAAE,IAAI;EAChC,IAAI,CAAC,eACH,OAAO,EAAE,KAAK,EAAE,OAAO,gBAAgB,EAAE,IAAI;EAG/C,MAAM,MAAM;GAEZ;CAEF,eAAe,IAAI;CAEnB,QAAQ,YAAqB;EAC3B,MAAM,SAAS,QAAQ,QAAQ,IAAI,SAAS;EAC5C,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,uBAAuB;EAEzC,MAAM,UAAU,aAAa,OAAO;EACpC,MAAM,cAAc,QAAQ;EAK5B,MAAM,QAAQ,mBAAmB;GAC/B,iBAAA;IAHyC;IAAa,WAFtC,QAAQ;IAKT;GACf;GACA;GACD,CAAC;EAEF,MAAM,SAAS,oBAAoB,EACjC,SAAS,CACP,qBAAqB,EACnB,sBAAsB;GACpB,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,uBAAuB;GAEzC,OAAO;KAEV,CAAC,CACH,EACF,CAAC;EAEF,MAAM,mBAAmB,gBAAgB,QAAQ;EAEjD,MAAM,eAAuC,EAC3C,SAAS;GACP;GACA;GACA,eAAe,MAAM;GACtB,EACF;EAED,OAAO,IAAI,MAAM,kBAAkB,aAAa"}
|