@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.
Files changed (199) hide show
  1. package/.turbo/turbo-format$colon$check.log +1 -1
  2. package/.turbo/turbo-test.log +81 -50
  3. package/.turbo/turbo-tsdown.log +119 -119
  4. package/build/tsconfig.browser.tsbuildinfo +1 -1
  5. package/build/tsconfig.server.tsbuildinfo +1 -1
  6. package/dist/browser/{HubSpotAppConnect-COQgPrFn.js → HubSpotAppConnect-DFe9b90e.js} +1 -2
  7. package/dist/browser/HubSpotAppConnect-DFe9b90e.js.map +1 -0
  8. package/dist/browser/create-crdncXsh.js.map +1 -1
  9. package/dist/browser/react/lovable.d.ts +1 -2
  10. package/dist/browser/react/lovable.js +1 -1
  11. package/dist/browser/react/lovable.js.map +1 -1
  12. package/dist/browser/react.d.ts +1 -2
  13. package/dist/browser/react.js +1 -1
  14. package/dist/server/api-client-core/apis/account/account-info.generated.js.map +1 -1
  15. package/dist/server/api-client-core/apis/account/audit-logs.generated.js.map +1 -1
  16. package/dist/server/api-client-core/apis/auth/oauth.generated.js.map +1 -1
  17. package/dist/server/api-client-core/apis/automation/actions.generated.js.map +1 -1
  18. package/dist/server/api-client-core/apis/automation/sequences.generated.js.map +1 -1
  19. package/dist/server/api-client-core/apis/business-units.generated.js.map +1 -1
  20. package/dist/server/api-client-core/apis/cms/authors.generated.js.map +1 -1
  21. package/dist/server/api-client-core/apis/cms/blog-settings.generated.js.map +1 -1
  22. package/dist/server/api-client-core/apis/cms/cms-content-audit.generated.js.map +1 -1
  23. package/dist/server/api-client-core/apis/cms/domains.generated.js.map +1 -1
  24. package/dist/server/api-client-core/apis/cms/hubdb.generated.js.map +1 -1
  25. package/dist/server/api-client-core/apis/cms/media-bridge.generated.js.map +1 -1
  26. package/dist/server/api-client-core/apis/cms/pages.generated.js.map +1 -1
  27. package/dist/server/api-client-core/apis/cms/posts.generated.js.map +1 -1
  28. package/dist/server/api-client-core/apis/cms/site-search.generated.js.map +1 -1
  29. package/dist/server/api-client-core/apis/cms/source-code.generated.js.map +1 -1
  30. package/dist/server/api-client-core/apis/cms/tags.generated.js.map +1 -1
  31. package/dist/server/api-client-core/apis/cms/url-mappings.generated.js.map +1 -1
  32. package/dist/server/api-client-core/apis/cms/url-redirects.generated.js.map +1 -1
  33. package/dist/server/api-client-core/apis/communication-preferences/subscriptions.generated.js.map +1 -1
  34. package/dist/server/api-client-core/apis/conversations/custom-channels.generated.js.map +1 -1
  35. package/dist/server/api-client-core/apis/conversations/visitor-identification.generated.js.map +1 -1
  36. package/dist/server/api-client-core/apis/conversations.generated.js.map +1 -1
  37. package/dist/server/api-client-core/apis/crm/app-uninstalls.generated.js.map +1 -1
  38. package/dist/server/api-client-core/apis/crm/appointments.generated.js.map +1 -1
  39. package/dist/server/api-client-core/apis/crm/associations-schema.generated.js.map +1 -1
  40. package/dist/server/api-client-core/apis/crm/associations.generated.js.map +1 -1
  41. package/dist/server/api-client-core/apis/crm/calling-extensions.generated.js.map +1 -1
  42. package/dist/server/api-client-core/apis/crm/calls.generated.js.map +1 -1
  43. package/dist/server/api-client-core/apis/crm/carts.generated.js.map +1 -1
  44. package/dist/server/api-client-core/apis/crm/commerce-payments.generated.js.map +1 -1
  45. package/dist/server/api-client-core/apis/crm/commerce-subscriptions.generated.js.map +1 -1
  46. package/dist/server/api-client-core/apis/crm/communications.generated.js.map +1 -1
  47. package/dist/server/api-client-core/apis/crm/companies.generated.js.map +1 -1
  48. package/dist/server/api-client-core/apis/crm/contacts.generated.js.map +1 -1
  49. package/dist/server/api-client-core/apis/crm/contracts.generated.js.map +1 -1
  50. package/dist/server/api-client-core/apis/crm/courses.generated.js.map +1 -1
  51. package/dist/server/api-client-core/apis/crm/crm-owners.generated.js.map +1 -1
  52. package/dist/server/api-client-core/apis/crm/custom-objects.generated.js.map +1 -1
  53. package/dist/server/api-client-core/apis/crm/deal-splits.generated.js.map +1 -1
  54. package/dist/server/api-client-core/apis/crm/deals.generated.js.map +1 -1
  55. package/dist/server/api-client-core/apis/crm/discounts.generated.js.map +1 -1
  56. package/dist/server/api-client-core/apis/crm/emails.generated.js.map +1 -1
  57. package/dist/server/api-client-core/apis/crm/exports.generated.js.map +1 -1
  58. package/dist/server/api-client-core/apis/crm/feedback-submissions.generated.js.map +1 -1
  59. package/dist/server/api-client-core/apis/crm/fees.generated.js.map +1 -1
  60. package/dist/server/api-client-core/apis/crm/goal-targets.generated.js.map +1 -1
  61. package/dist/server/api-client-core/apis/crm/imports.generated.js.map +1 -1
  62. package/dist/server/api-client-core/apis/crm/invoices.generated.js.map +1 -1
  63. package/dist/server/api-client-core/apis/crm/leads.generated.js.map +1 -1
  64. package/dist/server/api-client-core/apis/crm/limits-tracking.generated.js.map +1 -1
  65. package/dist/server/api-client-core/apis/crm/line-items.generated.js.map +1 -1
  66. package/dist/server/api-client-core/apis/crm/listings.generated.js.map +1 -1
  67. package/dist/server/api-client-core/apis/crm/lists.generated.js.map +1 -1
  68. package/dist/server/api-client-core/apis/crm/meetings.generated.js.map +1 -1
  69. package/dist/server/api-client-core/apis/crm/notes.generated.js.map +1 -1
  70. package/dist/server/api-client-core/apis/crm/object-library.generated.js.map +1 -1
  71. package/dist/server/api-client-core/apis/crm/objects.generated.js.map +1 -1
  72. package/dist/server/api-client-core/apis/crm/orders.generated.js.map +1 -1
  73. package/dist/server/api-client-core/apis/crm/partner-clients.generated.js.map +1 -1
  74. package/dist/server/api-client-core/apis/crm/partner-services.generated.js.map +1 -1
  75. package/dist/server/api-client-core/apis/crm/pipelines.generated.js.map +1 -1
  76. package/dist/server/api-client-core/apis/crm/postal-mail.generated.js.map +1 -1
  77. package/dist/server/api-client-core/apis/crm/products.generated.js.map +1 -1
  78. package/dist/server/api-client-core/apis/crm/projects.generated.js.map +1 -1
  79. package/dist/server/api-client-core/apis/crm/properties.generated.js.map +1 -1
  80. package/dist/server/api-client-core/apis/crm/property-validations.generated.js.map +1 -1
  81. package/dist/server/api-client-core/apis/crm/public-app-crm-cards.generated.js.map +1 -1
  82. package/dist/server/api-client-core/apis/crm/public-app-feature-flags.generated.js.map +1 -1
  83. package/dist/server/api-client-core/apis/crm/quotes.generated.js.map +1 -1
  84. package/dist/server/api-client-core/apis/crm/schemas.generated.js.map +1 -1
  85. package/dist/server/api-client-core/apis/crm/services.generated.js.map +1 -1
  86. package/dist/server/api-client-core/apis/crm/tasks.generated.js.map +1 -1
  87. package/dist/server/api-client-core/apis/crm/taxes.generated.js.map +1 -1
  88. package/dist/server/api-client-core/apis/crm/tickets.generated.js.map +1 -1
  89. package/dist/server/api-client-core/apis/crm/timeline.generated.js.map +1 -1
  90. package/dist/server/api-client-core/apis/crm/transcriptions.generated.js.map +1 -1
  91. package/dist/server/api-client-core/apis/crm/users.generated.js.map +1 -1
  92. package/dist/server/api-client-core/apis/crm/video-conferencing-extension.generated.js.map +1 -1
  93. package/dist/server/api-client-core/apis/events/manage-event-definitions.generated.js.map +1 -1
  94. package/dist/server/api-client-core/apis/events/send-event-completions.generated.js.map +1 -1
  95. package/dist/server/api-client-core/apis/events.generated.js.map +1 -1
  96. package/dist/server/api-client-core/apis/files.generated.js.map +1 -1
  97. package/dist/server/api-client-core/apis/marketing/campaigns-public-api.generated.js.map +1 -1
  98. package/dist/server/api-client-core/apis/marketing/marketing-emails.generated.js.map +1 -1
  99. package/dist/server/api-client-core/apis/marketing/marketing-events.generated.js.map +1 -1
  100. package/dist/server/api-client-core/apis/marketing/single-send.generated.js.map +1 -1
  101. package/dist/server/api-client-core/apis/marketing/transactional-single-send.generated.js.map +1 -1
  102. package/dist/server/api-client-core/apis/meta/origins.generated.js.map +1 -1
  103. package/dist/server/api-client-core/apis/scheduler/meetings.generated.js.map +1 -1
  104. package/dist/server/api-client-core/apis/settings/multicurrency.generated.js.map +1 -1
  105. package/dist/server/api-client-core/apis/settings/tax-rates.generated.js.map +1 -1
  106. package/dist/server/api-client-core/apis/settings/user-provisioning.generated.js.map +1 -1
  107. package/dist/server/api-client-core/apis/webhooks-journal.generated.js.map +1 -1
  108. package/dist/server/api-client-core/apis/webhooks.generated.js.map +1 -1
  109. package/dist/server/api-client-core/binary-data.js.map +1 -1
  110. package/dist/server/api-client-core/client.js +5 -1
  111. package/dist/server/api-client-core/client.js.map +1 -1
  112. package/dist/server/api-client-core/codegen-helpers/file-op-wrappers.js.map +1 -1
  113. package/dist/server/api-client-core/errors.js.map +1 -1
  114. package/dist/server/api-client-core/op.js.map +1 -1
  115. package/dist/server/api-client-core/pagination.js.map +1 -1
  116. package/dist/server/api-client-core/plugins/fetch-transport.js +28 -8
  117. package/dist/server/api-client-core/plugins/fetch-transport.js.map +1 -1
  118. package/dist/server/constants.js.map +1 -1
  119. package/dist/server/deno/start.js.map +1 -1
  120. package/dist/server/hono/hono-request-handler.js +21 -17
  121. package/dist/server/hono/hono-request-handler.js.map +1 -1
  122. package/dist/server/hono/hubspot-connect-routes/auth-complete.js +2 -1
  123. package/dist/server/hono/hubspot-connect-routes/auth-complete.js.map +1 -1
  124. package/dist/server/hono/hubspot-connect-routes/auth-init-session.js +3 -1
  125. package/dist/server/hono/hubspot-connect-routes/auth-init-session.js.map +1 -1
  126. package/dist/server/hono/hubspot-connect-routes/auth-logout.js +14 -8
  127. package/dist/server/hono/hubspot-connect-routes/auth-logout.js.map +1 -1
  128. package/dist/server/hono/hubspot-connect-routes/auth-refresh.js +26 -18
  129. package/dist/server/hono/hubspot-connect-routes/auth-refresh.js.map +1 -1
  130. package/dist/server/hono/hubspot-connect-routes/cimd-client-metadata-types.js.map +1 -1
  131. package/dist/server/hono/hubspot-connect-routes/cimd-public-routes.js +4 -1
  132. package/dist/server/hono/hubspot-connect-routes/cimd-public-routes.js.map +1 -1
  133. package/dist/server/hono/hubspot-connect-routes/fetch-hubspot-client-metadata.js.map +1 -1
  134. package/dist/server/hono/hubspot-connect-routes/hubspot-connect-routes.js.map +1 -1
  135. package/dist/server/hono/hubspot-connect-routes/load-hubspot-connect-routes-env.js.map +1 -1
  136. package/dist/server/hono/hubspot-connect-routes/oauth-client.js.map +1 -1
  137. package/dist/server/hono/hubspot-connect-routes/utils.js +5 -5
  138. package/dist/server/hono/hubspot-connect-routes/utils.js.map +1 -1
  139. package/dist/server/hono/types.d.ts +0 -5
  140. package/dist/server/hono/utils/cookie-utils.js.map +1 -1
  141. package/dist/server/hono/utils/cors-middleware.js.map +1 -1
  142. package/dist/server/import-app-keys.js.map +1 -1
  143. package/dist/server/lovable/create-app-function-start.d.ts +1 -1
  144. package/dist/server/lovable/create-app-function-start.js +4 -5
  145. package/dist/server/lovable/create-app-function-start.js.map +1 -1
  146. package/dist/server/lovable/hubspot-connect/index.js.map +1 -1
  147. package/dist/server/lovable/hubspot-connect/run-hubspot-connect-lovable-server.js +11 -11
  148. package/dist/server/lovable/hubspot-connect/run-hubspot-connect-lovable-server.js.map +1 -1
  149. package/dist/server/sanitize-request.js +0 -11
  150. package/dist/server/sanitize-request.js.map +1 -1
  151. package/dist/server/secure-start-core.js.map +1 -1
  152. package/dist/server/shared/constants.js +2 -2
  153. package/dist/server/shared/constants.js.map +1 -1
  154. package/dist/server/shared/encoding/base64.js.map +1 -1
  155. package/dist/server/shared/encoding/sha256.js.map +1 -1
  156. package/dist/server/shared/logger.js.map +1 -1
  157. package/dist/server/types.d.ts +1 -35
  158. package/dist/server/utils/cookie-utils.js.map +1 -1
  159. package/dist/server/utils/dpop-utils.js.map +1 -1
  160. package/dist/server/utils/env-utils.js +1 -1
  161. package/dist/server/utils/env-utils.js.map +1 -1
  162. package/dist/server/utils/hubspot-dpop-auth-headers.js +38 -0
  163. package/dist/server/utils/hubspot-dpop-auth-headers.js.map +1 -0
  164. package/dist/server/utils/jwk-utils.js.map +1 -1
  165. package/dist/server/utils/jwt-utils.js.map +1 -1
  166. package/package.json +16 -16
  167. package/src/browser/app-connect-controller/init.test.ts +4 -4
  168. package/src/browser/app-connect-controller/init.ts +2 -2
  169. package/src/browser/react/components/ConnectButton/ConnectButton.tsx +0 -1
  170. package/src/server/api-client-core/client.ts +5 -1
  171. package/src/server/api-client-core/plugins/fetch-transport.test.ts +114 -0
  172. package/src/server/api-client-core/plugins/fetch-transport.ts +65 -11
  173. package/src/server/api-client-core/plugins/index.ts +1 -0
  174. package/src/server/hono/hono-request-handler.ts +33 -19
  175. package/src/server/hono/hubspot-connect-routes/auth-complete.test.ts +2 -2
  176. package/src/server/hono/hubspot-connect-routes/auth-complete.ts +1 -0
  177. package/src/server/hono/hubspot-connect-routes/auth-init-session.test.ts +2 -2
  178. package/src/server/hono/hubspot-connect-routes/auth-init-session.ts +2 -0
  179. package/src/server/hono/hubspot-connect-routes/auth-logout.ts +21 -10
  180. package/src/server/hono/hubspot-connect-routes/auth-refresh.ts +18 -9
  181. package/src/server/hono/hubspot-connect-routes/cimd-public-routes.test.ts +7 -6
  182. package/src/server/hono/hubspot-connect-routes/cimd-public-routes.ts +5 -1
  183. package/src/server/hono/hubspot-connect-routes/hubspot-connect-routes.ts +2 -1
  184. package/src/server/hono/hubspot-connect-routes/utils.test.ts +16 -46
  185. package/src/server/hono/hubspot-connect-routes/utils.ts +6 -6
  186. package/src/server/hono/types.ts +0 -5
  187. package/src/server/lovable/create-app-function-start.ts +4 -4
  188. package/src/server/lovable/hubspot-connect/run-hubspot-connect-lovable-server.ts +12 -12
  189. package/src/server/sanitize-request.ts +1 -12
  190. package/src/server/types.ts +0 -36
  191. package/src/server/utils/env-utils.ts +1 -1
  192. package/src/server/utils/hubspot-dpop-auth-headers.test.ts +43 -0
  193. package/src/server/utils/hubspot-dpop-auth-headers.ts +48 -0
  194. package/src/shared/constants.ts +1 -1
  195. package/dist/browser/HubSpotAppConnect-COQgPrFn.js.map +0 -1
  196. package/dist/server/proxy.js +0 -68
  197. package/dist/server/proxy.js.map +0 -1
  198. package/src/server/proxy.test.ts +0 -80
  199. 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,aAAa,CAAC,OAAO,KAAK,UAAU,OAAO,CAAC,CAKb,CAAC,GAHf,UACrB,IAAI,aAAa,CAAC,OAAO,KAAK,UAAU,QAAQ,CAAC,CAEI;CACvD,MAAM,kBAAkB,MAAM,OAAO,OAAO,KAC1C;EAAE,MAAM;EAAS,MAAM;EAAW,EAClC,YACA,IAAI,aAAa,CAAC,OAAO,aAAa,CACvC;CACD,OAAO,GAAG,aAAa,GAAG,UAAU,IAAI,WAAW,gBAAgB,CAAC;;AAGtE,eAAe,gBAAgB,KAAqC;CAClE,OAAO,OAAO,OAAO,UACnB,OACA,KACA;EAAE,MAAM;EAAS,YAAY;EAAS,EACtC,OACA,CAAC,SAAS,CACX;;;;;;;;;;;AAiBH,eAAsB,mBACpB,SACkC;CAClC,MAAM,EAAE,OAAO,iBAAiB;CAChC,MAAM,QAAQ,MAAM,MAAM,IAAI;CAC9B,IAAI,MAAM,WAAW,GAAG,MAAM,IAAI,MAAM,qBAAqB;CAC7D,MAAM,CAAC,eAAe,gBAAgB,oBAAoB;CAK1D,MAAM,YAAY,MAAM,gBAAgB,aAAa;CAOrD,IAAI,CAAC,MANe,OAAO,OAAO,OAChC;EAAE,MAAM;EAAS,MAAM;EAAW,EAClC,WACA,gBAAgB,iBAAiB,EACjC,IAAI,aAAa,CAAC,OAAO,GAAG,cAAc,GAAG,iBAAiB,CAC/D,EACW,MAAM,IAAI,MAAM,oCAAoC;CAChE,OAAO,KAAK,MACV,IAAI,aAAa,CAAC,OAAO,gBAAgB,eAAe,CAAC,CAC1D;;;;;;;;AAgBH,eAAsB,UACpB,SACkC;CAClC,MAAM,EAAE,OAAO,iBAAiB;CAChC,MAAM,UAAU,MAAM,mBAAmB;EAAE;EAAO;EAAc,CAAC;CACjE,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;CACzC,IAAI,OAAO,QAAQ,WAAW,YAAY,QAAQ,SAAS,KACzD,MAAM,IAAI,MAAM,cAAc;CAEhC,OAAO;;;;;;;AAyBT,eAAsB,QAAQ,SAA0C;CACtE,MAAM,EAAE,YAAY,SAAS,eAAe;CAC5C,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;CAKzC,OAAO,iBAAiB;EACtB,QAAQ;GAAE,KAAK;GAAS,KAAK;GAAO;EACpC,SALA,eAAe,KAAA,IACX;GAAE,KAAK;GAAK,KAAK,MAAM;GAAY,GAAG;GAAS,GAC/C;GAAE,KAAK;GAAK,GAAG;GAAS;EAI5B;EACD,CAAC"}
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.11",
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-core.js",
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": "^1.4.1",
30
- "hono": "^4.7.11"
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": "^2.5.0",
37
- "@types/node": "25.6.0",
38
- "@types/react": "^19.1.0",
39
- "@vanilla-extract/css": "^1.20.1",
40
- "@vanilla-extract/rollup-plugin": "^1.5.3",
41
- "eslint": "10.0.3",
42
- "prettier": "3.8.1",
43
- "react": "^19.1.0",
44
- "tsdown": "0.22.0-beta.3",
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.0.18",
46
+ "vitest": "4.1.6",
47
47
  "@private/eslint-config": "0.1.0",
