@hubspot/app-connect-sdk 1.0.0-alpha.2 → 1.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-format$colon$check.log +1 -1
- package/.turbo/turbo-test.log +60 -58
- package/.turbo/turbo-tsdown.log +55 -52
- package/build/tsconfig.browser.tsbuildinfo +1 -1
- package/build/tsconfig.server.tsbuildinfo +1 -1
- package/dist/browser/{HubSpotAppConnect-BW45gyDs.js → HubSpotAppConnect-COQgPrFn.js} +5 -3
- package/dist/browser/HubSpotAppConnect-COQgPrFn.js.map +1 -0
- package/dist/browser/{create-vctOhpX9.js → create-crdncXsh.js} +53 -24
- package/dist/browser/create-crdncXsh.js.map +1 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/react/lovable.js +2 -2
- package/dist/browser/react.js +1 -1
- package/dist/server/api-client-core/plugins/fetch-transport.js +5 -1
- package/dist/server/api-client-core/plugins/fetch-transport.js.map +1 -1
- package/dist/server/constants.js +33 -6
- package/dist/server/constants.js.map +1 -1
- package/dist/server/hono/hono-request-handler.js +18 -13
- package/dist/server/hono/hono-request-handler.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-complete.js +154 -0
- package/dist/server/hono/hubspot-connect-routes/auth-complete.js.map +1 -0
- package/dist/server/hono/hubspot-connect-routes/auth-init-session.js +22 -11
- package/dist/server/hono/hubspot-connect-routes/auth-init-session.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-logout.js +18 -1
- package/dist/server/hono/hubspot-connect-routes/auth-logout.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/auth-refresh.js +6 -0
- package/dist/server/hono/hubspot-connect-routes/auth-refresh.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/hubspot-connect-routes.js +4 -2
- package/dist/server/hono/hubspot-connect-routes/hubspot-connect-routes.js.map +1 -1
- package/dist/server/hono/hubspot-connect-routes/utils.js +50 -3
- package/dist/server/hono/hubspot-connect-routes/utils.js.map +1 -1
- package/dist/server/hono/types.d.ts +13 -9
- package/dist/server/hono/utils/cookie-utils.js +2 -1
- package/dist/server/hono/utils/cookie-utils.js.map +1 -1
- package/dist/server/hono/utils/cors-middleware.js +85 -0
- package/dist/server/hono/utils/cors-middleware.js.map +1 -0
- package/dist/server/sanitize-request.js +24 -10
- package/dist/server/sanitize-request.js.map +1 -1
- package/dist/server/shared/constants.js +22 -9
- package/dist/server/shared/constants.js.map +1 -1
- package/package.json +3 -3
- package/src/browser/app-connect-controller/init.test.ts +167 -0
- package/src/browser/app-connect-controller/init.ts +70 -19
- package/src/browser/react/components/AppConnectHeader/AppConnectHeader.tsx +3 -5
- package/src/browser/react/components/ConnectButton/ConnectButton.tsx +2 -1
- package/src/server/api-client-core/plugins/fetch-transport.ts +5 -1
- package/src/server/constants.ts +29 -4
- package/src/server/hono/hono-request-handler.ts +42 -15
- package/src/server/hono/hubspot-connect-routes/auth-complete.test.ts +285 -0
- package/src/server/hono/hubspot-connect-routes/{auth-callback.ts → auth-complete.ts} +73 -30
- package/src/server/hono/hubspot-connect-routes/auth-init-session.test.ts +114 -30
- package/src/server/hono/hubspot-connect-routes/auth-init-session.ts +33 -10
- package/src/server/hono/hubspot-connect-routes/auth-logout.test.ts +13 -0
- package/src/server/hono/hubspot-connect-routes/auth-logout.ts +18 -0
- package/src/server/hono/hubspot-connect-routes/auth-refresh.test.ts +6 -0
- package/src/server/hono/hubspot-connect-routes/auth-refresh.ts +6 -0
- package/src/server/hono/hubspot-connect-routes/hubspot-connect-routes.ts +9 -2
- package/src/server/hono/hubspot-connect-routes/utils.ts +57 -1
- package/src/server/hono/types.ts +15 -9
- package/src/server/hono/utils/cookie-utils.ts +27 -2
- package/src/server/hono/utils/cors-middleware.test.ts +79 -0
- package/src/server/hono/utils/cors-middleware.ts +95 -0
- package/src/server/sanitize-request.ts +25 -11
- package/src/server/types.ts +2 -2
- package/src/shared/constants.ts +31 -3
- package/src/shared/wire-types.ts +19 -0
- package/dist/browser/HubSpotAppConnect-BW45gyDs.js.map +0 -1
- package/dist/browser/create-vctOhpX9.js.map +0 -1
- package/dist/server/hono/hubspot-connect-routes/auth-callback.js +0 -125
- package/dist/server/hono/hubspot-connect-routes/auth-callback.js.map +0 -1
- package/src/server/hono/hubspot-connect-routes/auth-callback.test.ts +0 -225
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { base64urlDecode } from "../../shared/encoding/base64.js";
|
|
2
|
+
import { HUBSPOT_ACCESS_TOKEN_COOKIE_NAME, HUBSPOT_APP_ORIGIN_COOKIE_NAME, HUBSPOT_REFRESH_COOKIE_PREFIX, TEMP_COOKIE_OAUTH_STATE, TEMP_COOKIE_PKCE_VERIFIER } from "../../constants.js";
|
|
3
|
+
import { parseCookies } from "../../utils/cookie-utils.js";
|
|
4
|
+
import { AUTH_COMPLETE_CODE_PARAM, AUTH_COMPLETE_STATE_PARAM } from "../../shared/constants.js";
|
|
5
|
+
import { serializeCookie, setResponseCookie } from "../utils/cookie-utils.js";
|
|
6
|
+
import { REFRESH_COOKIE_MAX_AGE_SEC } from "./constants.js";
|
|
7
|
+
import { buildClientAssertion, buildClientAssertionFormParams, buildClientSecretFormParams, buildTokenEndpointDpopProof, requestOAuthToken } from "./oauth-client.js";
|
|
8
|
+
import { buildCimdClientIdUrlFromRequest, buildFrontendOAuthRedirectUri, clearTempCookie, isPositiveFiniteNumber, isSafeReturnPath, parseAppOriginHeader } from "./utils.js";
|
|
9
|
+
//#region src/server/hono/hubspot-connect-routes/auth-complete.ts
|
|
10
|
+
/**
|
|
11
|
+
* Cross-origin OAuth completion endpoint.
|
|
12
|
+
*
|
|
13
|
+
* Called from the React app on the frontend OAuth callback path
|
|
14
|
+
* (`HUBSPOT_FRONTEND_CALLBACK_PATH`) once HubSpot has redirected the
|
|
15
|
+
* browser back with `?code` + `?state`. The browser POSTs both
|
|
16
|
+
* values here as a credentialed cross-site fetch — same partition as
|
|
17
|
+
* `init-session`, so the temp PKCE/state cookies are visible — and
|
|
18
|
+
* the SDK:
|
|
19
|
+
*
|
|
20
|
+
* 1. Validates `state` against the temp `__hs_oauth_state` cookie.
|
|
21
|
+
* 2. Pulls the PKCE verifier from `__hs_pkce_verifier`.
|
|
22
|
+
* 3. Rebuilds the same `redirect_uri` it sent to HubSpot during
|
|
23
|
+
* `init-session` (frontend origin + the fixed callback path);
|
|
24
|
+
* the OAuth token endpoint requires the two values to match.
|
|
25
|
+
* 4. Exchanges `code` for an access + refresh token (with DPoP /
|
|
26
|
+
* CIMD client-assertion when enabled).
|
|
27
|
+
* 5. Sets the durable session cookies (access token, refresh) with
|
|
28
|
+
* `SameSite=None; Secure; Partitioned` so they live in the
|
|
29
|
+
* `(frontend, edge)` partition where subsequent API fetches will
|
|
30
|
+
* read them.
|
|
31
|
+
* 6. Clears the temp cookies.
|
|
32
|
+
* 7. Returns `{ expires_at, return_path }` so the controller can
|
|
33
|
+
* update its session-storage expiry tracking and navigate back to
|
|
34
|
+
* the page the user started the connect flow from.
|
|
35
|
+
*/
|
|
36
|
+
async function handleAuthComplete(c, options) {
|
|
37
|
+
const { appKeys, refreshCookiePath, hubspotConnectEnv } = options;
|
|
38
|
+
const xForwardedProto = c.req.header("x-forwarded-proto") ?? void 0;
|
|
39
|
+
const xForwardedHost = c.req.header("x-forwarded-host") ?? void 0;
|
|
40
|
+
const requestHostHeader = c.req.header("host") ?? void 0;
|
|
41
|
+
const code = c.req.query(AUTH_COMPLETE_CODE_PARAM);
|
|
42
|
+
const state = c.req.query(AUTH_COMPLETE_STATE_PARAM);
|
|
43
|
+
if (!code || !state) return c.json({ error: "Missing code or state" }, 400);
|
|
44
|
+
if (hubspotConnectEnv.isAppPrivateKeyRequired && !appKeys) return c.json({ error: "Server misconfiguration: HUBSPOT_APP_PRIVATE_KEY is required when CIMD or DPoP is enabled" }, 500);
|
|
45
|
+
const cookies = parseCookies(c.req.header("Cookie"));
|
|
46
|
+
const expectedState = cookies[TEMP_COOKIE_OAUTH_STATE];
|
|
47
|
+
const codeVerifier = cookies[TEMP_COOKIE_PKCE_VERIFIER];
|
|
48
|
+
const appOriginCookie = cookies[HUBSPOT_APP_ORIGIN_COOKIE_NAME];
|
|
49
|
+
if (!expectedState || state !== decodeURIComponent(expectedState)) return c.json({ error: "State mismatch" }, 403);
|
|
50
|
+
if (!codeVerifier) return c.json({ error: "Missing PKCE verifier" }, 400);
|
|
51
|
+
const appOrigin = parseAppOriginHeader(appOriginCookie);
|
|
52
|
+
if (!appOrigin) return c.json({ error: "Missing app origin cookie" }, 400);
|
|
53
|
+
let statePayload;
|
|
54
|
+
try {
|
|
55
|
+
statePayload = JSON.parse(new TextDecoder().decode(base64urlDecode(decodeURIComponent(state))));
|
|
56
|
+
} catch {
|
|
57
|
+
return c.json({ error: "Malformed state value" }, 400);
|
|
58
|
+
}
|
|
59
|
+
const returnPath = statePayload.return_path;
|
|
60
|
+
if (!returnPath || !isSafeReturnPath(returnPath)) return c.json({ error: "Invalid return path in state" }, 400);
|
|
61
|
+
const sessionId = statePayload.sid;
|
|
62
|
+
if (!sessionId) return c.json({ error: "Missing app session cookie" }, 400);
|
|
63
|
+
const decodedCodeVerifier = decodeURIComponent(codeVerifier);
|
|
64
|
+
const clientId = hubspotConnectEnv.isCimdEnabled ? buildCimdClientIdUrlFromRequest({
|
|
65
|
+
requestUrl: c.req.url,
|
|
66
|
+
basePath: options.basePath,
|
|
67
|
+
xForwardedProto,
|
|
68
|
+
xForwardedHost,
|
|
69
|
+
requestHostHeader
|
|
70
|
+
}) : hubspotConnectEnv.hubspotClientId;
|
|
71
|
+
const redirectUri = buildFrontendOAuthRedirectUri(appOrigin);
|
|
72
|
+
const tokenEndpointUrl = new URL("/oauth/v1/token", hubspotConnectEnv.hubspotOAuthApiOrigin).href;
|
|
73
|
+
let dpopProof;
|
|
74
|
+
if (hubspotConnectEnv.isDpopEnabled) dpopProof = await buildTokenEndpointDpopProof({
|
|
75
|
+
appKeys,
|
|
76
|
+
tokenEndpointUrl,
|
|
77
|
+
sessionIdHash: sessionId
|
|
78
|
+
});
|
|
79
|
+
let formParams;
|
|
80
|
+
if (hubspotConnectEnv.isCimdEnabled) formParams = {
|
|
81
|
+
grant_type: "authorization_code",
|
|
82
|
+
code,
|
|
83
|
+
code_verifier: decodedCodeVerifier,
|
|
84
|
+
redirect_uri: redirectUri,
|
|
85
|
+
...buildClientAssertionFormParams({
|
|
86
|
+
clientId,
|
|
87
|
+
clientAssertion: await buildClientAssertion({
|
|
88
|
+
appKeys,
|
|
89
|
+
clientId,
|
|
90
|
+
audience: tokenEndpointUrl
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
};
|
|
94
|
+
else formParams = {
|
|
95
|
+
grant_type: "authorization_code",
|
|
96
|
+
code,
|
|
97
|
+
code_verifier: decodedCodeVerifier,
|
|
98
|
+
redirect_uri: redirectUri,
|
|
99
|
+
...buildClientSecretFormParams({
|
|
100
|
+
clientId,
|
|
101
|
+
clientSecret: hubspotConnectEnv.hubspotClientSecret
|
|
102
|
+
})
|
|
103
|
+
};
|
|
104
|
+
const tokenResult = await requestOAuthToken({
|
|
105
|
+
tokenEndpointUrl,
|
|
106
|
+
isDpopEnabled: hubspotConnectEnv.isDpopEnabled,
|
|
107
|
+
...dpopProof !== void 0 ? { dpopProof } : {},
|
|
108
|
+
formParams
|
|
109
|
+
});
|
|
110
|
+
if (!tokenResult.ok) return c.json({ error: `Token exchange failed: ${tokenResult.errorText}` }, 502);
|
|
111
|
+
const { access_token: accessToken, refresh_token: refreshToken, expires_in } = tokenResult.body;
|
|
112
|
+
if (!refreshToken) return c.json({ error: "Token response missing refresh_token" }, 502);
|
|
113
|
+
if (!isPositiveFiniteNumber(expires_in)) return c.json({ error: "Token response missing or invalid expires_in" }, 502);
|
|
114
|
+
const expiresAt = Date.now() + expires_in * 1e3;
|
|
115
|
+
const refreshCookieName = `${HUBSPOT_REFRESH_COOKIE_PREFIX}${sessionId}`;
|
|
116
|
+
setResponseCookie({
|
|
117
|
+
c,
|
|
118
|
+
value: serializeCookie({
|
|
119
|
+
name: HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,
|
|
120
|
+
value: accessToken,
|
|
121
|
+
path: "/",
|
|
122
|
+
sameSite: "None",
|
|
123
|
+
partitioned: true,
|
|
124
|
+
maxAge: expires_in
|
|
125
|
+
})
|
|
126
|
+
});
|
|
127
|
+
setResponseCookie({
|
|
128
|
+
c,
|
|
129
|
+
value: serializeCookie({
|
|
130
|
+
name: refreshCookieName,
|
|
131
|
+
value: refreshToken,
|
|
132
|
+
path: refreshCookiePath,
|
|
133
|
+
sameSite: "None",
|
|
134
|
+
partitioned: true,
|
|
135
|
+
maxAge: REFRESH_COOKIE_MAX_AGE_SEC
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
setResponseCookie({
|
|
139
|
+
c,
|
|
140
|
+
value: clearTempCookie(TEMP_COOKIE_PKCE_VERIFIER)
|
|
141
|
+
});
|
|
142
|
+
setResponseCookie({
|
|
143
|
+
c,
|
|
144
|
+
value: clearTempCookie(TEMP_COOKIE_OAUTH_STATE)
|
|
145
|
+
});
|
|
146
|
+
return c.json({
|
|
147
|
+
expires_at: expiresAt,
|
|
148
|
+
return_path: returnPath
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { handleAuthComplete };
|
|
153
|
+
|
|
154
|
+
//# sourceMappingURL=auth-complete.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-complete.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/auth-complete.ts"],"sourcesContent":["import type { Context } from 'hono';\n\nimport {\n AUTH_COMPLETE_CODE_PARAM,\n AUTH_COMPLETE_STATE_PARAM,\n} from '../../../shared/constants.ts';\nimport {\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_ORIGIN_COOKIE_NAME,\n HUBSPOT_REFRESH_COOKIE_PREFIX,\n TEMP_COOKIE_OAUTH_STATE,\n TEMP_COOKIE_PKCE_VERIFIER,\n} from '../../constants.ts';\nimport { base64urlDecode } from '../../utils/base64-utils.ts';\nimport { parseCookies } from '../../utils/cookie-utils.ts';\nimport { serializeCookie, setResponseCookie } from '../utils/cookie-utils.ts';\nimport { REFRESH_COOKIE_MAX_AGE_SEC } from './constants.ts';\nimport {\n buildClientAssertion,\n buildClientAssertionFormParams,\n buildClientSecretFormParams,\n buildTokenEndpointDpopProof,\n requestOAuthToken,\n} from './oauth-client.ts';\nimport type { HubSpotConnectOAuthRouteOptions } from './types.ts';\nimport {\n buildCimdClientIdUrlFromRequest,\n buildFrontendOAuthRedirectUri,\n clearTempCookie,\n isPositiveFiniteNumber,\n isSafeReturnPath,\n parseAppOriginHeader,\n} from './utils.ts';\n\ninterface OAuthStatePayload {\n return_path?: string;\n sid?: string;\n}\n\n/**\n * Cross-origin OAuth completion endpoint.\n *\n * Called from the React app on the frontend OAuth callback path\n * (`HUBSPOT_FRONTEND_CALLBACK_PATH`) once HubSpot has redirected the\n * browser back with `?code` + `?state`. The browser POSTs both\n * values here as a credentialed cross-site fetch — same partition as\n * `init-session`, so the temp PKCE/state cookies are visible — and\n * the SDK:\n *\n * 1. Validates `state` against the temp `__hs_oauth_state` cookie.\n * 2. Pulls the PKCE verifier from `__hs_pkce_verifier`.\n * 3. Rebuilds the same `redirect_uri` it sent to HubSpot during\n * `init-session` (frontend origin + the fixed callback path);\n * the OAuth token endpoint requires the two values to match.\n * 4. Exchanges `code` for an access + refresh token (with DPoP /\n * CIMD client-assertion when enabled).\n * 5. Sets the durable session cookies (access token, refresh) with\n * `SameSite=None; Secure; Partitioned` so they live in the\n * `(frontend, edge)` partition where subsequent API fetches will\n * read them.\n * 6. Clears the temp cookies.\n * 7. Returns `{ expires_at, return_path }` so the controller can\n * update its session-storage expiry tracking and navigate back to\n * the page the user started the connect flow from.\n */\nexport async function handleAuthComplete(\n c: Context,\n options: HubSpotConnectOAuthRouteOptions\n) {\n const { appKeys, refreshCookiePath, hubspotConnectEnv } = options;\n const xForwardedProto = c.req.header('x-forwarded-proto') ?? undefined;\n const xForwardedHost = c.req.header('x-forwarded-host') ?? undefined;\n const requestHostHeader = c.req.header('host') ?? undefined;\n const code = c.req.query(AUTH_COMPLETE_CODE_PARAM);\n const state = c.req.query(AUTH_COMPLETE_STATE_PARAM);\n\n if (!code || !state) {\n return c.json({ error: 'Missing code or state' }, 400);\n }\n\n if (hubspotConnectEnv.isAppPrivateKeyRequired && !appKeys) {\n return c.json(\n {\n error:\n 'Server misconfiguration: HUBSPOT_APP_PRIVATE_KEY is required when CIMD or DPoP is enabled',\n },\n 500\n );\n }\n\n const cookies = parseCookies(c.req.header('Cookie'));\n const expectedState = cookies[TEMP_COOKIE_OAUTH_STATE];\n const codeVerifier = cookies[TEMP_COOKIE_PKCE_VERIFIER];\n const appOriginCookie = cookies[HUBSPOT_APP_ORIGIN_COOKIE_NAME];\n\n if (!expectedState || state !== decodeURIComponent(expectedState)) {\n return c.json({ error: 'State mismatch' }, 403);\n }\n if (!codeVerifier) {\n return c.json({ error: 'Missing PKCE verifier' }, 400);\n }\n // The redirect_uri the OAuth token endpoint validates must equal\n // the one we sent during init-session. We rebuild it from the\n // pinned origin cookie so that value is anchored server-side, not\n // taken from the (caller-controlled) request `Origin` on this call.\n const appOrigin = parseAppOriginHeader(appOriginCookie);\n if (!appOrigin) {\n return c.json({ error: 'Missing app origin cookie' }, 400);\n }\n\n let statePayload: OAuthStatePayload;\n try {\n statePayload = JSON.parse(\n new TextDecoder().decode(base64urlDecode(decodeURIComponent(state)))\n ) as OAuthStatePayload;\n } catch {\n return c.json({ error: 'Malformed state value' }, 400);\n }\n const returnPath = statePayload.return_path;\n if (!returnPath || !isSafeReturnPath(returnPath)) {\n return c.json({ error: 'Invalid return path in state' }, 400);\n }\n\n const sessionId = statePayload.sid;\n if (!sessionId) {\n return c.json({ error: 'Missing app session cookie' }, 400);\n }\n\n const decodedCodeVerifier = decodeURIComponent(codeVerifier);\n\n const clientId = hubspotConnectEnv.isCimdEnabled\n ? buildCimdClientIdUrlFromRequest({\n requestUrl: c.req.url,\n basePath: options.basePath,\n xForwardedProto,\n xForwardedHost,\n requestHostHeader,\n })\n : hubspotConnectEnv.hubspotClientId;\n\n const redirectUri = buildFrontendOAuthRedirectUri(appOrigin);\n\n const tokenEndpointUrl = new URL(\n '/oauth/v1/token',\n hubspotConnectEnv.hubspotOAuthApiOrigin\n ).href;\n\n let dpopProof: string | undefined;\n if (hubspotConnectEnv.isDpopEnabled) {\n dpopProof = await buildTokenEndpointDpopProof({\n appKeys: appKeys!,\n tokenEndpointUrl,\n sessionIdHash: sessionId,\n });\n }\n\n let formParams: Record<string, string>;\n if (hubspotConnectEnv.isCimdEnabled) {\n const clientAssertion = await buildClientAssertion({\n appKeys: appKeys!,\n clientId,\n audience: tokenEndpointUrl,\n });\n formParams = {\n grant_type: 'authorization_code',\n code,\n code_verifier: decodedCodeVerifier,\n redirect_uri: redirectUri,\n ...buildClientAssertionFormParams({ clientId, clientAssertion }),\n };\n } else {\n formParams = {\n grant_type: 'authorization_code',\n code,\n code_verifier: decodedCodeVerifier,\n redirect_uri: redirectUri,\n ...buildClientSecretFormParams({\n clientId,\n clientSecret: hubspotConnectEnv.hubspotClientSecret,\n }),\n };\n }\n\n const tokenResult = await requestOAuthToken({\n tokenEndpointUrl,\n isDpopEnabled: hubspotConnectEnv.isDpopEnabled,\n ...(dpopProof !== undefined ? { dpopProof } : {}),\n formParams,\n });\n if (!tokenResult.ok) {\n return c.json(\n { error: `Token exchange failed: ${tokenResult.errorText}` },\n 502\n );\n }\n\n const {\n access_token: accessToken,\n refresh_token: refreshToken,\n expires_in,\n } = tokenResult.body;\n if (!refreshToken) {\n return c.json({ error: 'Token response missing refresh_token' }, 502);\n }\n if (!isPositiveFiniteNumber(expires_in)) {\n return c.json(\n { error: 'Token response missing or invalid expires_in' },\n 502\n );\n }\n\n const expiresAt = Date.now() + expires_in * 1000;\n const refreshCookieName = `${HUBSPOT_REFRESH_COOKIE_PREFIX}${sessionId}`;\n\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n value: accessToken,\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: expires_in,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: refreshCookieName,\n value: refreshToken,\n path: refreshCookiePath,\n sameSite: 'None',\n partitioned: true,\n maxAge: REFRESH_COOKIE_MAX_AGE_SEC,\n }),\n });\n setResponseCookie({ c, value: clearTempCookie(TEMP_COOKIE_PKCE_VERIFIER) });\n setResponseCookie({ c, value: clearTempCookie(TEMP_COOKIE_OAUTH_STATE) });\n\n return c.json({ expires_at: expiresAt, return_path: returnPath });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA,eAAsB,mBACpB,GACA,SACA;CACA,MAAM,EAAE,SAAS,mBAAmB,sBAAsB;CAC1D,MAAM,kBAAkB,EAAE,IAAI,OAAO,oBAAoB,IAAI,KAAA;CAC7D,MAAM,iBAAiB,EAAE,IAAI,OAAO,mBAAmB,IAAI,KAAA;CAC3D,MAAM,oBAAoB,EAAE,IAAI,OAAO,OAAO,IAAI,KAAA;CAClD,MAAM,OAAO,EAAE,IAAI,MAAM,yBAAyB;CAClD,MAAM,QAAQ,EAAE,IAAI,MAAM,0BAA0B;CAEpD,IAAI,CAAC,QAAQ,CAAC,OACZ,OAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,EAAE,IAAI;CAGxD,IAAI,kBAAkB,2BAA2B,CAAC,SAChD,OAAO,EAAE,KACP,EACE,OACE,6FACH,EACD,IACD;CAGH,MAAM,UAAU,aAAa,EAAE,IAAI,OAAO,SAAS,CAAC;CACpD,MAAM,gBAAgB,QAAQ;CAC9B,MAAM,eAAe,QAAQ;CAC7B,MAAM,kBAAkB,QAAQ;CAEhC,IAAI,CAAC,iBAAiB,UAAU,mBAAmB,cAAc,EAC/D,OAAO,EAAE,KAAK,EAAE,OAAO,kBAAkB,EAAE,IAAI;CAEjD,IAAI,CAAC,cACH,OAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,EAAE,IAAI;CAMxD,MAAM,YAAY,qBAAqB,gBAAgB;CACvD,IAAI,CAAC,WACH,OAAO,EAAE,KAAK,EAAE,OAAO,6BAA6B,EAAE,IAAI;CAG5D,IAAI;CACJ,IAAI;EACF,eAAe,KAAK,MAClB,IAAI,aAAa,CAAC,OAAO,gBAAgB,mBAAmB,MAAM,CAAC,CAAC,CACrE;SACK;EACN,OAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,EAAE,IAAI;;CAExD,MAAM,aAAa,aAAa;CAChC,IAAI,CAAC,cAAc,CAAC,iBAAiB,WAAW,EAC9C,OAAO,EAAE,KAAK,EAAE,OAAO,gCAAgC,EAAE,IAAI;CAG/D,MAAM,YAAY,aAAa;CAC/B,IAAI,CAAC,WACH,OAAO,EAAE,KAAK,EAAE,OAAO,8BAA8B,EAAE,IAAI;CAG7D,MAAM,sBAAsB,mBAAmB,aAAa;CAE5D,MAAM,WAAW,kBAAkB,gBAC/B,gCAAgC;EAC9B,YAAY,EAAE,IAAI;EAClB,UAAU,QAAQ;EAClB;EACA;EACA;EACD,CAAC,GACF,kBAAkB;CAEtB,MAAM,cAAc,8BAA8B,UAAU;CAE5D,MAAM,mBAAmB,IAAI,IAC3B,mBACA,kBAAkB,sBACnB,CAAC;CAEF,IAAI;CACJ,IAAI,kBAAkB,eACpB,YAAY,MAAM,4BAA4B;EACnC;EACT;EACA,eAAe;EAChB,CAAC;CAGJ,IAAI;CACJ,IAAI,kBAAkB,eAMpB,aAAa;EACX,YAAY;EACZ;EACA,eAAe;EACf,cAAc;EACd,GAAG,+BAA+B;GAAE;GAAU,iBAAA,MAVlB,qBAAqB;IACxC;IACT;IACA,UAAU;IACX,CAAC;GAM+D,CAAC;EACjE;MAED,aAAa;EACX,YAAY;EACZ;EACA,eAAe;EACf,cAAc;EACd,GAAG,4BAA4B;GAC7B;GACA,cAAc,kBAAkB;GACjC,CAAC;EACH;CAGH,MAAM,cAAc,MAAM,kBAAkB;EAC1C;EACA,eAAe,kBAAkB;EACjC,GAAI,cAAc,KAAA,IAAY,EAAE,WAAW,GAAG,EAAE;EAChD;EACD,CAAC;CACF,IAAI,CAAC,YAAY,IACf,OAAO,EAAE,KACP,EAAE,OAAO,0BAA0B,YAAY,aAAa,EAC5D,IACD;CAGH,MAAM,EACJ,cAAc,aACd,eAAe,cACf,eACE,YAAY;CAChB,IAAI,CAAC,cACH,OAAO,EAAE,KAAK,EAAE,OAAO,wCAAwC,EAAE,IAAI;CAEvE,IAAI,CAAC,uBAAuB,WAAW,EACrC,OAAO,EAAE,KACP,EAAE,OAAO,gDAAgD,EACzD,IACD;CAGH,MAAM,YAAY,KAAK,KAAK,GAAG,aAAa;CAC5C,MAAM,oBAAoB,GAAG,gCAAgC;CAE7D,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAAE;EAAG,OAAO,gBAAgB,0BAA0B;EAAE,CAAC;CAC3E,kBAAkB;EAAE;EAAG,OAAO,gBAAgB,wBAAwB;EAAE,CAAC;CAEzE,OAAO,EAAE,KAAK;EAAE,YAAY;EAAW,aAAa;EAAY,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { base64url } from "../../shared/encoding/base64.js";
|
|
2
2
|
import { sha256base64url } from "../../shared/encoding/sha256.js";
|
|
3
|
-
import { HUBSPOT_APP_SID_COOKIE_NAME, TEMP_COOKIE_OAUTH_STATE, TEMP_COOKIE_PKCE_VERIFIER } from "../../constants.js";
|
|
3
|
+
import { HUBSPOT_APP_ORIGIN_COOKIE_NAME, HUBSPOT_APP_SID_COOKIE_NAME, TEMP_COOKIE_OAUTH_STATE, TEMP_COOKIE_PKCE_VERIFIER } from "../../constants.js";
|
|
4
4
|
import { serializeCookie, setResponseCookie } from "../utils/cookie-utils.js";
|
|
5
5
|
import { OAUTH_TEMP_MAX_AGE_SEC, SESSION_MAX_AGE_SEC } from "./constants.js";
|
|
6
|
-
import { buildCimdClientIdUrlFromRequest,
|
|
6
|
+
import { buildCimdClientIdUrlFromRequest, buildFrontendOAuthRedirectUri, isSafeReturnPath, parseAppOriginHeader } from "./utils.js";
|
|
7
7
|
import { deriveHubSpotAuthorizeScopesFromClientMetadata } from "./fetch-hubspot-client-metadata.js";
|
|
8
8
|
//#region src/server/hono/hubspot-connect-routes/auth-init-session.ts
|
|
9
9
|
async function handleAuthInitSession(c, options) {
|
|
@@ -13,6 +13,8 @@ async function handleAuthInitSession(c, options) {
|
|
|
13
13
|
const requestHostHeader = c.req.header("host") ?? void 0;
|
|
14
14
|
const returnPath = new URL(c.req.url).searchParams.get("return_path") ?? "/";
|
|
15
15
|
if (!isSafeReturnPath(returnPath)) return c.text("Invalid return_path", 400);
|
|
16
|
+
const appOrigin = parseAppOriginHeader(c.req.header("Origin"));
|
|
17
|
+
if (!appOrigin) return c.text("Missing or invalid Origin header; init-session must be called from a browser", 400);
|
|
16
18
|
const sessionIdBytes = new Uint8Array(32);
|
|
17
19
|
crypto.getRandomValues(sessionIdBytes);
|
|
18
20
|
const sessionId = base64url(sessionIdBytes);
|
|
@@ -32,13 +34,7 @@ async function handleAuthInitSession(c, options) {
|
|
|
32
34
|
xForwardedHost,
|
|
33
35
|
requestHostHeader
|
|
34
36
|
}) : hubspotConnectEnv.hubspotClientId;
|
|
35
|
-
const redirectUri =
|
|
36
|
-
requestUrl: c.req.url,
|
|
37
|
-
basePath: options.basePath,
|
|
38
|
-
xForwardedProto,
|
|
39
|
-
xForwardedHost,
|
|
40
|
-
requestHostHeader
|
|
41
|
-
});
|
|
37
|
+
const redirectUri = buildFrontendOAuthRedirectUri(appOrigin);
|
|
42
38
|
const authorizeUrl = new URL(hubspotConnectEnv.hubspotAuthorizationEndpoint);
|
|
43
39
|
authorizeUrl.searchParams.set("response_type", "code");
|
|
44
40
|
authorizeUrl.searchParams.set("client_id", clientId);
|
|
@@ -53,12 +49,25 @@ async function handleAuthInitSession(c, options) {
|
|
|
53
49
|
authorizeUrl.searchParams.set("scope", scopesResult.scope);
|
|
54
50
|
if (scopesResult.optionalScope !== void 0) authorizeUrl.searchParams.set("optional_scope", scopesResult.optionalScope);
|
|
55
51
|
}
|
|
52
|
+
setResponseCookie({
|
|
53
|
+
c,
|
|
54
|
+
value: serializeCookie({
|
|
55
|
+
name: HUBSPOT_APP_ORIGIN_COOKIE_NAME,
|
|
56
|
+
value: appOrigin,
|
|
57
|
+
path: "/",
|
|
58
|
+
sameSite: "None",
|
|
59
|
+
partitioned: true,
|
|
60
|
+
maxAge: SESSION_MAX_AGE_SEC
|
|
61
|
+
})
|
|
62
|
+
});
|
|
56
63
|
setResponseCookie({
|
|
57
64
|
c,
|
|
58
65
|
value: serializeCookie({
|
|
59
66
|
name: HUBSPOT_APP_SID_COOKIE_NAME,
|
|
60
67
|
value: sessionId,
|
|
61
68
|
path: "/",
|
|
69
|
+
sameSite: "None",
|
|
70
|
+
partitioned: true,
|
|
62
71
|
maxAge: SESSION_MAX_AGE_SEC
|
|
63
72
|
})
|
|
64
73
|
});
|
|
@@ -68,7 +77,8 @@ async function handleAuthInitSession(c, options) {
|
|
|
68
77
|
name: TEMP_COOKIE_PKCE_VERIFIER,
|
|
69
78
|
value: encodeURIComponent(codeVerifier),
|
|
70
79
|
path: "/",
|
|
71
|
-
sameSite: "
|
|
80
|
+
sameSite: "None",
|
|
81
|
+
partitioned: true,
|
|
72
82
|
maxAge: OAUTH_TEMP_MAX_AGE_SEC
|
|
73
83
|
})
|
|
74
84
|
});
|
|
@@ -78,7 +88,8 @@ async function handleAuthInitSession(c, options) {
|
|
|
78
88
|
name: TEMP_COOKIE_OAUTH_STATE,
|
|
79
89
|
value: encodeURIComponent(stateValue),
|
|
80
90
|
path: "/",
|
|
81
|
-
sameSite: "
|
|
91
|
+
sameSite: "None",
|
|
92
|
+
partitioned: true,
|
|
82
93
|
maxAge: OAUTH_TEMP_MAX_AGE_SEC
|
|
83
94
|
})
|
|
84
95
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-init-session.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/auth-init-session.ts"],"sourcesContent":["import type { Context } from 'hono';\n\nimport {\n HUBSPOT_APP_SID_COOKIE_NAME,\n TEMP_COOKIE_OAUTH_STATE,\n TEMP_COOKIE_PKCE_VERIFIER,\n} from '../../constants.ts';\nimport { base64url } from '../../utils/base64-utils.ts';\nimport { sha256base64url } from '../../utils/crypto-utils.ts';\nimport { serializeCookie, setResponseCookie } from '../utils/cookie-utils.ts';\nimport { OAUTH_TEMP_MAX_AGE_SEC, SESSION_MAX_AGE_SEC } from './constants.ts';\nimport { deriveHubSpotAuthorizeScopesFromClientMetadata } from './fetch-hubspot-client-metadata.ts';\nimport type { HubSpotConnectOAuthRouteOptions } from './types.ts';\nimport {\n buildCimdClientIdUrlFromRequest,\n
|
|
1
|
+
{"version":3,"file":"auth-init-session.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/auth-init-session.ts"],"sourcesContent":["import type { Context } from 'hono';\n\nimport {\n HUBSPOT_APP_ORIGIN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n TEMP_COOKIE_OAUTH_STATE,\n TEMP_COOKIE_PKCE_VERIFIER,\n} from '../../constants.ts';\nimport { base64url } from '../../utils/base64-utils.ts';\nimport { sha256base64url } from '../../utils/crypto-utils.ts';\nimport { serializeCookie, setResponseCookie } from '../utils/cookie-utils.ts';\nimport { OAUTH_TEMP_MAX_AGE_SEC, SESSION_MAX_AGE_SEC } from './constants.ts';\nimport { deriveHubSpotAuthorizeScopesFromClientMetadata } from './fetch-hubspot-client-metadata.ts';\nimport type { HubSpotConnectOAuthRouteOptions } from './types.ts';\nimport {\n buildCimdClientIdUrlFromRequest,\n buildFrontendOAuthRedirectUri,\n isSafeReturnPath,\n parseAppOriginHeader,\n} from './utils.ts';\n\nexport async function handleAuthInitSession(\n c: Context,\n options: HubSpotConnectOAuthRouteOptions\n) {\n const { hubspotConnectEnv, cimdClientMetadata } = options;\n const xForwardedProto = c.req.header('x-forwarded-proto') ?? undefined;\n const xForwardedHost = c.req.header('x-forwarded-host') ?? undefined;\n const requestHostHeader = c.req.header('host') ?? undefined;\n const url = new URL(c.req.url);\n const returnPath = url.searchParams.get('return_path') ?? '/';\n if (!isSafeReturnPath(returnPath)) {\n return c.text('Invalid return_path', 400);\n }\n\n // The app origin pins the OAuth `redirect_uri` (which lands on the\n // frontend, not on this edge function) and, via the persisted\n // `__Host-hs_app_origin` cookie, drives credentialed\n // `Access-Control-Allow-Origin` on every subsequent SDK response.\n const appOrigin = parseAppOriginHeader(c.req.header('Origin'));\n if (!appOrigin) {\n return c.text(\n 'Missing or invalid Origin header; init-session must be called from a browser',\n 400\n );\n }\n\n const sessionIdBytes = new Uint8Array(32);\n crypto.getRandomValues(sessionIdBytes);\n const sessionId = base64url(sessionIdBytes);\n const sessionIdHash = await sha256base64url(sessionId);\n\n const codeVerifierBytes = new Uint8Array(32);\n crypto.getRandomValues(codeVerifierBytes);\n const codeVerifier = base64url(codeVerifierBytes);\n const codeChallenge = await sha256base64url(codeVerifier);\n\n const stateValue = base64url(\n new TextEncoder().encode(\n JSON.stringify({\n return_path: returnPath,\n sid: sessionIdHash,\n })\n )\n );\n\n const clientId = hubspotConnectEnv.isCimdEnabled\n ? buildCimdClientIdUrlFromRequest({\n requestUrl: c.req.url,\n basePath: options.basePath,\n xForwardedProto,\n xForwardedHost,\n requestHostHeader,\n })\n : hubspotConnectEnv.hubspotClientId;\n\n const redirectUri = buildFrontendOAuthRedirectUri(appOrigin);\n\n const authorizeUrl = new URL(hubspotConnectEnv.hubspotAuthorizationEndpoint);\n authorizeUrl.searchParams.set('response_type', 'code');\n authorizeUrl.searchParams.set('client_id', clientId);\n authorizeUrl.searchParams.set('redirect_uri', redirectUri);\n authorizeUrl.searchParams.set('code_challenge', codeChallenge);\n authorizeUrl.searchParams.set('code_challenge_method', 'S256');\n authorizeUrl.searchParams.set('state', stateValue);\n authorizeUrl.searchParams.set('sid', sessionIdHash);\n\n if (!hubspotConnectEnv.isCimdEnabled) {\n const scopesResult =\n deriveHubSpotAuthorizeScopesFromClientMetadata(cimdClientMetadata);\n if (!scopesResult.ok) {\n return c.text(scopesResult.message, scopesResult.status as 500 | 502);\n }\n authorizeUrl.searchParams.set('scope', scopesResult.scope);\n if (scopesResult.optionalScope !== undefined) {\n authorizeUrl.searchParams.set(\n 'optional_scope',\n scopesResult.optionalScope\n );\n }\n }\n\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_APP_ORIGIN_COOKIE_NAME,\n value: appOrigin,\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: SESSION_MAX_AGE_SEC,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_APP_SID_COOKIE_NAME,\n value: sessionId,\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: SESSION_MAX_AGE_SEC,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: TEMP_COOKIE_PKCE_VERIFIER,\n value: encodeURIComponent(codeVerifier),\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: OAUTH_TEMP_MAX_AGE_SEC,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: TEMP_COOKIE_OAUTH_STATE,\n value: encodeURIComponent(stateValue),\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: OAUTH_TEMP_MAX_AGE_SEC,\n }),\n });\n\n return c.json({ authorization_url: authorizeUrl.toString() });\n}\n"],"mappings":";;;;;;;;AAqBA,eAAsB,sBACpB,GACA,SACA;CACA,MAAM,EAAE,mBAAmB,uBAAuB;CAClD,MAAM,kBAAkB,EAAE,IAAI,OAAO,oBAAoB,IAAI,KAAA;CAC7D,MAAM,iBAAiB,EAAE,IAAI,OAAO,mBAAmB,IAAI,KAAA;CAC3D,MAAM,oBAAoB,EAAE,IAAI,OAAO,OAAO,IAAI,KAAA;CAElD,MAAM,aAAa,IADH,IAAI,EAAE,IAAI,IACJ,CAAC,aAAa,IAAI,cAAc,IAAI;CAC1D,IAAI,CAAC,iBAAiB,WAAW,EAC/B,OAAO,EAAE,KAAK,uBAAuB,IAAI;CAO3C,MAAM,YAAY,qBAAqB,EAAE,IAAI,OAAO,SAAS,CAAC;CAC9D,IAAI,CAAC,WACH,OAAO,EAAE,KACP,gFACA,IACD;CAGH,MAAM,iBAAiB,IAAI,WAAW,GAAG;CACzC,OAAO,gBAAgB,eAAe;CACtC,MAAM,YAAY,UAAU,eAAe;CAC3C,MAAM,gBAAgB,MAAM,gBAAgB,UAAU;CAEtD,MAAM,oBAAoB,IAAI,WAAW,GAAG;CAC5C,OAAO,gBAAgB,kBAAkB;CACzC,MAAM,eAAe,UAAU,kBAAkB;CACjD,MAAM,gBAAgB,MAAM,gBAAgB,aAAa;CAEzD,MAAM,aAAa,UACjB,IAAI,aAAa,CAAC,OAChB,KAAK,UAAU;EACb,aAAa;EACb,KAAK;EACN,CAAC,CACH,CACF;CAED,MAAM,WAAW,kBAAkB,gBAC/B,gCAAgC;EAC9B,YAAY,EAAE,IAAI;EAClB,UAAU,QAAQ;EAClB;EACA;EACA;EACD,CAAC,GACF,kBAAkB;CAEtB,MAAM,cAAc,8BAA8B,UAAU;CAE5D,MAAM,eAAe,IAAI,IAAI,kBAAkB,6BAA6B;CAC5E,aAAa,aAAa,IAAI,iBAAiB,OAAO;CACtD,aAAa,aAAa,IAAI,aAAa,SAAS;CACpD,aAAa,aAAa,IAAI,gBAAgB,YAAY;CAC1D,aAAa,aAAa,IAAI,kBAAkB,cAAc;CAC9D,aAAa,aAAa,IAAI,yBAAyB,OAAO;CAC9D,aAAa,aAAa,IAAI,SAAS,WAAW;CAClD,aAAa,aAAa,IAAI,OAAO,cAAc;CAEnD,IAAI,CAAC,kBAAkB,eAAe;EACpC,MAAM,eACJ,+CAA+C,mBAAmB;EACpE,IAAI,CAAC,aAAa,IAChB,OAAO,EAAE,KAAK,aAAa,SAAS,aAAa,OAAoB;EAEvE,aAAa,aAAa,IAAI,SAAS,aAAa,MAAM;EAC1D,IAAI,aAAa,kBAAkB,KAAA,GACjC,aAAa,aAAa,IACxB,kBACA,aAAa,cACd;;CAIL,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO,mBAAmB,aAAa;GACvC,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO,mBAAmB,WAAW;GACrC,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CAEF,OAAO,EAAE,KAAK,EAAE,mBAAmB,aAAa,UAAU,EAAE,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HUBSPOT_ACCESS_TOKEN_COOKIE_NAME, HUBSPOT_APP_SID_COOKIE_NAME } from "../../constants.js";
|
|
1
|
+
import { HUBSPOT_ACCESS_TOKEN_COOKIE_NAME, HUBSPOT_APP_ORIGIN_COOKIE_NAME, HUBSPOT_APP_SID_COOKIE_NAME } from "../../constants.js";
|
|
2
2
|
import { parseCookies } from "../../utils/cookie-utils.js";
|
|
3
3
|
import { serializeCookie, setResponseCookie } from "../utils/cookie-utils.js";
|
|
4
4
|
import { buildClientAssertion } from "./oauth-client.js";
|
|
@@ -66,6 +66,8 @@ async function handleAuthLogout(c, options) {
|
|
|
66
66
|
name: HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,
|
|
67
67
|
value: "",
|
|
68
68
|
path: "/",
|
|
69
|
+
sameSite: "None",
|
|
70
|
+
partitioned: true,
|
|
69
71
|
maxAge: 0
|
|
70
72
|
})
|
|
71
73
|
});
|
|
@@ -75,6 +77,19 @@ async function handleAuthLogout(c, options) {
|
|
|
75
77
|
name: HUBSPOT_APP_SID_COOKIE_NAME,
|
|
76
78
|
value: "",
|
|
77
79
|
path: "/",
|
|
80
|
+
sameSite: "None",
|
|
81
|
+
partitioned: true,
|
|
82
|
+
maxAge: 0
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
setResponseCookie({
|
|
86
|
+
c,
|
|
87
|
+
value: serializeCookie({
|
|
88
|
+
name: HUBSPOT_APP_ORIGIN_COOKIE_NAME,
|
|
89
|
+
value: "",
|
|
90
|
+
path: "/",
|
|
91
|
+
sameSite: "None",
|
|
92
|
+
partitioned: true,
|
|
78
93
|
maxAge: 0
|
|
79
94
|
})
|
|
80
95
|
});
|
|
@@ -85,6 +100,8 @@ async function handleAuthLogout(c, options) {
|
|
|
85
100
|
name: cookieName,
|
|
86
101
|
value: "",
|
|
87
102
|
path: refreshCookiePath,
|
|
103
|
+
sameSite: "None",
|
|
104
|
+
partitioned: true,
|
|
88
105
|
maxAge: 0
|
|
89
106
|
})
|
|
90
107
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-logout.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/auth-logout.ts"],"sourcesContent":["import type { Context } from 'hono';\n\nimport type { Logger } from '../../../shared/logger.ts';\nimport {\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n HUBSPOT_REFRESH_COOKIE_PREFIX,\n} from '../../constants.ts';\nimport { parseCookies } from '../../utils/cookie-utils.ts';\nimport { serializeCookie, setResponseCookie } from '../utils/cookie-utils.ts';\nimport { buildClientAssertion } from './oauth-client.ts';\nimport type { HubSpotConnectOAuthRouteOptions } from './types.ts';\nimport { buildCimdClientIdUrlFromRequest } from './utils.ts';\n\nasync function revokeToken(options: {\n revokeEndpointUrl: string;\n body: URLSearchParams;\n logger: Logger;\n}): Promise<void> {\n const { revokeEndpointUrl, body, logger } = options;\n try {\n const response = await fetch(revokeEndpointUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n });\n if (!response.ok) {\n logger.warn(\n `HubSpot token revoke returned HTTP ${response.status} ${response.statusText}`\n );\n }\n } catch (error) {\n logger.warn('HubSpot token revoke request failed', error);\n }\n}\n\nexport async function handleAuthLogout(\n c: Context,\n options: HubSpotConnectOAuthRouteOptions\n) {\n const { appKeys, refreshCookiePath, basePath, hubspotConnectEnv, logger } =\n options;\n const xForwardedProto = c.req.header('x-forwarded-proto') ?? undefined;\n const xForwardedHost = c.req.header('x-forwarded-host') ?? undefined;\n const requestHostHeader = c.req.header('host') ?? undefined;\n const cookies = parseCookies(c.req.header('Cookie'));\n const accessToken = cookies[HUBSPOT_ACCESS_TOKEN_COOKIE_NAME];\n\n const clientId = hubspotConnectEnv.isCimdEnabled\n ? buildCimdClientIdUrlFromRequest({\n requestUrl: c.req.url,\n basePath,\n xForwardedProto,\n xForwardedHost,\n requestHostHeader,\n })\n : hubspotConnectEnv.hubspotClientId;\n\n const revokeEndpointUrl = new URL(\n '/oauth/v1/revoke',\n hubspotConnectEnv.hubspotOAuthApiOrigin\n ).href;\n\n if (accessToken) {\n if (hubspotConnectEnv.isCimdEnabled) {\n if (!appKeys) {\n return c.json(\n {\n error:\n 'Server misconfiguration: HUBSPOT_APP_PRIVATE_KEY is required when CIMD is enabled',\n },\n 500\n );\n }\n const clientAssertion = await buildClientAssertion({\n appKeys,\n clientId,\n audience: revokeEndpointUrl,\n });\n await revokeToken({\n revokeEndpointUrl,\n body: new URLSearchParams({\n token: accessToken,\n token_type_hint: 'access_token',\n client_id: clientId,\n client_assertion_type:\n 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',\n client_assertion: clientAssertion,\n }),\n logger,\n });\n } else {\n await revokeToken({\n revokeEndpointUrl,\n body: new URLSearchParams({\n token: accessToken,\n token_type_hint: 'access_token',\n client_id: clientId,\n client_secret: hubspotConnectEnv.hubspotClientSecret,\n }),\n logger,\n });\n }\n }\n\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n value: '',\n path: '/',\n maxAge: 0,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_APP_SID_COOKIE_NAME,\n value: '',\n path: '/',\n maxAge: 0,\n }),\n });\n\n Object.keys(cookies).forEach((cookieName) => {\n if (cookieName.startsWith(HUBSPOT_REFRESH_COOKIE_PREFIX)) {\n setResponseCookie({\n c,\n value: serializeCookie({\n name: cookieName,\n value: '',\n path: refreshCookiePath,\n maxAge: 0,\n }),\n });\n }\n });\n\n return c.json({ redirect_to: '/' });\n}\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"auth-logout.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/auth-logout.ts"],"sourcesContent":["import type { Context } from 'hono';\n\nimport type { Logger } from '../../../shared/logger.ts';\nimport {\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_ORIGIN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n HUBSPOT_REFRESH_COOKIE_PREFIX,\n} from '../../constants.ts';\nimport { parseCookies } from '../../utils/cookie-utils.ts';\nimport { serializeCookie, setResponseCookie } from '../utils/cookie-utils.ts';\nimport { buildClientAssertion } from './oauth-client.ts';\nimport type { HubSpotConnectOAuthRouteOptions } from './types.ts';\nimport { buildCimdClientIdUrlFromRequest } from './utils.ts';\n\nasync function revokeToken(options: {\n revokeEndpointUrl: string;\n body: URLSearchParams;\n logger: Logger;\n}): Promise<void> {\n const { revokeEndpointUrl, body, logger } = options;\n try {\n const response = await fetch(revokeEndpointUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n });\n if (!response.ok) {\n logger.warn(\n `HubSpot token revoke returned HTTP ${response.status} ${response.statusText}`\n );\n }\n } catch (error) {\n logger.warn('HubSpot token revoke request failed', error);\n }\n}\n\nexport async function handleAuthLogout(\n c: Context,\n options: HubSpotConnectOAuthRouteOptions\n) {\n const { appKeys, refreshCookiePath, basePath, hubspotConnectEnv, logger } =\n options;\n const xForwardedProto = c.req.header('x-forwarded-proto') ?? undefined;\n const xForwardedHost = c.req.header('x-forwarded-host') ?? undefined;\n const requestHostHeader = c.req.header('host') ?? undefined;\n const cookies = parseCookies(c.req.header('Cookie'));\n const accessToken = cookies[HUBSPOT_ACCESS_TOKEN_COOKIE_NAME];\n\n const clientId = hubspotConnectEnv.isCimdEnabled\n ? buildCimdClientIdUrlFromRequest({\n requestUrl: c.req.url,\n basePath,\n xForwardedProto,\n xForwardedHost,\n requestHostHeader,\n })\n : hubspotConnectEnv.hubspotClientId;\n\n const revokeEndpointUrl = new URL(\n '/oauth/v1/revoke',\n hubspotConnectEnv.hubspotOAuthApiOrigin\n ).href;\n\n if (accessToken) {\n if (hubspotConnectEnv.isCimdEnabled) {\n if (!appKeys) {\n return c.json(\n {\n error:\n 'Server misconfiguration: HUBSPOT_APP_PRIVATE_KEY is required when CIMD is enabled',\n },\n 500\n );\n }\n const clientAssertion = await buildClientAssertion({\n appKeys,\n clientId,\n audience: revokeEndpointUrl,\n });\n await revokeToken({\n revokeEndpointUrl,\n body: new URLSearchParams({\n token: accessToken,\n token_type_hint: 'access_token',\n client_id: clientId,\n client_assertion_type:\n 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',\n client_assertion: clientAssertion,\n }),\n logger,\n });\n } else {\n await revokeToken({\n revokeEndpointUrl,\n body: new URLSearchParams({\n token: accessToken,\n token_type_hint: 'access_token',\n client_id: clientId,\n client_secret: hubspotConnectEnv.hubspotClientSecret,\n }),\n logger,\n });\n }\n }\n\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n value: '',\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: 0,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_APP_SID_COOKIE_NAME,\n value: '',\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: 0,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_APP_ORIGIN_COOKIE_NAME,\n value: '',\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: 0,\n }),\n });\n\n Object.keys(cookies).forEach((cookieName) => {\n if (cookieName.startsWith(HUBSPOT_REFRESH_COOKIE_PREFIX)) {\n setResponseCookie({\n c,\n value: serializeCookie({\n name: cookieName,\n value: '',\n path: refreshCookiePath,\n sameSite: 'None',\n partitioned: true,\n maxAge: 0,\n }),\n });\n }\n });\n\n return c.json({ redirect_to: '/' });\n}\n"],"mappings":";;;;;;AAeA,eAAe,YAAY,SAIT;CAChB,MAAM,EAAE,mBAAmB,MAAM,WAAW;CAC5C,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,mBAAmB;GAC9C,QAAQ;GACR,SAAS,EAAE,gBAAgB,qCAAqC;GAChE;GACD,CAAC;EACF,IAAI,CAAC,SAAS,IACZ,OAAO,KACL,sCAAsC,SAAS,OAAO,GAAG,SAAS,aACnE;UAEI,OAAO;EACd,OAAO,KAAK,uCAAuC,MAAM;;;AAI7D,eAAsB,iBACpB,GACA,SACA;CACA,MAAM,EAAE,SAAS,mBAAmB,UAAU,mBAAmB,WAC/D;CACF,MAAM,kBAAkB,EAAE,IAAI,OAAO,oBAAoB,IAAI,KAAA;CAC7D,MAAM,iBAAiB,EAAE,IAAI,OAAO,mBAAmB,IAAI,KAAA;CAC3D,MAAM,oBAAoB,EAAE,IAAI,OAAO,OAAO,IAAI,KAAA;CAClD,MAAM,UAAU,aAAa,EAAE,IAAI,OAAO,SAAS,CAAC;CACpD,MAAM,cAAc,QAAQ;CAE5B,MAAM,WAAW,kBAAkB,gBAC/B,gCAAgC;EAC9B,YAAY,EAAE,IAAI;EAClB;EACA;EACA;EACA;EACD,CAAC,GACF,kBAAkB;CAEtB,MAAM,oBAAoB,IAAI,IAC5B,oBACA,kBAAkB,sBACnB,CAAC;CAEF,IAAI,aACF,IAAI,kBAAkB,eAAe;EACnC,IAAI,CAAC,SACH,OAAO,EAAE,KACP,EACE,OACE,qFACH,EACD,IACD;EAEH,MAAM,kBAAkB,MAAM,qBAAqB;GACjD;GACA;GACA,UAAU;GACX,CAAC;EACF,MAAM,YAAY;GAChB;GACA,MAAM,IAAI,gBAAgB;IACxB,OAAO;IACP,iBAAiB;IACjB,WAAW;IACX,uBACE;IACF,kBAAkB;IACnB,CAAC;GACF;GACD,CAAC;QAEF,MAAM,YAAY;EAChB;EACA,MAAM,IAAI,gBAAgB;GACxB,OAAO;GACP,iBAAiB;GACjB,WAAW;GACX,eAAe,kBAAkB;GAClC,CAAC;EACF;EACD,CAAC;CAIN,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CAEF,OAAO,KAAK,QAAQ,CAAC,SAAS,eAAe;EAC3C,IAAI,WAAW,WAAA,cAAyC,EACtD,kBAAkB;GAChB;GACA,OAAO,gBAAgB;IACrB,MAAM;IACN,OAAO;IACP,MAAM;IACN,UAAU;IACV,aAAa;IACb,QAAQ;IACT,CAAC;GACH,CAAC;GAEJ;CAEF,OAAO,EAAE,KAAK,EAAE,aAAa,KAAK,CAAC"}
|
|
@@ -70,6 +70,8 @@ async function handleAuthRefresh(c, options) {
|
|
|
70
70
|
name: HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,
|
|
71
71
|
value: newAccessToken,
|
|
72
72
|
path: "/",
|
|
73
|
+
sameSite: "None",
|
|
74
|
+
partitioned: true,
|
|
73
75
|
maxAge: expires_in
|
|
74
76
|
})
|
|
75
77
|
});
|
|
@@ -79,6 +81,8 @@ async function handleAuthRefresh(c, options) {
|
|
|
79
81
|
name: refreshCookieName,
|
|
80
82
|
value: newRefreshToken,
|
|
81
83
|
path: refreshCookiePath,
|
|
84
|
+
sameSite: "None",
|
|
85
|
+
partitioned: true,
|
|
82
86
|
maxAge: REFRESH_COOKIE_MAX_AGE_SEC
|
|
83
87
|
})
|
|
84
88
|
});
|
|
@@ -89,6 +93,8 @@ async function handleAuthRefresh(c, options) {
|
|
|
89
93
|
name: cookieName,
|
|
90
94
|
value: "",
|
|
91
95
|
path: refreshCookiePath,
|
|
96
|
+
sameSite: "None",
|
|
97
|
+
partitioned: true,
|
|
92
98
|
maxAge: 0
|
|
93
99
|
})
|
|
94
100
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-refresh.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/auth-refresh.ts"],"sourcesContent":["import type { Context } from 'hono';\n\nimport {\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n HUBSPOT_REFRESH_COOKIE_PREFIX,\n} from '../../constants.ts';\nimport { parseCookies } from '../../utils/cookie-utils.ts';\nimport { sha256base64url } from '../../utils/crypto-utils.ts';\nimport { serializeCookie, setResponseCookie } from '../utils/cookie-utils.ts';\nimport { REFRESH_COOKIE_MAX_AGE_SEC } from './constants.ts';\nimport {\n buildClientAssertion,\n buildClientAssertionFormParams,\n buildClientSecretFormParams,\n buildTokenEndpointDpopProof,\n requestOAuthToken,\n} from './oauth-client.ts';\nimport type { HubSpotConnectOAuthRouteOptions } from './types.ts';\nimport {\n buildCimdClientIdUrlFromRequest,\n isPositiveFiniteNumber,\n} from './utils.ts';\n\nexport async function handleAuthRefresh(\n c: Context,\n options: HubSpotConnectOAuthRouteOptions\n) {\n const { appKeys, refreshCookiePath, basePath, hubspotConnectEnv } = options;\n const xForwardedProto = c.req.header('x-forwarded-proto') ?? undefined;\n const xForwardedHost = c.req.header('x-forwarded-host') ?? undefined;\n const requestHostHeader = c.req.header('host') ?? undefined;\n const cookies = parseCookies(c.req.header('Cookie'));\n const sessionId = cookies[HUBSPOT_APP_SID_COOKIE_NAME];\n if (!sessionId) {\n return c.json({ error: 'Missing session cookie' }, 401);\n }\n\n if (hubspotConnectEnv.isAppPrivateKeyRequired && !appKeys) {\n return c.json(\n {\n error:\n 'Server misconfiguration: HUBSPOT_APP_PRIVATE_KEY is required when CIMD or DPoP is enabled',\n },\n 500\n );\n }\n\n const sidHash = await sha256base64url(sessionId);\n const refreshCookieName = `${HUBSPOT_REFRESH_COOKIE_PREFIX}${sidHash}`;\n const refreshToken = cookies[refreshCookieName];\n if (!refreshToken) {\n return c.json({ error: 'Missing refresh token' }, 401);\n }\n\n const clientId = hubspotConnectEnv.isCimdEnabled\n ? buildCimdClientIdUrlFromRequest({\n requestUrl: c.req.url,\n basePath,\n xForwardedProto,\n xForwardedHost,\n requestHostHeader,\n })\n : hubspotConnectEnv.hubspotClientId;\n\n const tokenEndpointUrl = new URL(\n '/oauth/v1/token',\n hubspotConnectEnv.hubspotOAuthApiOrigin\n ).href;\n\n let dpopProof: string | undefined;\n if (hubspotConnectEnv.isDpopEnabled) {\n dpopProof = await buildTokenEndpointDpopProof({\n appKeys: appKeys!,\n tokenEndpointUrl,\n sessionIdHash: sidHash,\n });\n }\n\n let formParams: Record<string, string>;\n if (hubspotConnectEnv.isCimdEnabled) {\n const clientAssertion = await buildClientAssertion({\n appKeys: appKeys!,\n clientId,\n audience: tokenEndpointUrl,\n });\n formParams = {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n ...buildClientAssertionFormParams({ clientId, clientAssertion }),\n };\n } else {\n formParams = {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n ...buildClientSecretFormParams({\n clientId,\n clientSecret: hubspotConnectEnv.hubspotClientSecret,\n }),\n };\n }\n\n const tokenResult = await requestOAuthToken({\n tokenEndpointUrl,\n isDpopEnabled: hubspotConnectEnv.isDpopEnabled,\n ...(dpopProof !== undefined ? { dpopProof } : {}),\n formParams,\n });\n if (!tokenResult.ok) {\n return c.json(\n { error: `Token refresh failed: ${tokenResult.errorText}` },\n 502\n );\n }\n\n const {\n access_token: newAccessToken,\n refresh_token: newRefreshToken,\n expires_in,\n } = tokenResult.body;\n\n if (!newRefreshToken) {\n return c.json({ error: 'Token response missing refresh_token' }, 502);\n }\n if (!isPositiveFiniteNumber(expires_in)) {\n return c.json(\n { error: 'Token response missing or invalid expires_in' },\n 502\n );\n }\n\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n value: newAccessToken,\n path: '/',\n maxAge: expires_in,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: refreshCookieName,\n value: newRefreshToken,\n path: refreshCookiePath,\n maxAge: REFRESH_COOKIE_MAX_AGE_SEC,\n }),\n });\n\n // Cookies prefixed with HUBSPOT_REFRESH_COOKIE_PREFIX that don't match the\n // new refresh cookie name are stale and need to be cleared.\n Object.keys(cookies).forEach((cookieName) => {\n if (\n cookieName.startsWith(HUBSPOT_REFRESH_COOKIE_PREFIX) &&\n cookieName !== refreshCookieName\n ) {\n setResponseCookie({\n c,\n value: serializeCookie({\n name: cookieName,\n value: '',\n path: refreshCookiePath,\n maxAge: 0,\n }),\n });\n }\n });\n\n return c.json({ expires_in });\n}\n"],"mappings":";;;;;;;;AAwBA,eAAsB,kBACpB,GACA,SACA;CACA,MAAM,EAAE,SAAS,mBAAmB,UAAU,sBAAsB;CACpE,MAAM,kBAAkB,EAAE,IAAI,OAAO,oBAAoB,IAAI,KAAA;CAC7D,MAAM,iBAAiB,EAAE,IAAI,OAAO,mBAAmB,IAAI,KAAA;CAC3D,MAAM,oBAAoB,EAAE,IAAI,OAAO,OAAO,IAAI,KAAA;CAClD,MAAM,UAAU,aAAa,EAAE,IAAI,OAAO,SAAS,CAAC;CACpD,MAAM,YAAY,QAAQ;CAC1B,IAAI,CAAC,WACH,OAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;CAGzD,IAAI,kBAAkB,2BAA2B,CAAC,SAChD,OAAO,EAAE,KACP,EACE,OACE,6FACH,EACD,IACD;CAGH,MAAM,UAAU,MAAM,gBAAgB,UAAU;CAChD,MAAM,oBAAoB,GAAG,gCAAgC;CAC7D,MAAM,eAAe,QAAQ;CAC7B,IAAI,CAAC,cACH,OAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,EAAE,IAAI;CAGxD,MAAM,WAAW,kBAAkB,gBAC/B,gCAAgC;EAC9B,YAAY,EAAE,IAAI;EAClB;EACA;EACA;EACA;EACD,CAAC,GACF,kBAAkB;CAEtB,MAAM,mBAAmB,IAAI,IAC3B,mBACA,kBAAkB,sBACnB,CAAC;CAEF,IAAI;CACJ,IAAI,kBAAkB,eACpB,YAAY,MAAM,4BAA4B;EACnC;EACT;EACA,eAAe;EAChB,CAAC;CAGJ,IAAI;CACJ,IAAI,kBAAkB,eAMpB,aAAa;EACX,YAAY;EACZ,eAAe;EACf,GAAG,+BAA+B;GAAE;GAAU,iBAAA,MARlB,qBAAqB;IACxC;IACT;IACA,UAAU;IACX,CAAC;GAI+D,CAAC;EACjE;MAED,aAAa;EACX,YAAY;EACZ,eAAe;EACf,GAAG,4BAA4B;GAC7B;GACA,cAAc,kBAAkB;GACjC,CAAC;EACH;CAGH,MAAM,cAAc,MAAM,kBAAkB;EAC1C;EACA,eAAe,kBAAkB;EACjC,GAAI,cAAc,KAAA,IAAY,EAAE,WAAW,GAAG,EAAE;EAChD;EACD,CAAC;CACF,IAAI,CAAC,YAAY,IACf,OAAO,EAAE,KACP,EAAE,OAAO,yBAAyB,YAAY,aAAa,EAC3D,IACD;CAGH,MAAM,EACJ,cAAc,gBACd,eAAe,iBACf,eACE,YAAY;CAEhB,IAAI,CAAC,iBACH,OAAO,EAAE,KAAK,EAAE,OAAO,wCAAwC,EAAE,IAAI;CAEvE,IAAI,CAAC,uBAAuB,WAAW,EACrC,OAAO,EAAE,KACP,EAAE,OAAO,gDAAgD,EACzD,IACD;CAGH,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,QAAQ;GACT,CAAC;EACH,CAAC;CAIF,OAAO,KAAK,QAAQ,CAAC,SAAS,eAAe;EAC3C,IACE,WAAW,WAAA,cAAyC,IACpD,eAAe,mBAEf,kBAAkB;GAChB;GACA,OAAO,gBAAgB;IACrB,MAAM;IACN,OAAO;IACP,MAAM;IACN,QAAQ;IACT,CAAC;GACH,CAAC;GAEJ;CAEF,OAAO,EAAE,KAAK,EAAE,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"auth-refresh.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/auth-refresh.ts"],"sourcesContent":["import type { Context } from 'hono';\n\nimport {\n HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n HUBSPOT_APP_SID_COOKIE_NAME,\n HUBSPOT_REFRESH_COOKIE_PREFIX,\n} from '../../constants.ts';\nimport { parseCookies } from '../../utils/cookie-utils.ts';\nimport { sha256base64url } from '../../utils/crypto-utils.ts';\nimport { serializeCookie, setResponseCookie } from '../utils/cookie-utils.ts';\nimport { REFRESH_COOKIE_MAX_AGE_SEC } from './constants.ts';\nimport {\n buildClientAssertion,\n buildClientAssertionFormParams,\n buildClientSecretFormParams,\n buildTokenEndpointDpopProof,\n requestOAuthToken,\n} from './oauth-client.ts';\nimport type { HubSpotConnectOAuthRouteOptions } from './types.ts';\nimport {\n buildCimdClientIdUrlFromRequest,\n isPositiveFiniteNumber,\n} from './utils.ts';\n\nexport async function handleAuthRefresh(\n c: Context,\n options: HubSpotConnectOAuthRouteOptions\n) {\n const { appKeys, refreshCookiePath, basePath, hubspotConnectEnv } = options;\n const xForwardedProto = c.req.header('x-forwarded-proto') ?? undefined;\n const xForwardedHost = c.req.header('x-forwarded-host') ?? undefined;\n const requestHostHeader = c.req.header('host') ?? undefined;\n const cookies = parseCookies(c.req.header('Cookie'));\n const sessionId = cookies[HUBSPOT_APP_SID_COOKIE_NAME];\n if (!sessionId) {\n return c.json({ error: 'Missing session cookie' }, 401);\n }\n\n if (hubspotConnectEnv.isAppPrivateKeyRequired && !appKeys) {\n return c.json(\n {\n error:\n 'Server misconfiguration: HUBSPOT_APP_PRIVATE_KEY is required when CIMD or DPoP is enabled',\n },\n 500\n );\n }\n\n const sidHash = await sha256base64url(sessionId);\n const refreshCookieName = `${HUBSPOT_REFRESH_COOKIE_PREFIX}${sidHash}`;\n const refreshToken = cookies[refreshCookieName];\n if (!refreshToken) {\n return c.json({ error: 'Missing refresh token' }, 401);\n }\n\n const clientId = hubspotConnectEnv.isCimdEnabled\n ? buildCimdClientIdUrlFromRequest({\n requestUrl: c.req.url,\n basePath,\n xForwardedProto,\n xForwardedHost,\n requestHostHeader,\n })\n : hubspotConnectEnv.hubspotClientId;\n\n const tokenEndpointUrl = new URL(\n '/oauth/v1/token',\n hubspotConnectEnv.hubspotOAuthApiOrigin\n ).href;\n\n let dpopProof: string | undefined;\n if (hubspotConnectEnv.isDpopEnabled) {\n dpopProof = await buildTokenEndpointDpopProof({\n appKeys: appKeys!,\n tokenEndpointUrl,\n sessionIdHash: sidHash,\n });\n }\n\n let formParams: Record<string, string>;\n if (hubspotConnectEnv.isCimdEnabled) {\n const clientAssertion = await buildClientAssertion({\n appKeys: appKeys!,\n clientId,\n audience: tokenEndpointUrl,\n });\n formParams = {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n ...buildClientAssertionFormParams({ clientId, clientAssertion }),\n };\n } else {\n formParams = {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n ...buildClientSecretFormParams({\n clientId,\n clientSecret: hubspotConnectEnv.hubspotClientSecret,\n }),\n };\n }\n\n const tokenResult = await requestOAuthToken({\n tokenEndpointUrl,\n isDpopEnabled: hubspotConnectEnv.isDpopEnabled,\n ...(dpopProof !== undefined ? { dpopProof } : {}),\n formParams,\n });\n if (!tokenResult.ok) {\n return c.json(\n { error: `Token refresh failed: ${tokenResult.errorText}` },\n 502\n );\n }\n\n const {\n access_token: newAccessToken,\n refresh_token: newRefreshToken,\n expires_in,\n } = tokenResult.body;\n\n if (!newRefreshToken) {\n return c.json({ error: 'Token response missing refresh_token' }, 502);\n }\n if (!isPositiveFiniteNumber(expires_in)) {\n return c.json(\n { error: 'Token response missing or invalid expires_in' },\n 502\n );\n }\n\n setResponseCookie({\n c,\n value: serializeCookie({\n name: HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,\n value: newAccessToken,\n path: '/',\n sameSite: 'None',\n partitioned: true,\n maxAge: expires_in,\n }),\n });\n setResponseCookie({\n c,\n value: serializeCookie({\n name: refreshCookieName,\n value: newRefreshToken,\n path: refreshCookiePath,\n sameSite: 'None',\n partitioned: true,\n maxAge: REFRESH_COOKIE_MAX_AGE_SEC,\n }),\n });\n\n // Cookies prefixed with HUBSPOT_REFRESH_COOKIE_PREFIX that don't match the\n // new refresh cookie name are stale and need to be cleared.\n Object.keys(cookies).forEach((cookieName) => {\n if (\n cookieName.startsWith(HUBSPOT_REFRESH_COOKIE_PREFIX) &&\n cookieName !== refreshCookieName\n ) {\n setResponseCookie({\n c,\n value: serializeCookie({\n name: cookieName,\n value: '',\n path: refreshCookiePath,\n sameSite: 'None',\n partitioned: true,\n maxAge: 0,\n }),\n });\n }\n });\n\n return c.json({ expires_in });\n}\n"],"mappings":";;;;;;;;AAwBA,eAAsB,kBACpB,GACA,SACA;CACA,MAAM,EAAE,SAAS,mBAAmB,UAAU,sBAAsB;CACpE,MAAM,kBAAkB,EAAE,IAAI,OAAO,oBAAoB,IAAI,KAAA;CAC7D,MAAM,iBAAiB,EAAE,IAAI,OAAO,mBAAmB,IAAI,KAAA;CAC3D,MAAM,oBAAoB,EAAE,IAAI,OAAO,OAAO,IAAI,KAAA;CAClD,MAAM,UAAU,aAAa,EAAE,IAAI,OAAO,SAAS,CAAC;CACpD,MAAM,YAAY,QAAQ;CAC1B,IAAI,CAAC,WACH,OAAO,EAAE,KAAK,EAAE,OAAO,0BAA0B,EAAE,IAAI;CAGzD,IAAI,kBAAkB,2BAA2B,CAAC,SAChD,OAAO,EAAE,KACP,EACE,OACE,6FACH,EACD,IACD;CAGH,MAAM,UAAU,MAAM,gBAAgB,UAAU;CAChD,MAAM,oBAAoB,GAAG,gCAAgC;CAC7D,MAAM,eAAe,QAAQ;CAC7B,IAAI,CAAC,cACH,OAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,EAAE,IAAI;CAGxD,MAAM,WAAW,kBAAkB,gBAC/B,gCAAgC;EAC9B,YAAY,EAAE,IAAI;EAClB;EACA;EACA;EACA;EACD,CAAC,GACF,kBAAkB;CAEtB,MAAM,mBAAmB,IAAI,IAC3B,mBACA,kBAAkB,sBACnB,CAAC;CAEF,IAAI;CACJ,IAAI,kBAAkB,eACpB,YAAY,MAAM,4BAA4B;EACnC;EACT;EACA,eAAe;EAChB,CAAC;CAGJ,IAAI;CACJ,IAAI,kBAAkB,eAMpB,aAAa;EACX,YAAY;EACZ,eAAe;EACf,GAAG,+BAA+B;GAAE;GAAU,iBAAA,MARlB,qBAAqB;IACxC;IACT;IACA,UAAU;IACX,CAAC;GAI+D,CAAC;EACjE;MAED,aAAa;EACX,YAAY;EACZ,eAAe;EACf,GAAG,4BAA4B;GAC7B;GACA,cAAc,kBAAkB;GACjC,CAAC;EACH;CAGH,MAAM,cAAc,MAAM,kBAAkB;EAC1C;EACA,eAAe,kBAAkB;EACjC,GAAI,cAAc,KAAA,IAAY,EAAE,WAAW,GAAG,EAAE;EAChD;EACD,CAAC;CACF,IAAI,CAAC,YAAY,IACf,OAAO,EAAE,KACP,EAAE,OAAO,yBAAyB,YAAY,aAAa,EAC3D,IACD;CAGH,MAAM,EACJ,cAAc,gBACd,eAAe,iBACf,eACE,YAAY;CAEhB,IAAI,CAAC,iBACH,OAAO,EAAE,KAAK,EAAE,OAAO,wCAAwC,EAAE,IAAI;CAEvE,IAAI,CAAC,uBAAuB,WAAW,EACrC,OAAO,EAAE,KACP,EAAE,OAAO,gDAAgD,EACzD,IACD;CAGH,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CACF,kBAAkB;EAChB;EACA,OAAO,gBAAgB;GACrB,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACb,QAAQ;GACT,CAAC;EACH,CAAC;CAIF,OAAO,KAAK,QAAQ,CAAC,SAAS,eAAe;EAC3C,IACE,WAAW,WAAA,cAAyC,IACpD,eAAe,mBAEf,kBAAkB;GAChB;GACA,OAAO,gBAAgB;IACrB,MAAM;IACN,OAAO;IACP,MAAM;IACN,UAAU;IACV,aAAa;IACb,QAAQ;IACT,CAAC;GACH,CAAC;GAEJ;CAEF,OAAO,EAAE,KAAK,EAAE,YAAY,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertHubSpotConnectCimdClientMetadata } from "./cimd-client-metadata-types.js";
|
|
2
2
|
import { noopLogger } from "../../shared/logger.js";
|
|
3
|
-
import {
|
|
3
|
+
import { corsMiddleware } from "../utils/cors-middleware.js";
|
|
4
|
+
import { handleAuthComplete } from "./auth-complete.js";
|
|
4
5
|
import { handleAuthInitSession } from "./auth-init-session.js";
|
|
5
6
|
import { handleAuthLogout } from "./auth-logout.js";
|
|
6
7
|
import { handleAuthRefresh } from "./auth-refresh.js";
|
|
@@ -22,10 +23,11 @@ function registerHubSpotConnectRoutes(options) {
|
|
|
22
23
|
hubspotConnectEnv,
|
|
23
24
|
cimdClientMetadata
|
|
24
25
|
};
|
|
26
|
+
app.use("*", corsMiddleware());
|
|
25
27
|
app.get("/client.json", (c) => handleCimdClientJson(c, oauthRouteOptions));
|
|
26
28
|
if (hubspotConnectEnv.isCimdEnabled) app.get("/jwks.json", (c) => handleCimdAppJwks(c, oauthRouteOptions));
|
|
27
29
|
app.get("/auth/init-session", (c) => handleAuthInitSession(c, oauthRouteOptions));
|
|
28
|
-
app.
|
|
30
|
+
app.post("/auth/complete", (c) => handleAuthComplete(c, oauthRouteOptions));
|
|
29
31
|
app.post("/auth/refresh", (c) => handleAuthRefresh(c, oauthRouteOptions));
|
|
30
32
|
app.post("/auth/logout", (c) => handleAuthLogout(c, oauthRouteOptions));
|
|
31
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hubspot-connect-routes.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/hubspot-connect-routes.ts"],"sourcesContent":["import type { Hono } from 'hono';\n\nimport { noopLogger, type Logger } from '../../../shared/logger.ts';\nimport type { AppKeys } from '../../types.ts';\nimport {
|
|
1
|
+
{"version":3,"file":"hubspot-connect-routes.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/hubspot-connect-routes.ts"],"sourcesContent":["import type { Hono } from 'hono';\n\nimport { noopLogger, type Logger } from '../../../shared/logger.ts';\nimport type { AppKeys } from '../../types.ts';\nimport { corsMiddleware } from '../utils/cors-middleware.ts';\nimport { handleAuthComplete } from './auth-complete.ts';\nimport { handleAuthInitSession } from './auth-init-session.ts';\nimport { handleAuthLogout } from './auth-logout.ts';\nimport { handleAuthRefresh } from './auth-refresh.ts';\nimport { assertHubSpotConnectCimdClientMetadata } from './cimd-client-metadata-types.ts';\nimport type { HubSpotConnectCimdClientMetadata } from './cimd-client-metadata-types.ts';\nimport {\n handleCimdAppJwks,\n handleCimdClientJson,\n} from './cimd-public-routes.ts';\nimport type { HubSpotConnectRoutesEnv } from './load-hubspot-connect-routes-env.ts';\n\n/**\n * Options accepted by {@link registerHubSpotConnectRoutes}.\n */\nexport interface RegisterHubSpotConnectRoutesOptions {\n /** The Hono app to mount the OAuth routes on. */\n app: Hono;\n /**\n * Imported app keys from `secureStart`, or `null` when CIMD and DPoP\n * are both disabled.\n */\n appKeys: AppKeys | null;\n /**\n * Path the routes are mounted under (no trailing slash). Used to\n * scope refresh-token cookies via `Path=${basePath}/auth`.\n */\n basePath: string;\n /**\n * OAuth and client-mode settings, typically from\n * {@link loadHubSpotConnectRoutesEnv}.\n */\n hubspotConnectEnv: HubSpotConnectRoutesEnv;\n /**\n * Scope configuration for `GET /client.json` and for authorize URL\n * scopes when CIMD is off. Always required.\n */\n cimdClientMetadata: HubSpotConnectCimdClientMetadata;\n /**\n * Optional logger. When omitted the SDK uses a no-op logger so\n * server-side state never leaks into the host application's\n * console.\n */\n logger?: Logger;\n}\n\n/**\n * Mounts hubspot-connect routes: OAuth (`/auth/...`); `GET /client.json`\n * from `cimdClientMetadata`; `GET /jwks.json` when CIMD is enabled.\n */\nexport function registerHubSpotConnectRoutes(\n options: RegisterHubSpotConnectRoutesOptions\n): void {\n const {\n app,\n appKeys,\n basePath,\n hubspotConnectEnv,\n cimdClientMetadata,\n logger = noopLogger,\n } = options;\n\n if (!cimdClientMetadata) {\n throw new Error(\n 'registerHubSpotConnectRoutes: cimdClientMetadata is required'\n );\n }\n assertHubSpotConnectCimdClientMetadata(cimdClientMetadata);\n\n const refreshCookiePath = `${basePath}/auth`;\n const oauthRouteOptions = {\n appKeys,\n refreshCookiePath,\n logger,\n basePath,\n hubspotConnectEnv,\n cimdClientMetadata,\n };\n\n // Credentialed CORS for the cross-origin Lovable / Supabase shape.\n // Echoes the request `Origin` (or the pinned `__Host-hs_app_origin`\n // cookie value once init-session has run) and short-circuits OPTIONS\n // preflights with a 204 before any route handler runs.\n app.use('*', corsMiddleware());\n\n app.get('/client.json', (c) => handleCimdClientJson(c, oauthRouteOptions));\n if (hubspotConnectEnv.isCimdEnabled) {\n app.get('/jwks.json', (c) => handleCimdAppJwks(c, oauthRouteOptions));\n }\n\n app.get('/auth/init-session', (c) =>\n handleAuthInitSession(c, oauthRouteOptions)\n );\n app.post('/auth/complete', (c) => handleAuthComplete(c, oauthRouteOptions));\n app.post('/auth/refresh', (c) => handleAuthRefresh(c, oauthRouteOptions));\n app.post('/auth/logout', (c) => handleAuthLogout(c, oauthRouteOptions));\n}\n"],"mappings":";;;;;;;;;;;;;AAuDA,SAAgB,6BACd,SACM;CACN,MAAM,EACJ,KACA,SACA,UACA,mBACA,oBACA,SAAS,eACP;CAEJ,IAAI,CAAC,oBACH,MAAM,IAAI,MACR,+DACD;CAEH,uCAAuC,mBAAmB;CAG1D,MAAM,oBAAoB;EACxB;EACA,mBAAA,GAH2B,SAAS;EAIpC;EACA;EACA;EACA;EACD;CAMD,IAAI,IAAI,KAAK,gBAAgB,CAAC;CAE9B,IAAI,IAAI,iBAAiB,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;CAC1E,IAAI,kBAAkB,eACpB,IAAI,IAAI,eAAe,MAAM,kBAAkB,GAAG,kBAAkB,CAAC;CAGvE,IAAI,IAAI,uBAAuB,MAC7B,sBAAsB,GAAG,kBAAkB,CAC5C;CACD,IAAI,KAAK,mBAAmB,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;CAC3E,IAAI,KAAK,kBAAkB,MAAM,kBAAkB,GAAG,kBAAkB,CAAC;CACzE,IAAI,KAAK,iBAAiB,MAAM,iBAAiB,GAAG,kBAAkB,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HUBSPOT_FRONTEND_CALLBACK_PATH } from "../../shared/constants.js";
|
|
1
2
|
import { serializeCookie } from "../utils/cookie-utils.js";
|
|
2
3
|
//#region src/server/hono/hubspot-connect-routes/utils.ts
|
|
3
4
|
function clearTempCookie(name) {
|
|
@@ -5,10 +6,56 @@ function clearTempCookie(name) {
|
|
|
5
6
|
name,
|
|
6
7
|
value: "",
|
|
7
8
|
path: "/",
|
|
8
|
-
sameSite: "
|
|
9
|
-
maxAge: 0
|
|
9
|
+
sameSite: "None",
|
|
10
|
+
maxAge: 0,
|
|
11
|
+
partitioned: true
|
|
10
12
|
});
|
|
11
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Parses the request `Origin` header into the canonical origin
|
|
16
|
+
* string (`URL.origin`) or returns `null` when the header is
|
|
17
|
+
* missing, malformed, or carries a scheme/host the SDK does not
|
|
18
|
+
* accept.
|
|
19
|
+
*
|
|
20
|
+
* Accepted shapes:
|
|
21
|
+
*
|
|
22
|
+
* - `https://<host>` for production deployments.
|
|
23
|
+
* - `http://localhost[:<port>]` and `http://127.0.0.1[:<port>]`
|
|
24
|
+
* for local development; browsers exempt these from the `Secure`
|
|
25
|
+
* cookie restriction.
|
|
26
|
+
*
|
|
27
|
+
* Rejects values with a path/query/hash component (the request
|
|
28
|
+
* `Origin` header is by spec a bare origin, so anything else
|
|
29
|
+
* indicates a malformed or hostile request).
|
|
30
|
+
*/
|
|
31
|
+
function parseAppOriginHeader(originHeader) {
|
|
32
|
+
if (!originHeader) return null;
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = new URL(originHeader);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (parsed.pathname !== "/" && parsed.pathname !== "") return null;
|
|
40
|
+
if (parsed.search !== "" || parsed.hash !== "") return null;
|
|
41
|
+
if (parsed.protocol === "https:") return parsed.origin;
|
|
42
|
+
if (parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1")) return parsed.origin;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* OAuth `redirect_uri` for the cross-origin app shape: the OAuth
|
|
47
|
+
* callback lands on the **frontend** origin (not the SDK's edge
|
|
48
|
+
* function host), so all cookies set by `init-session` and read by
|
|
49
|
+
* `auth/complete` live in the same `(frontend, edge)` CHIPS
|
|
50
|
+
* partition.
|
|
51
|
+
*
|
|
52
|
+
* Used by `auth/init-session` (when building `authorization_url`)
|
|
53
|
+
* and `auth/complete` (which must rebuild the same value to satisfy
|
|
54
|
+
* the OAuth token endpoint's `redirect_uri` check).
|
|
55
|
+
*/
|
|
56
|
+
function buildFrontendOAuthRedirectUri(appOrigin) {
|
|
57
|
+
return `${appOrigin}${HUBSPOT_FRONTEND_CALLBACK_PATH}`;
|
|
58
|
+
}
|
|
12
59
|
function isSafeReturnPath(rawPath) {
|
|
13
60
|
if (!rawPath.startsWith("/")) return false;
|
|
14
61
|
if (rawPath.includes("\0")) return false;
|
|
@@ -68,6 +115,6 @@ function isPositiveFiniteNumber(value) {
|
|
|
68
115
|
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
69
116
|
}
|
|
70
117
|
//#endregion
|
|
71
|
-
export { buildCimdClientIdUrlFromRequest, buildHubSpotAppJwksUrlFromRequest, buildOAuthRedirectUriFromRequest, clearTempCookie, isPositiveFiniteNumber, isSafeReturnPath };
|
|
118
|
+
export { buildCimdClientIdUrlFromRequest, buildFrontendOAuthRedirectUri, buildHubSpotAppJwksUrlFromRequest, buildOAuthRedirectUriFromRequest, clearTempCookie, isPositiveFiniteNumber, isSafeReturnPath, parseAppOriginHeader };
|
|
72
119
|
|
|
73
120
|
//# sourceMappingURL=utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/utils.ts"],"sourcesContent":["import { serializeCookie } from '../utils/cookie-utils.ts';\n\nexport function clearTempCookie(name: string): string {\n return serializeCookie({\n name,\n value: '',\n path: '/',\n sameSite: '
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../../../src/server/hono/hubspot-connect-routes/utils.ts"],"sourcesContent":["import { HUBSPOT_FRONTEND_CALLBACK_PATH } from '../../../shared/constants.ts';\nimport { serializeCookie } from '../utils/cookie-utils.ts';\n\nexport function clearTempCookie(name: string): string {\n return serializeCookie({\n name,\n value: '',\n path: '/',\n sameSite: 'None',\n maxAge: 0,\n partitioned: true,\n });\n}\n\n/**\n * Parses the request `Origin` header into the canonical origin\n * string (`URL.origin`) or returns `null` when the header is\n * missing, malformed, or carries a scheme/host the SDK does not\n * accept.\n *\n * Accepted shapes:\n *\n * - `https://<host>` for production deployments.\n * - `http://localhost[:<port>]` and `http://127.0.0.1[:<port>]`\n * for local development; browsers exempt these from the `Secure`\n * cookie restriction.\n *\n * Rejects values with a path/query/hash component (the request\n * `Origin` header is by spec a bare origin, so anything else\n * indicates a malformed or hostile request).\n */\nexport function parseAppOriginHeader(\n originHeader: string | undefined\n): string | null {\n if (!originHeader) return null;\n let parsed: URL;\n try {\n parsed = new URL(originHeader);\n } catch {\n return null;\n }\n if (parsed.pathname !== '/' && parsed.pathname !== '') return null;\n if (parsed.search !== '' || parsed.hash !== '') return null;\n if (parsed.protocol === 'https:') return parsed.origin;\n if (\n parsed.protocol === 'http:' &&\n (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1')\n ) {\n return parsed.origin;\n }\n return null;\n}\n\n/**\n * OAuth `redirect_uri` for the cross-origin app shape: the OAuth\n * callback lands on the **frontend** origin (not the SDK's edge\n * function host), so all cookies set by `init-session` and read by\n * `auth/complete` live in the same `(frontend, edge)` CHIPS\n * partition.\n *\n * Used by `auth/init-session` (when building `authorization_url`)\n * and `auth/complete` (which must rebuild the same value to satisfy\n * the OAuth token endpoint's `redirect_uri` check).\n */\nexport function buildFrontendOAuthRedirectUri(appOrigin: string): string {\n return `${appOrigin}${HUBSPOT_FRONTEND_CALLBACK_PATH}`;\n}\n\nexport function isSafeReturnPath(rawPath: string): boolean {\n if (!rawPath.startsWith('/')) return false;\n if (rawPath.includes('\\0')) return false;\n let decoded: string;\n try {\n decoded = decodeURIComponent(rawPath);\n } catch {\n return false;\n }\n if (!decoded.startsWith('/')) return false;\n const second = decoded.charAt(1);\n if (second === '/' || second === '\\\\') return false;\n return true;\n}\n\nexport function getRequestHost(requestUrl: string): string {\n return new URL(requestUrl).host;\n}\n\nexport interface GetRequestHostForHubspotConnectOptions {\n requestUrl: string;\n xForwardedHost?: string | undefined;\n /** `Host` when `X-Forwarded-Host` is absent (some proxies only set `X-Forwarded-Proto`). */\n requestHostHeader?: string | undefined;\n}\n\n/**\n * Host for CIMD `client_id` URLs when hubspot-connect sits behind a reverse\n * proxy (e.g. Vite → Deno): prefers `X-Forwarded-Host`, then `Host`, then the\n * request URL host.\n */\nexport function getRequestHostForHubspotConnect(\n options: GetRequestHostForHubspotConnectOptions\n): string {\n const rawForwarded = options.xForwardedHost?.split(',')[0]?.trim();\n if (rawForwarded) {\n try {\n return new URL(`https://${rawForwarded}`).host;\n } catch {\n /* invalid forwarded host */\n }\n }\n const rawHost = options.requestHostHeader?.split(',')[0]?.trim();\n if (rawHost) {\n try {\n return new URL(`https://${rawHost}`).host;\n } catch {\n /* invalid host header */\n }\n }\n return getRequestHost(options.requestUrl);\n}\n\nexport interface BuildOAuthRedirectUriFromRequestOptions {\n requestUrl: string;\n basePath: string;\n xForwardedProto?: string | undefined;\n xForwardedHost?: string | undefined;\n /** `Host` when `X-Forwarded-Host` is absent but `X-Forwarded-Proto` is set. */\n requestHostHeader?: string | undefined;\n}\n\nfunction normalizeHubSpotConnectBasePath(basePath: string): string {\n return basePath.endsWith('/') && basePath.length > 1\n ? basePath.slice(0, -1)\n : basePath;\n}\n\n/**\n * Public origin for hubspot-connect URLs (`redirect_uri`, CIMD `client_id`,\n * `jwks_uri`). Matches the host/proto rules used for the OAuth callback.\n */\nexport function buildHubSpotConnectRequestOrigin(\n options: BuildOAuthRedirectUriFromRequestOptions\n): string {\n const { requestUrl, xForwardedProto, xForwardedHost, requestHostHeader } =\n options;\n const proto = xForwardedProto?.split(',')[0]?.trim();\n if (proto && (proto === 'http' || proto === 'https')) {\n const forwardedHost = xForwardedHost?.split(',')[0]?.trim();\n const hostHeader = requestHostHeader?.split(',')[0]?.trim();\n const hostPart = forwardedHost || hostHeader || new URL(requestUrl).host;\n return `${proto}://${hostPart}`;\n }\n return new URL(requestUrl).origin;\n}\n\n/**\n * OAuth `redirect_uri` for the hubspot-connect callback. Uses\n * `X-Forwarded-Proto` with `X-Forwarded-Host`, then `Host`, then the request URL\n * host when the proto is forwarded (reverse proxy); otherwise the request URL\n * origin.\n */\nexport function buildOAuthRedirectUriFromRequest(\n options: BuildOAuthRedirectUriFromRequestOptions\n): string {\n const trimmed = normalizeHubSpotConnectBasePath(options.basePath);\n const origin = buildHubSpotConnectRequestOrigin(options);\n return `${origin}${trimmed}/auth/callback`;\n}\n\n/**\n * CIMD `client_id` URL: `{origin}{basePath}/client.json`.\n */\nexport function buildCimdClientIdUrlFromRequest(\n options: BuildOAuthRedirectUriFromRequestOptions\n): string {\n const trimmed = normalizeHubSpotConnectBasePath(options.basePath);\n const origin = buildHubSpotConnectRequestOrigin(options);\n return `${origin}${trimmed}/client.json`;\n}\n\n/**\n * App JWKS URL published in CIMD: `{origin}{basePath}/jwks.json`.\n */\nexport function buildHubSpotAppJwksUrlFromRequest(\n options: BuildOAuthRedirectUriFromRequestOptions\n): string {\n const trimmed = normalizeHubSpotConnectBasePath(options.basePath);\n const origin = buildHubSpotConnectRequestOrigin(options);\n return `${origin}${trimmed}/jwks.json`;\n}\n\nexport function isPositiveFiniteNumber(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n"],"mappings":";;;AAGA,SAAgB,gBAAgB,MAAsB;CACpD,OAAO,gBAAgB;EACrB;EACA,OAAO;EACP,MAAM;EACN,UAAU;EACV,QAAQ;EACR,aAAa;EACd,CAAC;;;;;;;;;;;;;;;;;;;AAoBJ,SAAgB,qBACd,cACe;CACf,IAAI,CAAC,cAAc,OAAO;CAC1B,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,aAAa;SACxB;EACN,OAAO;;CAET,IAAI,OAAO,aAAa,OAAO,OAAO,aAAa,IAAI,OAAO;CAC9D,IAAI,OAAO,WAAW,MAAM,OAAO,SAAS,IAAI,OAAO;CACvD,IAAI,OAAO,aAAa,UAAU,OAAO,OAAO;CAChD,IACE,OAAO,aAAa,YACnB,OAAO,aAAa,eAAe,OAAO,aAAa,cAExD,OAAO,OAAO;CAEhB,OAAO;;;;;;;;;;;;;AAcT,SAAgB,8BAA8B,WAA2B;CACvE,OAAO,GAAG,YAAY;;AAGxB,SAAgB,iBAAiB,SAA0B;CACzD,IAAI,CAAC,QAAQ,WAAW,IAAI,EAAE,OAAO;CACrC,IAAI,QAAQ,SAAS,KAAK,EAAE,OAAO;CACnC,IAAI;CACJ,IAAI;EACF,UAAU,mBAAmB,QAAQ;SAC/B;EACN,OAAO;;CAET,IAAI,CAAC,QAAQ,WAAW,IAAI,EAAE,OAAO;CACrC,MAAM,SAAS,QAAQ,OAAO,EAAE;CAChC,IAAI,WAAW,OAAO,WAAW,MAAM,OAAO;CAC9C,OAAO;;AAkDT,SAAS,gCAAgC,UAA0B;CACjE,OAAO,SAAS,SAAS,IAAI,IAAI,SAAS,SAAS,IAC/C,SAAS,MAAM,GAAG,GAAG,GACrB;;;;;;AAON,SAAgB,iCACd,SACQ;CACR,MAAM,EAAE,YAAY,iBAAiB,gBAAgB,sBACnD;CACF,MAAM,QAAQ,iBAAiB,MAAM,IAAI,CAAC,IAAI,MAAM;CACpD,IAAI,UAAU,UAAU,UAAU,UAAU,UAAU;EACpD,MAAM,gBAAgB,gBAAgB,MAAM,IAAI,CAAC,IAAI,MAAM;EAC3D,MAAM,aAAa,mBAAmB,MAAM,IAAI,CAAC,IAAI,MAAM;EAE3D,OAAO,GAAG,MAAM,KADC,iBAAiB,cAAc,IAAI,IAAI,WAAW,CAAC;;CAGtE,OAAO,IAAI,IAAI,WAAW,CAAC;;;;;;;;AAS7B,SAAgB,iCACd,SACQ;CACR,MAAM,UAAU,gCAAgC,QAAQ,SAAS;CAEjE,OAAO,GADQ,iCAAiC,QAChC,GAAG,QAAQ;;;;;AAM7B,SAAgB,gCACd,SACQ;CACR,MAAM,UAAU,gCAAgC,QAAQ,SAAS;CAEjE,OAAO,GADQ,iCAAiC,QAChC,GAAG,QAAQ;;;;;AAM7B,SAAgB,kCACd,SACQ;CACR,MAAM,UAAU,gCAAgC,QAAQ,SAAS;CAEjE,OAAO,GADQ,iCAAiC,QAChC,GAAG,QAAQ;;AAG7B,SAAgB,uBAAuB,OAAiC;CACtE,OAAO,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM,IAAI,QAAQ"}
|
|
@@ -2,23 +2,27 @@ import { HubSpotClient } from "../api-client-core/types.js";
|
|
|
2
2
|
import { HubSpotProxy } from "../types.js";
|
|
3
3
|
|
|
4
4
|
//#region src/server/hono/types.d.ts
|
|
5
|
-
interface
|
|
5
|
+
interface AppConnectRequestContext {
|
|
6
|
+
/**
|
|
7
|
+
* HubSpot API proxy.
|
|
8
|
+
*/
|
|
9
|
+
proxy: HubSpotProxy;
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
8
|
-
* API on behalf of the browser session that made the inbound
|
|
9
|
-
* request. `authenticated: false` when the session cookies are
|
|
10
|
-
* absent or invalid.
|
|
11
|
+
* HubSpot API client.
|
|
11
12
|
*/
|
|
12
|
-
|
|
13
|
+
client: HubSpotClient;
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
+
* Whether the browser session is authenticated.
|
|
15
16
|
*/
|
|
16
|
-
|
|
17
|
+
authenticated: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface AppConnectHonoBindings {
|
|
20
|
+
hubSpot: AppConnectRequestContext;
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Hono environment shape used by handlers running inside a hubspot-
|
|
20
24
|
* connect request handler. Exposes the per-request
|
|
21
|
-
* {@link
|
|
25
|
+
* {@link AppConnectRequestContext} as `c.env.hubSpot`.
|
|
22
26
|
*/
|
|
23
27
|
interface AppConnectHonoEnv {
|
|
24
28
|
Bindings: AppConnectHonoBindings;
|