@hubspot/app-connect-sdk 1.0.0-alpha.11 → 1.0.0-alpha.13
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 +81 -50
- package/.turbo/turbo-tsdown.log +119 -119
- package/build/tsconfig.browser.tsbuildinfo +1 -1
- package/build/tsconfig.server.tsbuildinfo +1 -1
- package/dist/browser/{HubSpotAppConnect-COQgPrFn.js → HubSpotAppConnect-DFe9b90e.js} +1 -2
- package/dist/browser/HubSpotAppConnect-DFe9b90e.js.map +1 -0
- package/dist/browser/create-crdncXsh.js.map +1 -1
- package/dist/browser/react/lovable.d.ts +1 -2
- package/dist/browser/react/lovable.js +1 -1
- package/dist/browser/react/lovable.js.map +1 -1
- package/dist/browser/react.d.ts +1 -2
- package/dist/browser/react.js +1 -1
- package/dist/server/api-client-core/apis/account/account-info.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/account/audit-logs.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/auth/oauth.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/automation/actions.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/automation/sequences.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/business-units.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/authors.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/blog-settings.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/cms-content-audit.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/domains.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/hubdb.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/media-bridge.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/pages.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/posts.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/site-search.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/source-code.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/tags.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/url-mappings.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/cms/url-redirects.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/communication-preferences/subscriptions.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/conversations/custom-channels.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/conversations/visitor-identification.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/conversations.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/app-uninstalls.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/appointments.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/associations-schema.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/associations.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/calling-extensions.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/calls.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/carts.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/commerce-payments.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/commerce-subscriptions.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/communications.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/companies.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/contacts.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/contracts.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/courses.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/crm-owners.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/custom-objects.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/deal-splits.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/deals.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/discounts.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/emails.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/exports.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/feedback-submissions.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/fees.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/goal-targets.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/imports.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/invoices.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/leads.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/limits-tracking.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/line-items.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/listings.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/lists.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/meetings.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/notes.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/object-library.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/objects.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/orders.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/partner-clients.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/partner-services.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/pipelines.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/postal-mail.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/products.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/projects.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/properties.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/property-validations.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/public-app-crm-cards.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/public-app-feature-flags.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/quotes.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/schemas.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/services.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/tasks.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/taxes.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/tickets.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/timeline.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/transcriptions.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/users.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/crm/video-conferencing-extension.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/events/manage-event-definitions.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/events/send-event-completions.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/events.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/files.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/marketing/campaigns-public-api.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/marketing/marketing-emails.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/marketing/marketing-events.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/marketing/single-send.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/marketing/transactional-single-send.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/meta/origins.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/scheduler/meetings.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/settings/multicurrency.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/settings/tax-rates.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/settings/user-provisioning.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/webhooks-journal.generated.js.map +1 -1
- package/dist/server/api-client-core/apis/webhooks.generated.js.map +1 -1
- package/dist/server/api-client-core/binary-data.js.map +1 -1
- package/dist/server/api-client-core/client.js +5 -1
- package/dist/server/api-client-core/client.js.map +1 -1
- package/dist/server/api-client-core/codegen-helpers/file-op-wrappers.js.map +1 -1
- package/dist/server/api-client-core/errors.js.map +1 -1
- package/dist/server/api-client-core/op.js.map +1 -1
- package/dist/server/api-client-core/pagination.js.map +1 -1
- package/dist/server/api-client-core/plugins/fetch-transport.js +28 -8
- package/dist/server/api-client-core/plugins/fetch-transport.js.map +1 -1
- package/dist/server/constants.js.map +1 -1
- package/dist/server/deno/start.js.map +1 -1
- package/dist/server/hono/hono-request-handler.js +21 -17
- package/dist/server/hono/hono-request-handler.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-complete.js +2 -1
- package/dist/server/hono/hubspot-connect-routes/auth-complete.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-init-session.js +3 -1
- package/dist/server/hono/hubspot-connect-routes/auth-init-session.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-logout.js +14 -8
- package/dist/server/hono/hubspot-connect-routes/auth-logout.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-refresh.js +26 -18
- package/dist/server/hono/hubspot-connect-routes/auth-refresh.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/cimd-client-metadata-types.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/cimd-public-routes.js +4 -1
- package/dist/server/hono/hubspot-connect-routes/cimd-public-routes.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/fetch-hubspot-client-metadata.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/hubspot-connect-routes.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/load-hubspot-connect-routes-env.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/oauth-client.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/utils.js +5 -5
- package/dist/server/hono/hubspot-connect-routes/utils.js.map +1 -1
- package/dist/server/hono/types.d.ts +0 -5
- package/dist/server/hono/utils/cookie-utils.js.map +1 -1
- package/dist/server/hono/utils/cors-middleware.js.map +1 -1
- package/dist/server/import-app-keys.js.map +1 -1
- package/dist/server/lovable/create-app-function-start.d.ts +1 -1
- package/dist/server/lovable/create-app-function-start.js +4 -5
- package/dist/server/lovable/create-app-function-start.js.map +1 -1
- package/dist/server/lovable/hubspot-connect/index.js.map +1 -1
- package/dist/server/lovable/hubspot-connect/run-hubspot-connect-lovable-server.js +11 -11
- package/dist/server/lovable/hubspot-connect/run-hubspot-connect-lovable-server.js.map +1 -1
- package/dist/server/sanitize-request.js +0 -11
- package/dist/server/sanitize-request.js.map +1 -1
- package/dist/server/secure-start-core.js.map +1 -1
- package/dist/server/shared/constants.js +2 -2
- package/dist/server/shared/constants.js.map +1 -1
- package/dist/server/shared/encoding/base64.js.map +1 -1
- package/dist/server/shared/encoding/sha256.js.map +1 -1
- package/dist/server/shared/logger.js.map +1 -1
- package/dist/server/types.d.ts +1 -35
- package/dist/server/utils/cookie-utils.js.map +1 -1
- package/dist/server/utils/dpop-utils.js.map +1 -1
- package/dist/server/utils/env-utils.js +1 -1
- package/dist/server/utils/env-utils.js.map +1 -1
- package/dist/server/utils/hubspot-dpop-auth-headers.js +38 -0
- package/dist/server/utils/hubspot-dpop-auth-headers.js.map +1 -0
- package/dist/server/utils/jwk-utils.js.map +1 -1
- package/dist/server/utils/jwt-utils.js.map +1 -1
- package/package.json +16 -16
- package/src/browser/app-connect-controller/init.test.ts +4 -4
- package/src/browser/app-connect-controller/init.ts +2 -2
- package/src/browser/react/components/ConnectButton/ConnectButton.tsx +0 -1
- package/src/server/api-client-core/client.ts +5 -1
- package/src/server/api-client-core/plugins/fetch-transport.test.ts +114 -0
- package/src/server/api-client-core/plugins/fetch-transport.ts +65 -11
- package/src/server/api-client-core/plugins/index.ts +1 -0
- package/src/server/hono/hono-request-handler.ts +33 -19
- package/src/server/hono/hubspot-connect-routes/auth-complete.test.ts +2 -2
- package/src/server/hono/hubspot-connect-routes/auth-complete.ts +1 -0
- package/src/server/hono/hubspot-connect-routes/auth-init-session.test.ts +2 -2
- package/src/server/hono/hubspot-connect-routes/auth-init-session.ts +2 -0
- package/src/server/hono/hubspot-connect-routes/auth-logout.ts +21 -10
- package/src/server/hono/hubspot-connect-routes/auth-refresh.ts +18 -9
- package/src/server/hono/hubspot-connect-routes/cimd-public-routes.test.ts +7 -6
- package/src/server/hono/hubspot-connect-routes/cimd-public-routes.ts +5 -1
- package/src/server/hono/hubspot-connect-routes/hubspot-connect-routes.ts +2 -1
- package/src/server/hono/hubspot-connect-routes/utils.test.ts +16 -46
- package/src/server/hono/hubspot-connect-routes/utils.ts +6 -6
- package/src/server/hono/types.ts +0 -5
- package/src/server/lovable/create-app-function-start.ts +4 -4
- package/src/server/lovable/hubspot-connect/run-hubspot-connect-lovable-server.ts +12 -12
- package/src/server/sanitize-request.ts +1 -12
- package/src/server/types.ts +0 -36
- package/src/server/utils/env-utils.ts +1 -1
- package/src/server/utils/hubspot-dpop-auth-headers.test.ts +43 -0
- package/src/server/utils/hubspot-dpop-auth-headers.ts +48 -0
- package/src/shared/constants.ts +1 -1
- package/dist/browser/HubSpotAppConnect-COQgPrFn.js.map +0 -1
- package/dist/server/proxy.js +0 -68
- package/dist/server/proxy.js.map +0 -1
- package/src/server/proxy.test.ts +0 -80
- package/src/server/proxy.ts +0 -119
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
+
import { OAUTH_CALLBACK_PATH } from '../../../shared/constants.ts';
|
|
4
5
|
import { createTestAppKeys } from '../../utils/test-fixtures.ts';
|
|
5
6
|
import {
|
|
6
7
|
handleCimdAppJwks,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
import type { HubSpotConnectOAuthRouteOptions } from './types.ts';
|
|
14
15
|
|
|
15
16
|
const BASE_PATH = '/functions/v1/hubspot-connect';
|
|
17
|
+
const APP_ORIGIN = 'https://app.example.com';
|
|
16
18
|
|
|
17
19
|
function buildOptions(
|
|
18
20
|
hubspotConnectEnv: HubSpotConnectOAuthRouteOptions['hubspotConnectEnv'],
|
|
@@ -57,10 +59,11 @@ describe('handleCimdClientJson', () => {
|
|
|
57
59
|
app.get(`${BASE_PATH}/client.json`, (c) =>
|
|
58
60
|
handleCimdClientJson(c, buildOptions(clientSecretEnv))
|
|
59
61
|
);
|
|
60
|
-
const
|
|
61
|
-
`http://app.example.test${BASE_PATH}/client.json
|
|
62
|
-
{ method: 'GET' }
|
|
62
|
+
const clientJsonUrl = new URL(
|
|
63
|
+
`http://app.example.test${BASE_PATH}/client.json`
|
|
63
64
|
);
|
|
65
|
+
clientJsonUrl.searchParams.set('app_origin', APP_ORIGIN);
|
|
66
|
+
const res = await app.request(clientJsonUrl.toString(), { method: 'GET' });
|
|
64
67
|
|
|
65
68
|
expect(res.status).toBe(200);
|
|
66
69
|
expect(res.headers.get('Content-Type')).toContain('application/json');
|
|
@@ -71,9 +74,7 @@ describe('handleCimdClientJson', () => {
|
|
|
71
74
|
scope: { required: string[]; optional?: string[] };
|
|
72
75
|
};
|
|
73
76
|
|
|
74
|
-
expect(body.redirect_uri).toBe(
|
|
75
|
-
`http://app.example.test${BASE_PATH}/auth/callback`
|
|
76
|
-
);
|
|
77
|
+
expect(body.redirect_uri).toBe(`${APP_ORIGIN}${OAUTH_CALLBACK_PATH}`);
|
|
77
78
|
expect(body.jwks_uri).toBe(`http://app.example.test${BASE_PATH}/jwks.json`);
|
|
78
79
|
expect(body.scope.required).toContain('crm.objects.contacts.read');
|
|
79
80
|
expect(body.scope.optional).toContain('crm.objects.deals.read');
|
|
@@ -22,13 +22,17 @@ export async function handleCimdClientJson(
|
|
|
22
22
|
const xForwardedProto = c.req.header('x-forwarded-proto') ?? undefined;
|
|
23
23
|
const xForwardedHost = c.req.header('x-forwarded-host') ?? undefined;
|
|
24
24
|
const requestHostHeader = c.req.header('host') ?? undefined;
|
|
25
|
-
|
|
25
|
+
const appOrigin = c.req.query('app_origin');
|
|
26
|
+
if (!appOrigin) {
|
|
27
|
+
return c.text('Missing app origin', 400);
|
|
28
|
+
}
|
|
26
29
|
const forwarded: BuildOAuthRedirectUriFromRequestOptions = {
|
|
27
30
|
requestUrl: c.req.url,
|
|
28
31
|
basePath,
|
|
29
32
|
xForwardedProto,
|
|
30
33
|
xForwardedHost,
|
|
31
34
|
requestHostHeader,
|
|
35
|
+
appOrigin,
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
const body: HubSpotConnectCimdClientDocument = {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
handleCimdClientJson,
|
|
15
15
|
} from './cimd-public-routes.ts';
|
|
16
16
|
import type { HubSpotConnectRoutesEnv } from './load-hubspot-connect-routes-env.ts';
|
|
17
|
+
import type { HubSpotConnectOAuthRouteOptions } from './types.ts';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Options accepted by {@link registerHubSpotConnectRoutes}.
|
|
@@ -73,7 +74,7 @@ export function registerHubSpotConnectRoutes(
|
|
|
73
74
|
assertHubSpotConnectCimdClientMetadata(cimdClientMetadata);
|
|
74
75
|
|
|
75
76
|
const refreshCookiePath = `${basePath}/auth`;
|
|
76
|
-
const oauthRouteOptions = {
|
|
77
|
+
const oauthRouteOptions: HubSpotConnectOAuthRouteOptions = {
|
|
77
78
|
appKeys,
|
|
78
79
|
refreshCookiePath,
|
|
79
80
|
logger,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { OAUTH_CALLBACK_PATH } from '../../../shared/constants.ts';
|
|
3
4
|
import {
|
|
4
5
|
buildCimdClientIdUrlFromRequest,
|
|
5
6
|
buildHubSpotAppJwksUrlFromRequest,
|
|
@@ -7,6 +8,9 @@ import {
|
|
|
7
8
|
getRequestHostForHubspotConnect,
|
|
8
9
|
} from './utils.ts';
|
|
9
10
|
|
|
11
|
+
const APP_ORIGIN = 'https://third-party-app.test:5173';
|
|
12
|
+
const APP_ORIGIN_QUERY = `app_origin=${encodeURIComponent(APP_ORIGIN)}`;
|
|
13
|
+
|
|
10
14
|
describe('buildCimdClientIdUrlFromRequest', () => {
|
|
11
15
|
it('builds client id URL under base path', () => {
|
|
12
16
|
expect(
|
|
@@ -14,9 +18,10 @@ describe('buildCimdClientIdUrlFromRequest', () => {
|
|
|
14
18
|
requestUrl:
|
|
15
19
|
'https://third-party-app.test:5173/functions/v1/hubspot-connect/auth/init-session',
|
|
16
20
|
basePath: '/functions/v1/hubspot-connect',
|
|
21
|
+
appOrigin: APP_ORIGIN,
|
|
17
22
|
})
|
|
18
23
|
).toBe(
|
|
19
|
-
|
|
24
|
+
`https://third-party-app.test:5173/functions/v1/hubspot-connect/client.json?${APP_ORIGIN_QUERY}`
|
|
20
25
|
);
|
|
21
26
|
});
|
|
22
27
|
|
|
@@ -28,9 +33,10 @@ describe('buildCimdClientIdUrlFromRequest', () => {
|
|
|
28
33
|
basePath: '/functions/v1/hubspot-connect',
|
|
29
34
|
xForwardedProto: 'https',
|
|
30
35
|
xForwardedHost: 'third-party-app.test:5173',
|
|
36
|
+
appOrigin: APP_ORIGIN,
|
|
31
37
|
})
|
|
32
38
|
).toBe(
|
|
33
|
-
|
|
39
|
+
`https://third-party-app.test:5173/functions/v1/hubspot-connect/client.json?${APP_ORIGIN_QUERY}`
|
|
34
40
|
);
|
|
35
41
|
});
|
|
36
42
|
});
|
|
@@ -42,6 +48,7 @@ describe('buildHubSpotAppJwksUrlFromRequest', () => {
|
|
|
42
48
|
requestUrl:
|
|
43
49
|
'https://third-party-app.test:5173/functions/v1/hubspot-connect/auth/init-session',
|
|
44
50
|
basePath: '/functions/v1/hubspot-connect',
|
|
51
|
+
appOrigin: APP_ORIGIN,
|
|
45
52
|
})
|
|
46
53
|
).toBe(
|
|
47
54
|
'https://third-party-app.test:5173/functions/v1/hubspot-connect/jwks.json'
|
|
@@ -50,64 +57,27 @@ describe('buildHubSpotAppJwksUrlFromRequest', () => {
|
|
|
50
57
|
});
|
|
51
58
|
|
|
52
59
|
describe('buildOAuthRedirectUriFromRequest', () => {
|
|
53
|
-
it('builds callback URL from origin and
|
|
60
|
+
it('builds callback URL from app origin and frontend callback path', () => {
|
|
54
61
|
expect(
|
|
55
62
|
buildOAuthRedirectUriFromRequest({
|
|
56
63
|
requestUrl:
|
|
57
64
|
'https://third-party-app.test:5173/functions/v1/hubspot-connect/auth/init-session',
|
|
58
65
|
basePath: '/functions/v1/hubspot-connect',
|
|
66
|
+
appOrigin: APP_ORIGIN,
|
|
59
67
|
})
|
|
60
|
-
).toBe(
|
|
61
|
-
'https://third-party-app.test:5173/functions/v1/hubspot-connect/auth/callback'
|
|
62
|
-
);
|
|
68
|
+
).toBe(`${APP_ORIGIN}${OAUTH_CALLBACK_PATH}`);
|
|
63
69
|
});
|
|
64
70
|
|
|
65
|
-
it('
|
|
71
|
+
it('ignores basePath and forwarded headers for redirect URI', () => {
|
|
66
72
|
expect(
|
|
67
73
|
buildOAuthRedirectUriFromRequest({
|
|
68
74
|
requestUrl: 'https://example.com/x',
|
|
69
75
|
basePath: '/functions/v1/hubspot-connect/',
|
|
70
|
-
})
|
|
71
|
-
).toBe('https://example.com/functions/v1/hubspot-connect/auth/callback');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('uses X-Forwarded-Proto and X-Forwarded-Host when present', () => {
|
|
75
|
-
expect(
|
|
76
|
-
buildOAuthRedirectUriFromRequest({
|
|
77
|
-
requestUrl:
|
|
78
|
-
'http://127.0.0.1:5175/functions/v1/hubspot-connect/auth/init-session',
|
|
79
|
-
basePath: '/functions/v1/hubspot-connect',
|
|
80
|
-
xForwardedProto: 'https',
|
|
81
|
-
xForwardedHost: 'third-party-app.test:5173',
|
|
82
|
-
})
|
|
83
|
-
).toBe(
|
|
84
|
-
'https://third-party-app.test:5173/functions/v1/hubspot-connect/auth/callback'
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('uses X-Forwarded-Proto with Host header when X-Forwarded-Host is absent', () => {
|
|
89
|
-
expect(
|
|
90
|
-
buildOAuthRedirectUriFromRequest({
|
|
91
|
-
requestUrl:
|
|
92
|
-
'http://127.0.0.1:5175/functions/v1/hubspot-connect/auth/init-session',
|
|
93
|
-
basePath: '/functions/v1/hubspot-connect',
|
|
94
|
-
xForwardedProto: 'https',
|
|
95
|
-
requestHostHeader: 'third-party-app.test:5173',
|
|
96
|
-
})
|
|
97
|
-
).toBe(
|
|
98
|
-
'https://third-party-app.test:5173/functions/v1/hubspot-connect/auth/callback'
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('uses X-Forwarded-Proto with request URL host when forwarded host and Host are absent', () => {
|
|
103
|
-
expect(
|
|
104
|
-
buildOAuthRedirectUriFromRequest({
|
|
105
|
-
requestUrl:
|
|
106
|
-
'http://127.0.0.1:5175/functions/v1/hubspot-connect/auth/init-session',
|
|
107
|
-
basePath: '/functions/v1/hubspot-connect',
|
|
108
76
|
xForwardedProto: 'https',
|
|
77
|
+
xForwardedHost: 'other.example.com',
|
|
78
|
+
appOrigin: APP_ORIGIN,
|
|
109
79
|
})
|
|
110
|
-
).toBe(
|
|
80
|
+
).toBe(`${APP_ORIGIN}${OAUTH_CALLBACK_PATH}`);
|
|
111
81
|
});
|
|
112
82
|
});
|
|
113
83
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OAUTH_CALLBACK_PATH } from '../../../shared/constants.ts';
|
|
2
2
|
import { serializeCookie } from '../utils/cookie-utils.ts';
|
|
3
3
|
|
|
4
4
|
export function clearTempCookie(name: string): string {
|
|
@@ -63,7 +63,7 @@ export function parseAppOriginHeader(
|
|
|
63
63
|
* the OAuth token endpoint's `redirect_uri` check).
|
|
64
64
|
*/
|
|
65
65
|
export function buildFrontendOAuthRedirectUri(appOrigin: string): string {
|
|
66
|
-
return `${appOrigin}${
|
|
66
|
+
return `${appOrigin}${OAUTH_CALLBACK_PATH}`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export function isSafeReturnPath(rawPath: string): boolean {
|
|
@@ -126,6 +126,7 @@ export interface BuildOAuthRedirectUriFromRequestOptions {
|
|
|
126
126
|
xForwardedHost?: string | undefined;
|
|
127
127
|
/** `Host` when `X-Forwarded-Host` is absent but `X-Forwarded-Proto` is set. */
|
|
128
128
|
requestHostHeader?: string | undefined;
|
|
129
|
+
appOrigin: string;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
function normalizeHubSpotConnectBasePath(basePath: string): string {
|
|
@@ -162,9 +163,8 @@ export function buildHubSpotConnectRequestOrigin(
|
|
|
162
163
|
export function buildOAuthRedirectUriFromRequest(
|
|
163
164
|
options: BuildOAuthRedirectUriFromRequestOptions
|
|
164
165
|
): string {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
return `${origin}${trimmed}/auth/callback`;
|
|
166
|
+
const { appOrigin } = options;
|
|
167
|
+
return `${appOrigin}${OAUTH_CALLBACK_PATH}`;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
@@ -175,7 +175,7 @@ export function buildCimdClientIdUrlFromRequest(
|
|
|
175
175
|
): string {
|
|
176
176
|
const trimmed = normalizeHubSpotConnectBasePath(options.basePath);
|
|
177
177
|
const origin = buildHubSpotConnectRequestOrigin(options);
|
|
178
|
-
return `${origin}${trimmed}/client.json`;
|
|
178
|
+
return `${origin}${trimmed}/client.json?app_origin=${encodeURIComponent(options.appOrigin)}`;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
package/src/server/hono/types.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type AppFunctionStart = (context: SecureStartContext) => Promise<void>;
|
|
|
27
27
|
/**
|
|
28
28
|
* Builds a Deno-style `start({ appKeys })` entry point that boots a
|
|
29
29
|
* Hono app under `basePath`, wires the SDK's per-request HubSpot
|
|
30
|
-
*
|
|
30
|
+
* client via `createAppConnectRequestHandler`, and serves it with
|
|
31
31
|
* `Deno.serve` on `PORT`.
|
|
32
32
|
*/
|
|
33
33
|
export function createAppFunctionStart(
|
|
@@ -35,8 +35,8 @@ export function createAppFunctionStart(
|
|
|
35
35
|
): AppFunctionStart {
|
|
36
36
|
const { basePath, registerRoutes, logger } = options;
|
|
37
37
|
|
|
38
|
-
return ({ appKeys }) => {
|
|
39
|
-
Deno.serve(
|
|
38
|
+
return async ({ appKeys }) => {
|
|
39
|
+
const server = Deno.serve(
|
|
40
40
|
serveOptions,
|
|
41
41
|
createAppConnectRequestHandler({
|
|
42
42
|
appKeys,
|
|
@@ -47,6 +47,6 @@ export function createAppFunctionStart(
|
|
|
47
47
|
})
|
|
48
48
|
);
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
await server.finished;
|
|
51
51
|
};
|
|
52
52
|
}
|
|
@@ -10,29 +10,29 @@ import {
|
|
|
10
10
|
const PORT = Deno.env.get('PORT');
|
|
11
11
|
const port = typeof PORT === 'string' ? parseInt(PORT!, 10) : undefined;
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const PUBLIC_HUBSPOT_CONNECT_BASE_PATH = '/functions/v1/hubspot-connect';
|
|
14
14
|
|
|
15
15
|
export interface RunHubSpotConnectLovableServerOptions extends SecureStartContext {
|
|
16
16
|
cimdClientMetadata: HubSpotConnectCimdClientMetadata;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function runHubSpotConnectLovableServer(
|
|
19
|
+
export async function runHubSpotConnectLovableServer(
|
|
20
20
|
options: RunHubSpotConnectLovableServerOptions
|
|
21
21
|
): Promise<void> {
|
|
22
22
|
const { appKeys, cimdClientMetadata } = options;
|
|
23
23
|
const hubspotConnectEnv = loadHubSpotConnectRoutesEnv();
|
|
24
24
|
|
|
25
|
-
const app = new Hono().basePath(
|
|
25
|
+
const app = new Hono().basePath('/hubspot-connect');
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
27
|
+
registerHubSpotConnectRoutes({
|
|
28
|
+
app,
|
|
29
|
+
appKeys,
|
|
30
|
+
basePath: PUBLIC_HUBSPOT_CONNECT_BASE_PATH,
|
|
31
|
+
hubspotConnectEnv,
|
|
32
|
+
cimdClientMetadata,
|
|
33
|
+
});
|
|
35
34
|
|
|
35
|
+
const serveHandler = (request: Request): Response | Promise<Response> => {
|
|
36
36
|
return app.fetch(request);
|
|
37
37
|
};
|
|
38
38
|
|
|
@@ -41,5 +41,5 @@ export function runHubSpotConnectLovableServer(
|
|
|
41
41
|
? Deno.serve({ port }, serveHandler)
|
|
42
42
|
: Deno.serve(serveHandler);
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
await server.finished;
|
|
45
45
|
}
|
|
@@ -13,13 +13,8 @@ function serializeCookies(cookies: Map<string, string>): string {
|
|
|
13
13
|
* Mutates `headers` in place: parses the `Cookie` header, drops every
|
|
14
14
|
* protected cookie (see {@link isProtectedCookieName}), and rewrites
|
|
15
15
|
* the header. Deletes the header entirely when nothing survives.
|
|
16
|
-
*
|
|
17
|
-
* Used by the SDK's auth middleware after it has read the access
|
|
18
|
-
* token / session ID, so user route handlers never see — and
|
|
19
|
-
* therefore cannot leak — those cookies, while CORS / auth
|
|
20
|
-
* middleware that ran first still got the raw values.
|
|
21
16
|
*/
|
|
22
|
-
|
|
17
|
+
function stripProtectedCookies(headers: Headers): void {
|
|
23
18
|
const cookies = parseCookies(headers.get('Cookie'));
|
|
24
19
|
const surviving = new Map<string, string>();
|
|
25
20
|
for (const [name, value] of Object.entries(cookies)) {
|
|
@@ -39,12 +34,6 @@ export function stripProtectedCookies(headers: Headers): void {
|
|
|
39
34
|
* Returns a clone of `original` whose `Cookie` header has every
|
|
40
35
|
* protected cookie removed (see {@link isProtectedCookieName}). When
|
|
41
36
|
* no other cookies remain, the header is dropped entirely.
|
|
42
|
-
*
|
|
43
|
-
* Standalone helper retained for callers that want a new Request
|
|
44
|
-
* (e.g. tests). Inside the SDK's per-request handler, the auth
|
|
45
|
-
* middleware uses {@link stripProtectedCookies} directly on
|
|
46
|
-
* `c.req.raw.headers` so the auth check itself can still read the
|
|
47
|
-
* protected cookies before they are stripped.
|
|
48
37
|
*/
|
|
49
38
|
export function sanitizeRequest(original: Request): Request {
|
|
50
39
|
const headers = new Headers(original.headers);
|
package/src/server/types.ts
CHANGED
|
@@ -10,42 +10,6 @@ export interface AppKeys {
|
|
|
10
10
|
appPublicKeyJwk: JsonWebKey;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Request shape accepted by `HubSpotProxy.fetch`. Only the `path`
|
|
15
|
-
* is required; everything else mirrors the equivalent fetch fields.
|
|
16
|
-
*/
|
|
17
|
-
export interface HubSpotProxyRequest {
|
|
18
|
-
/** Path component of the upstream URL, including leading slash. */
|
|
19
|
-
path: string;
|
|
20
|
-
/** HTTP method. Defaults to `GET`. */
|
|
21
|
-
method?: string;
|
|
22
|
-
/**
|
|
23
|
-
* Extra request headers. The proxy adds `Authorization` itself
|
|
24
|
-
* (`DPoP` access token plus `DPoP` proof when DPoP is enabled and
|
|
25
|
-
* `appKeys` is non-null; otherwise `Bearer` only).
|
|
26
|
-
*/
|
|
27
|
-
headers?: Record<string, string>;
|
|
28
|
-
/** Optional request body. Pass `null`/`undefined` for empty bodies. */
|
|
29
|
-
body?: string | null | undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Authenticated proxy returned by `createHubSpotProxy`. Use it
|
|
34
|
-
* inside Hono handlers (via `c.env.hubSpotProxy`) to call
|
|
35
|
-
* HubSpot's API on behalf of the browser session that issued the
|
|
36
|
-
* incoming request.
|
|
37
|
-
*/
|
|
38
|
-
export interface HubSpotProxy {
|
|
39
|
-
/**
|
|
40
|
-
* `true` when the session cookies present on the inbound request
|
|
41
|
-
* yielded a usable access token. When `false`, every `fetch()` call
|
|
42
|
-
* returns a 401 without contacting the upstream.
|
|
43
|
-
*/
|
|
44
|
-
authenticated: boolean;
|
|
45
|
-
/** Performs an authenticated upstream request. */
|
|
46
|
-
fetch: (request: HubSpotProxyRequest) => Promise<Response>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
13
|
/**
|
|
50
14
|
* RFC 7517 JWK Set. Returned by HubSpot's `/oauth/v1/jwks` endpoint
|
|
51
15
|
* and used to verify access tokens on the resource server.
|
|
@@ -60,7 +60,7 @@ export function requireEnv(key: string): string {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* HubSpot API origin used by
|
|
63
|
+
* HubSpot API origin used by the HubSpot API client transport. Defaults to
|
|
64
64
|
* `https://api.hubapi.com` when `HUBSPOT_API_ORIGIN` is unset.
|
|
65
65
|
*/
|
|
66
66
|
export function getHubSpotApiOrigin(): string {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { sha256base64url } from './crypto-utils.ts';
|
|
4
|
+
import { verifyDpopProof } from './dpop-utils.ts';
|
|
5
|
+
import { buildHubSpotDpopAuthHeaders } from './hubspot-dpop-auth-headers.ts';
|
|
6
|
+
import { createTestAppKeys } from './test-fixtures.ts';
|
|
7
|
+
|
|
8
|
+
describe('buildHubSpotDpopAuthHeaders', () => {
|
|
9
|
+
it('returns DPoP authorization headers with a signed proof', async () => {
|
|
10
|
+
const appKeys = await createTestAppKeys();
|
|
11
|
+
const headers = await buildHubSpotDpopAuthHeaders({
|
|
12
|
+
accessToken: 'tok',
|
|
13
|
+
sessionId: 'sid',
|
|
14
|
+
appKeys,
|
|
15
|
+
method: 'GET',
|
|
16
|
+
targetUrl: 'https://api.example.test/x',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(headers.Authorization).toBe('DPoP tok');
|
|
20
|
+
expect(headers.DPoP).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('strips query string parameters from the htu claim', async () => {
|
|
24
|
+
const appKeys = await createTestAppKeys();
|
|
25
|
+
const headers = await buildHubSpotDpopAuthHeaders({
|
|
26
|
+
accessToken: 'tok',
|
|
27
|
+
sessionId: 'sid',
|
|
28
|
+
appKeys,
|
|
29
|
+
method: 'GET',
|
|
30
|
+
targetUrl: 'https://api.example.test/x?limit=10&properties=email',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await expect(
|
|
34
|
+
verifyDpopProof({
|
|
35
|
+
proof: headers.DPoP!,
|
|
36
|
+
htm: 'GET',
|
|
37
|
+
htu: 'https://api.example.test/x',
|
|
38
|
+
ath: await sha256base64url('tok'),
|
|
39
|
+
sid: await sha256base64url('sid'),
|
|
40
|
+
})
|
|
41
|
+
).resolves.toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type AppKeys } from '../types.ts';
|
|
2
|
+
import { sha256base64url } from './crypto-utils.ts';
|
|
3
|
+
import { signDpopProof } from './dpop-utils.ts';
|
|
4
|
+
|
|
5
|
+
export interface BuildHubSpotDpopAuthHeadersOptions {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
appKeys: AppKeys;
|
|
9
|
+
method: string;
|
|
10
|
+
targetUrl: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getDpopHtuFromTargetUrl(targetUrl: string): string {
|
|
14
|
+
const url = new URL(targetUrl);
|
|
15
|
+
url.search = '';
|
|
16
|
+
url.hash = '';
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds `Authorization` and `DPoP` headers for an authenticated
|
|
22
|
+
* HubSpot API request when DPoP is enabled.
|
|
23
|
+
*/
|
|
24
|
+
export async function buildHubSpotDpopAuthHeaders(
|
|
25
|
+
options: BuildHubSpotDpopAuthHeadersOptions
|
|
26
|
+
): Promise<Record<string, string>> {
|
|
27
|
+
const { accessToken, sessionId, appKeys, method, targetUrl } = options;
|
|
28
|
+
const htu = getDpopHtuFromTargetUrl(targetUrl);
|
|
29
|
+
|
|
30
|
+
const ath = await sha256base64url(accessToken);
|
|
31
|
+
const sid = await sha256base64url(sessionId);
|
|
32
|
+
const dpopProof = await signDpopProof({
|
|
33
|
+
appKeys,
|
|
34
|
+
claims: {
|
|
35
|
+
htm: method,
|
|
36
|
+
htu,
|
|
37
|
+
jti: crypto.randomUUID(),
|
|
38
|
+
iat: Math.floor(Date.now() / 1000),
|
|
39
|
+
ath,
|
|
40
|
+
sid,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
Authorization: `DPoP ${accessToken}`,
|
|
46
|
+
DPoP: dpopProof,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/shared/constants.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const EXPIRES_AT_URL_PARAM = '__hs_expires_at';
|
|
|
26
26
|
* register `${app_origin}${HUBSPOT_FRONTEND_CALLBACK_PATH}` as a
|
|
27
27
|
* redirect URI in its HubSpot app settings.
|
|
28
28
|
*/
|
|
29
|
-
export const
|
|
29
|
+
export const OAUTH_CALLBACK_PATH = '/__hubspot_oauth_callback';
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Query parameter on the `auth/complete` POST request carrying the
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|