@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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt-utils.js","names":[],"sources":["../../../src/server/utils/jwt-utils.ts"],"sourcesContent":["import { base64url, base64urlDecode } from './base64-utils.ts';\n\ninterface EncodeAndSignJwtOptions {\n header: Record<string, unknown>;\n payload: Record<string, unknown>;\n privateKey: CryptoKey;\n}\n\n/**\n * Low-level helper that encodes a JWS Compact Serialization JWT\n * (RFC 7519) and signs it with the supplied `privateKey` using\n * ES256 (P-256 + SHA-256). Returns the three-segment compact form.\n */\nexport async function encodeAndSignJwt(\n options: EncodeAndSignJwtOptions\n): Promise<string> {\n const { header, payload, privateKey } = options;\n\n const encodedHeader = base64url(\n new TextEncoder().encode(JSON.stringify(header))\n );\n const encodedPayload = base64url(\n new TextEncoder().encode(JSON.stringify(payload))\n );\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n const signatureBuffer = await crypto.subtle.sign(\n { name: 'ECDSA', hash: 'SHA-256' },\n privateKey,\n new TextEncoder().encode(signingInput)\n );\n return `${signingInput}.${base64url(new Uint8Array(signatureBuffer))}`;\n}\n\nasync function importPublicKey(jwk: JsonWebKey): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n 'jwk',\n jwk,\n { name: 'ECDSA', namedCurve: 'P-256' },\n false,\n ['verify']\n );\n}\n\ninterface DecodeAndVerifyJwtOptions {\n token: string;\n publicKeyJwk: JsonWebKey;\n}\n\n/**\n * Verifies the ES256 signature on `token` against `publicKeyJwk` and\n * returns the decoded payload. Does not check `exp` — use\n * {@link verifyJwt} when expiry enforcement is desired.\n *\n * @throws {Error} When the token isn't three segments, when the\n * signature fails verification, or when the payload isn't valid\n * JSON.\n */\nexport async function decodeAndVerifyJwt(\n options: DecodeAndVerifyJwtOptions\n): Promise<Record<string, unknown>> {\n const { token, publicKeyJwk } = options;\n const parts = token.split('.');\n if (parts.length !== 3) throw new Error('Invalid JWT format');\n const [encodedHeader, encodedPayload, encodedSignature] = parts as [\n string,\n string,\n string,\n ];\n const publicKey = await importPublicKey(publicKeyJwk);\n const valid = await crypto.subtle.verify(\n { name: 'ECDSA', hash: 'SHA-256' },\n publicKey,\n base64urlDecode(encodedSignature),\n new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`)\n );\n if (!valid) throw new Error('JWT signature verification failed');\n return JSON.parse(\n new TextDecoder().decode(base64urlDecode(encodedPayload))\n ) as Record<string, unknown>;\n}\n\nexport interface VerifyJwtOptions {\n /** Compact-serialized JWT to verify. */\n token: string;\n /** Public key in JWK form. Caller is responsible for trusting it. */\n publicKeyJwk: JsonWebKey;\n}\n\n/**\n * Verifies signature and (if present) `exp` (RFC 7519 §4.1.4) on a\n * JWT and returns its payload.\n *\n * @throws {Error} When the signature fails or when `exp` has passed.\n */\nexport async function verifyJwt(\n options: VerifyJwtOptions\n): Promise<Record<string, unknown>> {\n const { token, publicKeyJwk } = options;\n const payload = await decodeAndVerifyJwt({ token, publicKeyJwk });\n const now = Math.floor(Date.now() / 1000);\n if (typeof payload['exp'] === 'number' && payload['exp'] < now) {\n throw new Error('JWT expired');\n }\n return payload;\n}\n\nexport interface SignJwtOptions {\n /** ES256 private key as a non-extractable WebCrypto key. */\n privateKey: CryptoKey;\n /**\n * Custom claims merged onto an `iat` claim (and `exp` when\n * `ttlSeconds` is supplied). Caller-provided keys override the\n * standard ones.\n */\n payload: Record<string, unknown>;\n /**\n * Lifetime of the token in seconds. When set, the JWT's `exp` claim\n * is computed as `iat + ttlSeconds`. When omitted, no `exp` is added\n * (the caller is responsible for one if needed).\n */\n ttlSeconds?: number;\n}\n\n/**\n * Signs a JWT (RFC 7519) with `alg=ES256, typ=JWT` and returns the\n * compact serialization. Always sets `iat` to the current second; the\n * caller controls every other claim via `payload`.\n */\nexport async function signJwt(options: SignJwtOptions): Promise<string> {\n const { privateKey, payload, ttlSeconds } = options;\n const now = Math.floor(Date.now() / 1000);\n const payloadWithStandardClaims =\n ttlSeconds !== undefined\n ? { iat: now, exp: now + ttlSeconds, ...payload }\n : { iat: now, ...payload };\n return encodeAndSignJwt({\n header: { alg: 'ES256', typ: 'JWT' },\n payload: payloadWithStandardClaims,\n privateKey,\n });\n}\n"],"mappings":";;;;;;;AAaA,eAAsB,iBACpB,SACiB;CACjB,MAAM,EAAE,QAAQ,SAAS,eAAe;CAQxC,MAAM,eAAe,GANC,UACpB,IAAI,
|
|
1
|
+
{"version":3,"file":"jwt-utils.js","names":[],"sources":["../../../src/server/utils/jwt-utils.ts"],"sourcesContent":["import { base64url, base64urlDecode } from './base64-utils.ts';\n\ninterface EncodeAndSignJwtOptions {\n header: Record<string, unknown>;\n payload: Record<string, unknown>;\n privateKey: CryptoKey;\n}\n\n/**\n * Low-level helper that encodes a JWS Compact Serialization JWT\n * (RFC 7519) and signs it with the supplied `privateKey` using\n * ES256 (P-256 + SHA-256). Returns the three-segment compact form.\n */\nexport async function encodeAndSignJwt(\n options: EncodeAndSignJwtOptions\n): Promise<string> {\n const { header, payload, privateKey } = options;\n\n const encodedHeader = base64url(\n new TextEncoder().encode(JSON.stringify(header))\n );\n const encodedPayload = base64url(\n new TextEncoder().encode(JSON.stringify(payload))\n );\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n const signatureBuffer = await crypto.subtle.sign(\n { name: 'ECDSA', hash: 'SHA-256' },\n privateKey,\n new TextEncoder().encode(signingInput)\n );\n return `${signingInput}.${base64url(new Uint8Array(signatureBuffer))}`;\n}\n\nasync function importPublicKey(jwk: JsonWebKey): Promise<CryptoKey> {\n return crypto.subtle.importKey(\n 'jwk',\n jwk,\n { name: 'ECDSA', namedCurve: 'P-256' },\n false,\n ['verify']\n );\n}\n\ninterface DecodeAndVerifyJwtOptions {\n token: string;\n publicKeyJwk: JsonWebKey;\n}\n\n/**\n * Verifies the ES256 signature on `token` against `publicKeyJwk` and\n * returns the decoded payload. Does not check `exp` — use\n * {@link verifyJwt} when expiry enforcement is desired.\n *\n * @throws {Error} When the token isn't three segments, when the\n * signature fails verification, or when the payload isn't valid\n * JSON.\n */\nexport async function decodeAndVerifyJwt(\n options: DecodeAndVerifyJwtOptions\n): Promise<Record<string, unknown>> {\n const { token, publicKeyJwk } = options;\n const parts = token.split('.');\n if (parts.length !== 3) throw new Error('Invalid JWT format');\n const [encodedHeader, encodedPayload, encodedSignature] = parts as [\n string,\n string,\n string,\n ];\n const publicKey = await importPublicKey(publicKeyJwk);\n const valid = await crypto.subtle.verify(\n { name: 'ECDSA', hash: 'SHA-256' },\n publicKey,\n base64urlDecode(encodedSignature),\n new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`)\n );\n if (!valid) throw new Error('JWT signature verification failed');\n return JSON.parse(\n new TextDecoder().decode(base64urlDecode(encodedPayload))\n ) as Record<string, unknown>;\n}\n\nexport interface VerifyJwtOptions {\n /** Compact-serialized JWT to verify. */\n token: string;\n /** Public key in JWK form. Caller is responsible for trusting it. */\n publicKeyJwk: JsonWebKey;\n}\n\n/**\n * Verifies signature and (if present) `exp` (RFC 7519 §4.1.4) on a\n * JWT and returns its payload.\n *\n * @throws {Error} When the signature fails or when `exp` has passed.\n */\nexport async function verifyJwt(\n options: VerifyJwtOptions\n): Promise<Record<string, unknown>> {\n const { token, publicKeyJwk } = options;\n const payload = await decodeAndVerifyJwt({ token, publicKeyJwk });\n const now = Math.floor(Date.now() / 1000);\n if (typeof payload['exp'] === 'number' && payload['exp'] < now) {\n throw new Error('JWT expired');\n }\n return payload;\n}\n\nexport interface SignJwtOptions {\n /** ES256 private key as a non-extractable WebCrypto key. */\n privateKey: CryptoKey;\n /**\n * Custom claims merged onto an `iat` claim (and `exp` when\n * `ttlSeconds` is supplied). Caller-provided keys override the\n * standard ones.\n */\n payload: Record<string, unknown>;\n /**\n * Lifetime of the token in seconds. When set, the JWT's `exp` claim\n * is computed as `iat + ttlSeconds`. When omitted, no `exp` is added\n * (the caller is responsible for one if needed).\n */\n ttlSeconds?: number;\n}\n\n/**\n * Signs a JWT (RFC 7519) with `alg=ES256, typ=JWT` and returns the\n * compact serialization. Always sets `iat` to the current second; the\n * caller controls every other claim via `payload`.\n */\nexport async function signJwt(options: SignJwtOptions): Promise<string> {\n const { privateKey, payload, ttlSeconds } = options;\n const now = Math.floor(Date.now() / 1000);\n const payloadWithStandardClaims =\n ttlSeconds !== undefined\n ? { iat: now, exp: now + ttlSeconds, ...payload }\n : { iat: now, ...payload };\n return encodeAndSignJwt({\n header: { alg: 'ES256', typ: 'JWT' },\n payload: payloadWithStandardClaims,\n privateKey,\n });\n}\n"],"mappings":";;;;;;;AAaA,eAAsB,iBACpB,SACiB;CACjB,MAAM,EAAE,QAAQ,SAAS,eAAe;CAQxC,MAAM,eAAe,GANC,UACpB,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,MAAM,CAAC,CAKb,EAAE,GAHf,UACrB,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,OAAO,CAAC,CAEI;CACtD,MAAM,kBAAkB,MAAM,OAAO,OAAO,KAC1C;EAAE,MAAM;EAAS,MAAM;CAAU,GACjC,YACA,IAAI,YAAY,EAAE,OAAO,YAAY,CACvC;CACA,OAAO,GAAG,aAAa,GAAG,UAAU,IAAI,WAAW,eAAe,CAAC;AACrE;AAEA,eAAe,gBAAgB,KAAqC;CAClE,OAAO,OAAO,OAAO,UACnB,OACA,KACA;EAAE,MAAM;EAAS,YAAY;CAAQ,GACrC,OACA,CAAC,QAAQ,CACX;AACF;;;;;;;;;;AAgBA,eAAsB,mBACpB,SACkC;CAClC,MAAM,EAAE,OAAO,iBAAiB;CAChC,MAAM,QAAQ,MAAM,MAAM,GAAG;CAC7B,IAAI,MAAM,WAAW,GAAG,MAAM,IAAI,MAAM,oBAAoB;CAC5D,MAAM,CAAC,eAAe,gBAAgB,oBAAoB;CAK1D,MAAM,YAAY,MAAM,gBAAgB,YAAY;CAOpD,IAAI,CAAC,MANe,OAAO,OAAO,OAChC;EAAE,MAAM;EAAS,MAAM;CAAU,GACjC,WACA,gBAAgB,gBAAgB,GAChC,IAAI,YAAY,EAAE,OAAO,GAAG,cAAc,GAAG,gBAAgB,CAC/D,GACY,MAAM,IAAI,MAAM,mCAAmC;CAC/D,OAAO,KAAK,MACV,IAAI,YAAY,EAAE,OAAO,gBAAgB,cAAc,CAAC,CAC1D;AACF;;;;;;;AAeA,eAAsB,UACpB,SACkC;CAClC,MAAM,EAAE,OAAO,iBAAiB;CAChC,MAAM,UAAU,MAAM,mBAAmB;EAAE;EAAO;CAAa,CAAC;CAChE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CACxC,IAAI,OAAO,QAAQ,WAAW,YAAY,QAAQ,SAAS,KACzD,MAAM,IAAI,MAAM,aAAa;CAE/B,OAAO;AACT;;;;;;AAwBA,eAAsB,QAAQ,SAA0C;CACtE,MAAM,EAAE,YAAY,SAAS,eAAe;CAC5C,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CAKxC,OAAO,iBAAiB;EACtB,QAAQ;GAAE,KAAK;GAAS,KAAK;EAAM;EACnC,SALA,eAAe,KAAA,IACX;GAAE,KAAK;GAAK,KAAK,MAAM;GAAY,GAAG;EAAQ,IAC9C;GAAE,KAAK;GAAK,GAAG;EAAQ;EAI3B;CACF,CAAC;AACH"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/app-connect-sdk",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.13",
|
|
4
4
|
"description": "HubSpot App Connect SDK (alpha release). Documentation and integration guidance forthcoming.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./browser": "./dist/browser/index.js",
|
|
8
8
|
"./react": "./dist/browser/react.js",
|
|
9
9
|
"./react/lovable": "./dist/browser/react/lovable.js",
|
|
10
|
-
"./server/api-client": "./dist/server/api-client
|
|
10
|
+
"./server/api-client": "./dist/server/api-client.js",
|
|
11
11
|
"./server/lovable": "./dist/server/lovable.js",
|
|
12
12
|
"./server/oauth": "./dist/server/oauth.js"
|
|
13
13
|
},
|
|
@@ -26,27 +26,27 @@
|
|
|
26
26
|
"react": "^18.0.0 || ^19.0.0"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@base-ui/react": "
|
|
30
|
-
"hono": "
|
|
29
|
+
"@base-ui/react": "1.4.1",
|
|
30
|
+
"hono": "4.12.19"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=24.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@types/deno": "
|
|
37
|
-
"@types/node": "25.
|
|
38
|
-
"@types/react": "
|
|
39
|
-
"@vanilla-extract/css": "
|
|
40
|
-
"@vanilla-extract/rollup-plugin": "
|
|
41
|
-
"eslint": "10.0
|
|
42
|
-
"prettier": "3.8.
|
|
43
|
-
"react": "
|
|
44
|
-
"tsdown": "0.22.0
|
|
36
|
+
"@types/deno": "2.7.0",
|
|
37
|
+
"@types/node": "25.9.0",
|
|
38
|
+
"@types/react": "19.2.14",
|
|
39
|
+
"@vanilla-extract/css": "1.20.1",
|
|
40
|
+
"@vanilla-extract/rollup-plugin": "1.5.3",
|
|
41
|
+
"eslint": "10.4.0",
|
|
42
|
+
"prettier": "3.8.3",
|
|
43
|
+
"react": "19.2.6",
|
|
44
|
+
"tsdown": "0.22.0",
|
|
45
45
|
"typescript": "6.0.3",
|
|
46
|
-
"vitest": "4.
|
|
46
|
+
"vitest": "4.1.6",
|
|
47
47
|
"@private/eslint-config": "0.1.0",
|
|
48
|
-
"@private/
|
|
49
|
-
"@private/
|
|
48
|
+
"@private/tsconfig": "0.1.0",
|
|
49
|
+
"@private/prettier-config": "0.1.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"clean": "rm -rf dist build *.tsbuildinfo node_modules .turbo",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { OAUTH_CALLBACK_PATH } from '../../shared/constants.ts';
|
|
4
4
|
import { noopLogger } from '../../shared/logger.ts';
|
|
5
5
|
import { EXPIRES_AT_KEY } from './constants.ts';
|
|
6
6
|
import { initAppConnect } from './init.ts';
|
|
@@ -104,7 +104,7 @@ describe('initAppConnect', () => {
|
|
|
104
104
|
it('POSTs to /auth/complete on the OAuth callback path and persists expires_at + return_path', async () => {
|
|
105
105
|
const expiresAt = Date.now() + 1800 * 1000;
|
|
106
106
|
installFakeWindow({
|
|
107
|
-
href: `https://app.example.com${
|
|
107
|
+
href: `https://app.example.com${OAUTH_CALLBACK_PATH}?code=auth-code&state=auth-state`,
|
|
108
108
|
});
|
|
109
109
|
const fetchSpy = vi
|
|
110
110
|
.spyOn(globalThis, 'fetch')
|
|
@@ -138,7 +138,7 @@ describe('initAppConnect', () => {
|
|
|
138
138
|
|
|
139
139
|
it('throws and clears session storage when /auth/complete returns a non-OK response', async () => {
|
|
140
140
|
installFakeWindow({
|
|
141
|
-
href: `https://app.example.com${
|
|
141
|
+
href: `https://app.example.com${OAUTH_CALLBACK_PATH}?code=auth-code&state=bad-state`,
|
|
142
142
|
});
|
|
143
143
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
144
144
|
new Response('{"error":"State mismatch"}', {
|
|
@@ -155,7 +155,7 @@ describe('initAppConnect', () => {
|
|
|
155
155
|
|
|
156
156
|
it('skips the callback step on the callback path when code or state are missing', async () => {
|
|
157
157
|
installFakeWindow({
|
|
158
|
-
href: `https://app.example.com${
|
|
158
|
+
href: `https://app.example.com${OAUTH_CALLBACK_PATH}`,
|
|
159
159
|
});
|
|
160
160
|
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
161
161
|
const context = createTestContext();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AUTH_COMPLETE_CODE_PARAM,
|
|
3
3
|
AUTH_COMPLETE_STATE_PARAM,
|
|
4
|
-
|
|
4
|
+
OAUTH_CALLBACK_PATH,
|
|
5
5
|
} from '../../shared/constants.ts';
|
|
6
6
|
import type { AuthCompleteResponse } from '../../shared/wire-types.ts';
|
|
7
7
|
import { EXPIRES_AT_KEY } from './constants.ts';
|
|
@@ -37,7 +37,7 @@ export async function initAppConnect(
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async function consumeOAuthCallback(context: AppConnectContext): Promise<void> {
|
|
40
|
-
if (window.location.pathname !==
|
|
40
|
+
if (window.location.pathname !== OAUTH_CALLBACK_PATH) return;
|
|
41
41
|
|
|
42
42
|
const params = new URLSearchParams(window.location.search);
|
|
43
43
|
const code = params.get(AUTH_COMPLETE_CODE_PARAM);
|
|
@@ -20,7 +20,6 @@ export function ConnectButton({
|
|
|
20
20
|
className,
|
|
21
21
|
}: ConnectButtonProps) {
|
|
22
22
|
const { status, connectToHubSpot } = useHubSpotAppConnect();
|
|
23
|
-
console.log('status', status);
|
|
24
23
|
const isConnecting = status === 'connecting' || status === 'initializing';
|
|
25
24
|
const composedClassName = [root, className].filter(Boolean).join(' ');
|
|
26
25
|
const labelClassName = isConnecting ? `${label} ${labelMuted}` : label;
|
|
@@ -18,7 +18,11 @@ import type {
|
|
|
18
18
|
*/
|
|
19
19
|
function assertSuccess(response: ApiResponse): void {
|
|
20
20
|
if (response.status < 200 || response.status >= 300) {
|
|
21
|
-
|
|
21
|
+
const error = new HubSpotApiError(response);
|
|
22
|
+
if (response.bodyJson == null) {
|
|
23
|
+
error.message = `${error.message} (empty or non-JSON response body)`;
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createHubSpotClient } from '../client.ts';
|
|
4
|
+
import { op } from '../op.ts';
|
|
5
|
+
import {
|
|
6
|
+
fetchTransportPlugin,
|
|
7
|
+
type FetchTransportHeaderContext,
|
|
8
|
+
} from './fetch-transport.ts';
|
|
9
|
+
|
|
10
|
+
describe('fetchTransportPlugin', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('uses Bearer authorization when getAuthorizationHeaders is omitted', async () => {
|
|
16
|
+
const fetchSpy = vi
|
|
17
|
+
.spyOn(globalThis, 'fetch')
|
|
18
|
+
.mockResolvedValue(new Response('{}'));
|
|
19
|
+
|
|
20
|
+
const client = createHubSpotClient({
|
|
21
|
+
plugins: [
|
|
22
|
+
fetchTransportPlugin({
|
|
23
|
+
getEndpoint: () => 'https://api.example.test',
|
|
24
|
+
getAccessToken: () => 'tok',
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await client.send(
|
|
30
|
+
op.get('/crm/v3/objects/contacts/{contactId}', {
|
|
31
|
+
pathParams: { contactId: '123' },
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
36
|
+
'https://api.example.test/crm/v3/objects/contacts/123',
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
headers: expect.objectContaining({
|
|
39
|
+
Authorization: 'Bearer tok',
|
|
40
|
+
}),
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('uses getAuthorizationHeaders for authorization when provided', async () => {
|
|
46
|
+
const fetchSpy = vi
|
|
47
|
+
.spyOn(globalThis, 'fetch')
|
|
48
|
+
.mockResolvedValue(new Response('{}'));
|
|
49
|
+
const getAuthorizationHeaders = vi.fn(
|
|
50
|
+
(_ctx: FetchTransportHeaderContext) => ({
|
|
51
|
+
Authorization: 'DPoP tok',
|
|
52
|
+
DPoP: 'proof',
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const client = createHubSpotClient({
|
|
57
|
+
plugins: [
|
|
58
|
+
fetchTransportPlugin({
|
|
59
|
+
getEndpoint: () => 'https://api.example.test',
|
|
60
|
+
getAuthorizationHeaders,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await client.send(op.get('/x'));
|
|
66
|
+
|
|
67
|
+
const init = fetchSpy.mock.calls[0]?.[1] as RequestInit;
|
|
68
|
+
const headers = init.headers as Record<string, string>;
|
|
69
|
+
expect(headers.Authorization).toBe('DPoP tok');
|
|
70
|
+
expect(headers.DPoP).toBe('proof');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('passes targetUrl with query params to getAuthorizationHeaders', async () => {
|
|
74
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('{}'));
|
|
75
|
+
const getAuthorizationHeaders = vi.fn(
|
|
76
|
+
(_ctx: FetchTransportHeaderContext) => ({
|
|
77
|
+
Authorization: 'DPoP tok',
|
|
78
|
+
DPoP: 'proof',
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const client = createHubSpotClient({
|
|
83
|
+
plugins: [
|
|
84
|
+
fetchTransportPlugin({
|
|
85
|
+
getEndpoint: () => 'https://api.example.test',
|
|
86
|
+
getAuthorizationHeaders,
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await client.send(
|
|
92
|
+
op.get('/x', {
|
|
93
|
+
queryParams: { limit: 10, properties: ['email'] },
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(getAuthorizationHeaders).toHaveBeenCalledWith(
|
|
98
|
+
expect.objectContaining({
|
|
99
|
+
targetUrl: 'https://api.example.test/x?limit=10&properties=email',
|
|
100
|
+
method: 'GET',
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('throws when neither getAccessToken nor getAuthorizationHeaders is provided', async () => {
|
|
106
|
+
const client = createHubSpotClient({
|
|
107
|
+
plugins: [fetchTransportPlugin({})],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await expect(client.send(op.get('/x'))).rejects.toThrow(
|
|
111
|
+
'fetchTransportPlugin: getAccessToken or getAuthorizationHeaders is required'
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { isBinaryData } from '../binary-data.ts';
|
|
2
|
-
import type { Plugin } from '../types.ts';
|
|
2
|
+
import type { Operation, Plugin } from '../types.ts';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const HUBSPOT_API_ENDPOINT = 'https://api.hubapi.com';
|
|
5
|
+
|
|
6
|
+
export interface FetchTransportHeaderContext {
|
|
7
|
+
/** Fully resolved request URL (endpoint + path + query). */
|
|
8
|
+
targetUrl: string;
|
|
9
|
+
/** Uppercase HTTP method. */
|
|
10
|
+
method: string;
|
|
11
|
+
/** The operation being sent. */
|
|
12
|
+
operation: Operation;
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
export interface FetchTransportPluginOptions {
|
|
7
|
-
|
|
16
|
+
getEndpoint?: () => string;
|
|
17
|
+
getAccessToken?: () => string;
|
|
18
|
+
getAuthorizationHeaders?: (
|
|
19
|
+
ctx: FetchTransportHeaderContext
|
|
20
|
+
) => Record<string, string> | Promise<Record<string, string>>;
|
|
8
21
|
}
|
|
9
22
|
|
|
10
23
|
function appendRecordToUrlSearchParams(
|
|
@@ -23,6 +36,28 @@ function appendRecordToUrlSearchParams(
|
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
38
|
|
|
39
|
+
async function readResponseBodyJson(response: Response): Promise<unknown> {
|
|
40
|
+
if (response.status === 204) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const text = await response.text();
|
|
45
|
+
if (text.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(text) as unknown;
|
|
51
|
+
} catch {
|
|
52
|
+
if (response.status >= 200 && response.status < 300) {
|
|
53
|
+
throw new SyntaxError(
|
|
54
|
+
`fetchTransportPlugin: response body is not valid JSON (status ${response.status})`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
/**
|
|
27
62
|
* Transport plugin that executes HTTP requests using the Fetch API.
|
|
28
63
|
*
|
|
@@ -38,12 +73,17 @@ export function fetchTransportPlugin(
|
|
|
38
73
|
activate(api) {
|
|
39
74
|
api.addMiddleware(async (ctx) => {
|
|
40
75
|
const { operation } = ctx;
|
|
76
|
+
const endpoint = options.getEndpoint?.() ?? HUBSPOT_API_ENDPOINT;
|
|
77
|
+
const method = operation.method.toUpperCase();
|
|
41
78
|
|
|
42
79
|
// Interpolate path parameters (e.g. "/contacts/{contactId}" → "/contacts/123")
|
|
43
|
-
let
|
|
80
|
+
let targetUrl = `${endpoint}${operation.path}`;
|
|
44
81
|
if (operation.pathParams) {
|
|
45
82
|
for (const [key, value] of Object.entries(operation.pathParams)) {
|
|
46
|
-
|
|
83
|
+
targetUrl = targetUrl.replace(
|
|
84
|
+
`{${key}}`,
|
|
85
|
+
encodeURIComponent(String(value))
|
|
86
|
+
);
|
|
47
87
|
}
|
|
48
88
|
}
|
|
49
89
|
|
|
@@ -52,16 +92,30 @@ export function fetchTransportPlugin(
|
|
|
52
92
|
const params = new URLSearchParams();
|
|
53
93
|
appendRecordToUrlSearchParams(params, operation.queryParams);
|
|
54
94
|
const qs = params.toString();
|
|
55
|
-
if (qs)
|
|
95
|
+
if (qs) targetUrl += `?${qs}`;
|
|
56
96
|
}
|
|
57
97
|
|
|
98
|
+
const authHeaders = options.getAuthorizationHeaders
|
|
99
|
+
? await options.getAuthorizationHeaders({
|
|
100
|
+
targetUrl,
|
|
101
|
+
method,
|
|
102
|
+
operation,
|
|
103
|
+
})
|
|
104
|
+
: options.getAccessToken
|
|
105
|
+
? { Authorization: `Bearer ${options.getAccessToken()}` }
|
|
106
|
+
: (() => {
|
|
107
|
+
throw new Error(
|
|
108
|
+
'fetchTransportPlugin: getAccessToken or getAuthorizationHeaders is required'
|
|
109
|
+
);
|
|
110
|
+
})();
|
|
111
|
+
|
|
58
112
|
const headers: Record<string, string> = {
|
|
59
113
|
...(operation.headers ?? {}),
|
|
60
|
-
|
|
114
|
+
...authHeaders,
|
|
61
115
|
};
|
|
62
116
|
|
|
63
117
|
const init: RequestInit = {
|
|
64
|
-
method
|
|
118
|
+
method,
|
|
65
119
|
headers,
|
|
66
120
|
};
|
|
67
121
|
|
|
@@ -100,7 +154,8 @@ export function fetchTransportPlugin(
|
|
|
100
154
|
init.body = JSON.stringify(operation.body);
|
|
101
155
|
}
|
|
102
156
|
}
|
|
103
|
-
|
|
157
|
+
|
|
158
|
+
const response = await fetch(targetUrl, init);
|
|
104
159
|
const responseHeaders: Record<string, string> = Object.create(null);
|
|
105
160
|
response.headers.forEach((value, key) => {
|
|
106
161
|
responseHeaders[key] = value;
|
|
@@ -110,8 +165,7 @@ export function fetchTransportPlugin(
|
|
|
110
165
|
status: response.status,
|
|
111
166
|
statusText: response.statusText,
|
|
112
167
|
headers: responseHeaders,
|
|
113
|
-
|
|
114
|
-
bodyJson: response.status === 204 ? undefined : await response.json(),
|
|
168
|
+
bodyJson: await readResponseBodyJson(response),
|
|
115
169
|
};
|
|
116
170
|
});
|
|
117
171
|
},
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type Logger } from '../../shared/logger.ts';
|
|
4
4
|
import { createHubSpotClient } from '../api-client-core/client.ts';
|
|
5
5
|
import { fetchTransportPlugin } from '../api-client-core/plugins/fetch-transport.ts';
|
|
6
6
|
import {
|
|
7
7
|
HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,
|
|
8
8
|
HUBSPOT_APP_SID_COOKIE_NAME,
|
|
9
9
|
} from '../constants.ts';
|
|
10
|
-
import { createHubSpotProxy } from '../proxy.ts';
|
|
11
10
|
import { sanitizeRequest } from '../sanitize-request.ts';
|
|
12
|
-
import type { AppKeys
|
|
11
|
+
import type { AppKeys } from '../types.ts';
|
|
13
12
|
import { parseCookies } from '../utils/cookie-utils.ts';
|
|
13
|
+
import {
|
|
14
|
+
getHubSpotApiOrigin,
|
|
15
|
+
isHubspotDpopEnabled,
|
|
16
|
+
} from '../utils/env-utils.ts';
|
|
17
|
+
import { buildHubSpotDpopAuthHeaders } from '../utils/hubspot-dpop-auth-headers.ts';
|
|
14
18
|
import type { AppConnectHonoBindings, AppConnectHonoEnv } from './types.ts';
|
|
15
19
|
import { corsMiddleware } from './utils/cors-middleware.ts';
|
|
16
20
|
|
|
@@ -54,14 +58,21 @@ export interface CreateAppConnectRequestHandlerOptions {
|
|
|
54
58
|
*
|
|
55
59
|
* - Strips SDK-managed cookies (access token, refresh, sid) from the
|
|
56
60
|
* request before the app sees them, via `sanitizeRequest`.
|
|
57
|
-
* - Exposes a `
|
|
61
|
+
* - Exposes a `hubSpot.client` on the Hono context so route handlers
|
|
58
62
|
* can issue authenticated calls to HubSpot's API on behalf of the
|
|
59
63
|
* browser session.
|
|
60
64
|
*/
|
|
61
65
|
export function createAppConnectRequestHandler(
|
|
62
66
|
options: CreateAppConnectRequestHandlerOptions
|
|
63
67
|
): AppConnectFetchHandler {
|
|
64
|
-
const { registerRoutes, appKeys
|
|
68
|
+
const { registerRoutes, appKeys } = options;
|
|
69
|
+
|
|
70
|
+
if (isHubspotDpopEnabled() && appKeys === null) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'createAppConnectRequestHandler: appKeys is required when HUBSPOT_DPOP_ENABLED is true'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
const app = new Hono<AppConnectHonoEnv>();
|
|
66
77
|
// Credentialed CORS first: preflights short-circuit with 204
|
|
67
78
|
// before the auth check runs, and 401 responses still carry
|
|
@@ -97,22 +108,26 @@ export function createAppConnectRequestHandler(
|
|
|
97
108
|
const accessToken = cookies[HUBSPOT_ACCESS_TOKEN_COOKIE_NAME];
|
|
98
109
|
const sessionId = cookies[HUBSPOT_APP_SID_COOKIE_NAME];
|
|
99
110
|
|
|
100
|
-
const userCredentials: UserCredentials = { accessToken, sessionId };
|
|
101
|
-
|
|
102
|
-
const proxy = createHubSpotProxy({
|
|
103
|
-
userCredentials,
|
|
104
|
-
appKeys,
|
|
105
|
-
logger,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
111
|
const client = createHubSpotClient({
|
|
109
112
|
plugins: [
|
|
110
113
|
fetchTransportPlugin({
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
getEndpoint: () => {
|
|
115
|
+
return getHubSpotApiOrigin();
|
|
116
|
+
},
|
|
117
|
+
getAuthorizationHeaders: (ctx) => {
|
|
118
|
+
if (isHubspotDpopEnabled()) {
|
|
119
|
+
return buildHubSpotDpopAuthHeaders({
|
|
120
|
+
accessToken: accessToken!,
|
|
121
|
+
sessionId: sessionId!,
|
|
122
|
+
appKeys: appKeys!,
|
|
123
|
+
method: ctx.method,
|
|
124
|
+
targetUrl: ctx.targetUrl,
|
|
125
|
+
});
|
|
114
126
|
}
|
|
115
|
-
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
Authorization: `Bearer ${accessToken!}`,
|
|
130
|
+
};
|
|
116
131
|
},
|
|
117
132
|
}),
|
|
118
133
|
],
|
|
@@ -122,9 +137,8 @@ export function createAppConnectRequestHandler(
|
|
|
122
137
|
|
|
123
138
|
const honoBindings: AppConnectHonoBindings = {
|
|
124
139
|
hubSpot: {
|
|
125
|
-
proxy,
|
|
126
140
|
client,
|
|
127
|
-
authenticated:
|
|
141
|
+
authenticated: Boolean(accessToken && sessionId),
|
|
128
142
|
},
|
|
129
143
|
};
|
|
130
144
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { OAUTH_CALLBACK_PATH } from '../../../shared/constants.ts';
|
|
5
5
|
import {
|
|
6
6
|
HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,
|
|
7
7
|
HUBSPOT_APP_ORIGIN_COOKIE_NAME,
|
|
@@ -216,7 +216,7 @@ describe('handleAuthComplete', () => {
|
|
|
216
216
|
const body = (init as RequestInit).body as URLSearchParams | string;
|
|
217
217
|
const formParams = new URLSearchParams(body as string);
|
|
218
218
|
expect(formParams.get('redirect_uri')).toBe(
|
|
219
|
-
`${APP_ORIGIN}${
|
|
219
|
+
`${APP_ORIGIN}${OAUTH_CALLBACK_PATH}`
|
|
220
220
|
);
|
|
221
221
|
expect(formParams.get('grant_type')).toBe('authorization_code');
|
|
222
222
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { OAUTH_CALLBACK_PATH } from '../../../shared/constants.ts';
|
|
5
5
|
import {
|
|
6
6
|
HUBSPOT_APP_ORIGIN_COOKIE_NAME,
|
|
7
7
|
HUBSPOT_APP_SID_COOKIE_NAME,
|
|
@@ -111,7 +111,7 @@ describe('handleAuthInitSession', () => {
|
|
|
111
111
|
const body = (await res.json()) as { authorization_url: string };
|
|
112
112
|
const authUrl = new URL(body.authorization_url);
|
|
113
113
|
expect(authUrl.searchParams.get('redirect_uri')).toBe(
|
|
114
|
-
`${APP_ORIGIN}${
|
|
114
|
+
`${APP_ORIGIN}${OAUTH_CALLBACK_PATH}`
|
|
115
115
|
);
|
|
116
116
|
});
|
|
117
117
|
|
|
@@ -71,9 +71,11 @@ export async function handleAuthInitSession(
|
|
|
71
71
|
xForwardedProto,
|
|
72
72
|
xForwardedHost,
|
|
73
73
|
requestHostHeader,
|
|
74
|
+
appOrigin,
|
|
74
75
|
})
|
|
75
76
|
: hubspotConnectEnv.hubspotClientId;
|
|
76
77
|
|
|
78
|
+
console.log('clientId', clientId);
|
|
77
79
|
const redirectUri = buildFrontendOAuthRedirectUri(appOrigin);
|
|
78
80
|
|
|
79
81
|
const authorizeUrl = new URL(hubspotConnectEnv.hubspotAuthorizationEndpoint);
|
|
@@ -11,7 +11,10 @@ import { parseCookies } from '../../utils/cookie-utils.ts';
|
|
|
11
11
|
import { serializeCookie, setResponseCookie } from '../utils/cookie-utils.ts';
|
|
12
12
|
import { buildClientAssertion } from './oauth-client.ts';
|
|
13
13
|
import type { HubSpotConnectOAuthRouteOptions } from './types.ts';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
buildCimdClientIdUrlFromRequest,
|
|
16
|
+
parseAppOriginHeader,
|
|
17
|
+
} from './utils.ts';
|
|
15
18
|
|
|
16
19
|
async function revokeToken(options: {
|
|
17
20
|
revokeEndpointUrl: string;
|
|
@@ -47,15 +50,23 @@ export async function handleAuthLogout(
|
|
|
47
50
|
const cookies = parseCookies(c.req.header('Cookie'));
|
|
48
51
|
const accessToken = cookies[HUBSPOT_ACCESS_TOKEN_COOKIE_NAME];
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
let clientId: string;
|
|
54
|
+
if (hubspotConnectEnv.isCimdEnabled) {
|
|
55
|
+
const appOrigin = parseAppOriginHeader(c.req.header('Origin'));
|
|
56
|
+
if (!appOrigin) {
|
|
57
|
+
return c.json({ error: 'Missing or invalid Origin header' }, 400);
|
|
58
|
+
}
|
|
59
|
+
clientId = buildCimdClientIdUrlFromRequest({
|
|
60
|
+
requestUrl: c.req.url,
|
|
61
|
+
basePath,
|
|
62
|
+
xForwardedProto,
|
|
63
|
+
xForwardedHost,
|
|
64
|
+
requestHostHeader,
|
|
65
|
+
appOrigin,
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
clientId = hubspotConnectEnv.hubspotClientId;
|
|
69
|
+
}
|
|
59
70
|
|
|
60
71
|
const revokeEndpointUrl = new URL(
|
|
61
72
|
'/oauth/v1/revoke',
|
|
@@ -20,6 +20,7 @@ import type { HubSpotConnectOAuthRouteOptions } from './types.ts';
|
|
|
20
20
|
import {
|
|
21
21
|
buildCimdClientIdUrlFromRequest,
|
|
22
22
|
isPositiveFiniteNumber,
|
|
23
|
+
parseAppOriginHeader,
|
|
23
24
|
} from './utils.ts';
|
|
24
25
|
|
|
25
26
|
export async function handleAuthRefresh(
|
|
@@ -53,15 +54,23 @@ export async function handleAuthRefresh(
|
|
|
53
54
|
return c.json({ error: 'Missing refresh token' }, 401);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
let clientId: string;
|
|
58
|
+
if (hubspotConnectEnv.isCimdEnabled) {
|
|
59
|
+
const appOrigin = parseAppOriginHeader(c.req.header('Origin'));
|
|
60
|
+
if (!appOrigin) {
|
|
61
|
+
return c.json({ error: 'Missing or invalid Origin header' }, 400);
|
|
62
|
+
}
|
|
63
|
+
clientId = buildCimdClientIdUrlFromRequest({
|
|
64
|
+
requestUrl: c.req.url,
|
|
65
|
+
basePath,
|
|
66
|
+
xForwardedProto,
|
|
67
|
+
xForwardedHost,
|
|
68
|
+
requestHostHeader,
|
|
69
|
+
appOrigin,
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
clientId = hubspotConnectEnv.hubspotClientId;
|
|
73
|
+
}
|
|
65
74
|
|
|
66
75
|
const tokenEndpointUrl = new URL(
|
|
67
76
|
'/oauth/v1/token',
|