48
- "@private/prettier-config": "0.1.0",
49
- "@private/tsconfig": "0.1.0"
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 { HUBSPOT_FRONTEND_CALLBACK_PATH } from '../../shared/constants.ts';
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${HUBSPOT_FRONTEND_CALLBACK_PATH}?code=auth-code&state=auth-state`,
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${HUBSPOT_FRONTEND_CALLBACK_PATH}?code=auth-code&state=bad-state`,
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${HUBSPOT_FRONTEND_CALLBACK_PATH}`,
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
- HUBSPOT_FRONTEND_CALLBACK_PATH,
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 !== HUBSPOT_FRONTEND_CALLBACK_PATH) return;
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
- throw new HubSpotApiError(response);
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 BASE_URL = 'https://api.hubapi.com';
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
- getAccessToken: () => string;
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 url = `${BASE_URL}${operation.path}`;
80
+ let targetUrl = `${endpoint}${operation.path}`;
44
81
  if (operation.pathParams) {
45
82
  for (const [key, value] of Object.entries(operation.pathParams)) {
46
- url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
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) url += `?${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
- Authorization: `Bearer ${options.getAccessToken()}`,
114
+ ...authHeaders,
61
115
  };
62
116
 
63
117
  const init: RequestInit = {
64
- method: operation.method.toUpperCase(),
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
- const response = await fetch(url, init);
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
- // 204 No Content responses have no body to parse
114
- bodyJson: response.status === 204 ? undefined : await response.json(),
168
+ bodyJson: await readResponseBodyJson(response),
115
169
  };
