@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
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { EXPIRES_AT_URL_PARAM } from '../../../shared/constants.ts';
|
|
5
|
-
import {
|
|
6
|
-
HUBSPOT_ACCESS_TOKEN_COOKIE_NAME,
|
|
7
|
-
HUBSPOT_REFRESH_COOKIE_PREFIX,
|
|
8
|
-
TEMP_COOKIE_OAUTH_STATE,
|
|
9
|
-
TEMP_COOKIE_PKCE_VERIFIER,
|
|
10
|
-
} from '../../constants.ts';
|
|
11
|
-
import { handleAuthCallback } from './auth-callback.ts';
|
|
12
|
-
import type { HubSpotConnectRoutesEnvClientSecret } from './load-hubspot-connect-routes-env.ts';
|
|
13
|
-
import type { HubSpotConnectOAuthRouteOptions } from './types.ts';
|
|
14
|
-
|
|
15
|
-
const hubspotConnectEnv = {
|
|
16
|
-
hubspotAuthorizationEndpoint: 'https://auth.example.test/oauth/authorize',
|
|
17
|
-
hubspotOAuthApiOrigin: 'https://auth.example.test',
|
|
18
|
-
isCimdEnabled: false,
|
|
19
|
-
isDpopEnabled: false,
|
|
20
|
-
isAppPrivateKeyRequired: false,
|
|
21
|
-
hubspotClientId: 'test-client-id',
|
|
22
|
-
hubspotClientSecret: 'test-client-secret',
|
|
23
|
-
} satisfies HubSpotConnectRoutesEnvClientSecret;
|
|
24
|
-
|
|
25
|
-
const BASE_PATH = '/functions/v1/hubspot-connect';
|
|
26
|
-
|
|
27
|
-
function buildOAuthRouteOptions(): HubSpotConnectOAuthRouteOptions {
|
|
28
|
-
return {
|
|
29
|
-
appKeys: null,
|
|
30
|
-
refreshCookiePath: `${BASE_PATH}/auth`,
|
|
31
|
-
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
32
|
-
basePath: BASE_PATH,
|
|
33
|
-
hubspotConnectEnv,
|
|
34
|
-
cimdClientMetadata: { scope: { required: ['crm.objects.contacts.read'] } },
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function base64urlEncode(input: string): string {
|
|
39
|
-
let binary = '';
|
|
40
|
-
const bytes = new TextEncoder().encode(input);
|
|
41
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
42
|
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function buildCallbackFixture(
|
|
46
|
-
returnPath: string,
|
|
47
|
-
sid = 'test-session-id-hash'
|
|
48
|
-
): { stateValue: string; cookieHeader: string } {
|
|
49
|
-
const stateValue = base64urlEncode(
|
|
50
|
-
JSON.stringify({ return_path: returnPath, sid })
|
|
51
|
-
);
|
|
52
|
-
const cookieHeader = [
|
|
53
|
-
`${TEMP_COOKIE_OAUTH_STATE}=${encodeURIComponent(stateValue)}`,
|
|
54
|
-
`${TEMP_COOKIE_PKCE_VERIFIER}=${encodeURIComponent('test-pkce-verifier')}`,
|
|
55
|
-
].join('; ');
|
|
56
|
-
return { stateValue, cookieHeader };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function buildCallbackUrl(stateValue: string, code = 'test-auth-code'): string {
|
|
60
|
-
const url = new URL(`http://localhost${BASE_PATH}/auth/callback`);
|
|
61
|
-
url.searchParams.set('code', code);
|
|
62
|
-
url.searchParams.set('state', stateValue);
|
|
63
|
-
return url.toString();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
describe('handleAuthCallback', () => {
|
|
67
|
-
afterEach(() => {
|
|
68
|
-
vi.restoreAllMocks();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('returns 400 when code is missing', async () => {
|
|
72
|
-
const app = new Hono();
|
|
73
|
-
app.get('/auth/callback', (c) =>
|
|
74
|
-
handleAuthCallback(c, buildOAuthRouteOptions())
|
|
75
|
-
);
|
|
76
|
-
const res = await app.request(`http://localhost/auth/callback?state=abc`, {
|
|
77
|
-
method: 'GET',
|
|
78
|
-
});
|
|
79
|
-
expect(res.status).toBe(400);
|
|
80
|
-
expect(await res.text()).toContain('Missing code or state');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('returns 400 when state is missing', async () => {
|
|
84
|
-
const app = new Hono();
|
|
85
|
-
app.get('/auth/callback', (c) =>
|
|
86
|
-
handleAuthCallback(c, buildOAuthRouteOptions())
|
|
87
|
-
);
|
|
88
|
-
const res = await app.request(`http://localhost/auth/callback?code=abc`, {
|
|
89
|
-
method: 'GET',
|
|
90
|
-
});
|
|
91
|
-
expect(res.status).toBe(400);
|
|
92
|
-
expect(await res.text()).toContain('Missing code or state');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('returns 403 when state cookie is missing', async () => {
|
|
96
|
-
const app = new Hono();
|
|
97
|
-
app.get(`${BASE_PATH}/auth/callback`, (c) =>
|
|
98
|
-
handleAuthCallback(c, buildOAuthRouteOptions())
|
|
99
|
-
);
|
|
100
|
-
const { stateValue } = buildCallbackFixture('/dashboard');
|
|
101
|
-
const res = await app.request(buildCallbackUrl(stateValue), {
|
|
102
|
-
method: 'GET',
|
|
103
|
-
});
|
|
104
|
-
expect(res.status).toBe(403);
|
|
105
|
-
expect(await res.text()).toContain('State mismatch');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('returns 403 when state does not match cookie', async () => {
|
|
109
|
-
const app = new Hono();
|
|
110
|
-
app.get(`${BASE_PATH}/auth/callback`, (c) =>
|
|
111
|
-
handleAuthCallback(c, buildOAuthRouteOptions())
|
|
112
|
-
);
|
|
113
|
-
const { cookieHeader } = buildCallbackFixture('/dashboard');
|
|
114
|
-
const res = await app.request(buildCallbackUrl('wrong-state-value'), {
|
|
115
|
-
method: 'GET',
|
|
116
|
-
headers: { Cookie: cookieHeader },
|
|
117
|
-
});
|
|
118
|
-
expect(res.status).toBe(403);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('returns 400 when state payload has invalid return_path', async () => {
|
|
122
|
-
const app = new Hono();
|
|
123
|
-
app.get(`${BASE_PATH}/auth/callback`, (c) =>
|
|
124
|
-
handleAuthCallback(c, buildOAuthRouteOptions())
|
|
125
|
-
);
|
|
126
|
-
const { stateValue, cookieHeader } =
|
|
127
|
-
buildCallbackFixture('//evil.example.com');
|
|
128
|
-
const res = await app.request(buildCallbackUrl(stateValue), {
|
|
129
|
-
method: 'GET',
|
|
130
|
-
headers: { Cookie: cookieHeader },
|
|
131
|
-
});
|
|
132
|
-
expect(res.status).toBe(400);
|
|
133
|
-
expect(await res.text()).toContain('Invalid return path');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('returns 502 when upstream token exchange fails', async () => {
|
|
137
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
138
|
-
new Response('{"error":"invalid_grant"}', {
|
|
139
|
-
status: 400,
|
|
140
|
-
statusText: 'Bad Request',
|
|
141
|
-
})
|
|
142
|
-
);
|
|
143
|
-
const app = new Hono();
|
|
144
|
-
app.get(`${BASE_PATH}/auth/callback`, (c) =>
|
|
145
|
-
handleAuthCallback(c, buildOAuthRouteOptions())
|
|
146
|
-
);
|
|
147
|
-
const { stateValue, cookieHeader } = buildCallbackFixture('/dashboard');
|
|
148
|
-
const res = await app.request(buildCallbackUrl(stateValue), {
|
|
149
|
-
method: 'GET',
|
|
150
|
-
headers: { Cookie: cookieHeader },
|
|
151
|
-
});
|
|
152
|
-
expect(res.status).toBe(502);
|
|
153
|
-
expect(await res.text()).toContain('Token exchange failed');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('returns 502 when token response is missing refresh_token', async () => {
|
|
157
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
158
|
-
new Response(JSON.stringify({ access_token: 'at', expires_in: 1800 }), {
|
|
159
|
-
status: 200,
|
|
160
|
-
})
|
|
161
|
-
);
|
|
162
|
-
const app = new Hono();
|
|
163
|
-
app.get(`${BASE_PATH}/auth/callback`, (c) =>
|
|
164
|
-
handleAuthCallback(c, buildOAuthRouteOptions())
|
|
165
|
-
);
|
|
166
|
-
const { stateValue, cookieHeader } = buildCallbackFixture('/dashboard');
|
|
167
|
-
const res = await app.request(buildCallbackUrl(stateValue), {
|
|
168
|
-
method: 'GET',
|
|
169
|
-
headers: { Cookie: cookieHeader },
|
|
170
|
-
});
|
|
171
|
-
expect(res.status).toBe(502);
|
|
172
|
-
expect(await res.text()).toContain('refresh_token');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('redirects and sets cookies on success', async () => {
|
|
176
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
177
|
-
new Response(
|
|
178
|
-
JSON.stringify({
|
|
179
|
-
access_token: 'new-access-token',
|
|
180
|
-
refresh_token: 'new-refresh-token',
|
|
181
|
-
expires_in: 1800,
|
|
182
|
-
}),
|
|
183
|
-
{ status: 200 }
|
|
184
|
-
)
|
|
185
|
-
);
|
|
186
|
-
const app = new Hono();
|
|
187
|
-
app.get(`${BASE_PATH}/auth/callback`, (c) =>
|
|
188
|
-
handleAuthCallback(c, buildOAuthRouteOptions())
|
|
189
|
-
);
|
|
190
|
-
const { stateValue, cookieHeader } = buildCallbackFixture(
|
|
191
|
-
'/dashboard',
|
|
192
|
-
'abc123sid'
|
|
193
|
-
);
|
|
194
|
-
const res = await app.request(buildCallbackUrl(stateValue), {
|
|
195
|
-
method: 'GET',
|
|
196
|
-
headers: { Cookie: cookieHeader },
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
expect(res.status).toBe(302);
|
|
200
|
-
const location = res.headers.get('location') ?? '';
|
|
201
|
-
expect(location).toContain('/dashboard');
|
|
202
|
-
expect(location).toContain(EXPIRES_AT_URL_PARAM);
|
|
203
|
-
|
|
204
|
-
const setCookies = res.headers.getSetCookie();
|
|
205
|
-
const accessCookie = setCookies.find((h) =>
|
|
206
|
-
h.startsWith(`${HUBSPOT_ACCESS_TOKEN_COOKIE_NAME}=`)
|
|
207
|
-
);
|
|
208
|
-
expect(accessCookie).toContain('new-access-token');
|
|
209
|
-
|
|
210
|
-
const refreshCookie = setCookies.find((h) =>
|
|
211
|
-
h.startsWith(`${HUBSPOT_REFRESH_COOKIE_PREFIX}`)
|
|
212
|
-
);
|
|
213
|
-
expect(refreshCookie).toContain('new-refresh-token');
|
|
214
|
-
|
|
215
|
-
const pkceCleared = setCookies.find((h) =>
|
|
216
|
-
h.startsWith(`${TEMP_COOKIE_PKCE_VERIFIER}=`)
|
|
217
|
-
);
|
|
218
|
-
expect(pkceCleared).toContain('Max-Age=0');
|
|
219
|
-
|
|
220
|
-
const stateCleared = setCookies.find((h) =>
|
|
221
|
-
h.startsWith(`${TEMP_COOKIE_OAUTH_STATE}=`)
|
|
222
|
-
);
|
|
223
|
-
expect(stateCleared).toContain('Max-Age=0');
|
|
224
|
-
});
|
|
225
|
-
});
|