116
170
  });
117
171
  },
@@ -5,6 +5,7 @@
5
5
  * middleware plugins that can be passed to {@link createHubSpotClient}.
6
6
  */
7
7
  export {
8
+ type FetchTransportHeaderContext,
8
9
  type FetchTransportPluginOptions,
9
10
  fetchTransportPlugin,
10
11
  } from './fetch-transport.ts';
@@ -1,16 +1,20 @@
1
1
  import { Hono } from 'hono';
2
2
 
3
- import { noopLogger, type Logger } from '../../shared/logger.ts';
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, UserCredentials } from '../types.ts';
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 `hubSpotProxy` on the Hono context so route handlers
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, logger = noopLogger } = options;
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
- getAccessToken: () => {
112
- if (!accessToken) {
113
- throw new Error('Missing access token');
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
- return accessToken;
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: proxy.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 { HUBSPOT_FRONTEND_CALLBACK_PATH } from '../../../shared/constants.ts';
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}${HUBSPOT_FRONTEND_CALLBACK_PATH}`
219
+ `${APP_ORIGIN}${OAUTH_CALLBACK_PATH}`
220
220
  );
221
221
  expect(formParams.get('grant_type')).toBe('authorization_code');
222
222
  });
@@ -135,6 +135,7 @@ export async function handleAuthComplete(
135
135
  xForwardedProto,
136
136
  xForwardedHost,
137
137
  requestHostHeader,
138
+ appOrigin,
138
139
  })
139
140
  : hubspotConnectEnv.hubspotClientId;
140
141
 
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
3
 
4
- import { HUBSPOT_FRONTEND_CALLBACK_PATH } from '../../../shared/constants.ts';
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}${HUBSPOT_FRONTEND_CALLBACK_PATH}`
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 { buildCimdClientIdUrlFromRequest } from './utils.ts';
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
- const clientId = hubspotConnectEnv.isCimdEnabled
51
- ? buildCimdClientIdUrlFromRequest({
52
- requestUrl: c.req.url,
53
- basePath,
54
- xForwardedProto,
55
- xForwardedHost,
56
- requestHostHeader,
57
- })
58
- : hubspotConnectEnv.hubspotClientId;
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
- const clientId = hubspotConnectEnv.isCimdEnabled
57
- ? buildCimdClientIdUrlFromRequest({
58
- requestUrl: c.req.url,
59
- basePath,
60
- xForwardedProto,
61
- xForwardedHost,
62
- requestHostHeader,
63
- })
64
- : hubspotConnectEnv.hubspotClientId;
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',