@hubspot/ui-extensions-dev-server 1.1.6 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/lib/DevServerState.d.ts +1 -1
  2. package/dist/lib/ExtensionsWebSocket.js +26 -2
  3. package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +42 -8
  4. package/dist/lib/__tests__/app-functions/context.spec.d.ts +1 -0
  5. package/dist/lib/__tests__/app-functions/context.spec.js +101 -0
  6. package/dist/lib/__tests__/app-functions/errorReporter.spec.d.ts +1 -0
  7. package/dist/lib/__tests__/app-functions/errorReporter.spec.js +102 -0
  8. package/dist/lib/__tests__/app-functions/executor_v20231.spec.d.ts +1 -0
  9. package/dist/lib/__tests__/app-functions/executor_v20231.spec.js +168 -0
  10. package/dist/lib/__tests__/app-functions/executor_v20232.spec.d.ts +1 -0
  11. package/dist/lib/__tests__/app-functions/executor_v20232.spec.js +190 -0
  12. package/dist/lib/__tests__/app-functions/fixtures/constants.d.ts +18 -0
  13. package/dist/lib/__tests__/app-functions/fixtures/constants.js +139 -0
  14. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-fails.cjs +8 -0
  15. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-fails.d.cts +1 -0
  16. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-succeeds.cjs +8 -0
  17. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-succeeds.d.cts +1 -0
  18. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-rejected.cjs +8 -0
  19. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-rejected.d.cts +1 -0
  20. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-resolved.cjs +8 -0
  21. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-resolved.d.cts +1 -0
  22. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-does-not-export-main.cjs +4 -0
  23. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-does-not-export-main.d.cts +1 -0
  24. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-echos-input.cjs +8 -0
  25. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-echos-input.d.cts +1 -0
  26. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-logs.cjs +10 -0
  27. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-logs.d.cts +1 -0
  28. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-function.cjs +4 -0
  29. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-function.d.cts +1 -0
  30. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-rejected.cjs +7 -0
  31. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-rejected.d.cts +1 -0
  32. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-resolved.cjs +7 -0
  33. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-resolved.d.cts +1 -0
  34. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-text.cjs +4 -0
  35. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-text.d.cts +1 -0
  36. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-undefined.cjs +4 -0
  37. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-undefined.d.cts +1 -0
  38. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-throws-error.cjs +4 -0
  39. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-throws-error.d.cts +1 -0
  40. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-times-out.cjs +10 -0
  41. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-times-out.d.cts +1 -0
  42. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-undeclared.cjs +4 -0
  43. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-undeclared.d.cts +1 -0
  44. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-uses-secret.cjs +14 -0
  45. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-uses-secret.d.cts +1 -0
  46. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-fails.cjs +5 -0
  47. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-fails.d.cts +1 -0
  48. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-succeeds.cjs +5 -0
  49. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-succeeds.d.cts +3 -0
  50. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-calls-callback.cjs +4 -0
  51. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-calls-callback.d.cts +1 -0
  52. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-does-not-export-main.cjs +4 -0
  53. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-does-not-export-main.d.cts +1 -0
  54. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-echos-input.cjs +8 -0
  55. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-echos-input.d.cts +5 -0
  56. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-logs.cjs +10 -0
  57. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-logs.d.cts +3 -0
  58. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-function.cjs +4 -0
  59. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-function.d.cts +1 -0
  60. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-implicitly.cjs +7 -0
  61. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-implicitly.d.cts +1 -0
  62. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-rejected.cjs +4 -0
  63. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-rejected.d.cts +1 -0
  64. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-resolved.cjs +4 -0
  65. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-resolved.d.cts +3 -0
  66. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-text.cjs +4 -0
  67. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-text.d.cts +1 -0
  68. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-undefined.cjs +4 -0
  69. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-undefined.d.cts +1 -0
  70. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-throws-error.cjs +4 -0
  71. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-throws-error.d.cts +1 -0
  72. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-times-out.cjs +12 -0
  73. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-times-out.d.cts +1 -0
  74. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-undeclared.cjs +4 -0
  75. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-undeclared.d.cts +1 -0
  76. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-uses-secret.cjs +14 -0
  77. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-uses-secret.d.cts +4 -0
  78. package/dist/lib/__tests__/app-functions/secrets.spec.d.ts +1 -0
  79. package/dist/lib/__tests__/app-functions/secrets.spec.js +278 -0
  80. package/dist/lib/__tests__/app-functions/services/AppProxyService.spec.d.ts +1 -0
  81. package/dist/lib/__tests__/app-functions/services/AppProxyService.spec.js +667 -0
  82. package/dist/lib/__tests__/app-functions/services/PrivateAppUserTokenManager.spec.d.ts +1 -0
  83. package/dist/lib/__tests__/app-functions/services/PrivateAppUserTokenManager.spec.js +243 -0
  84. package/dist/lib/__tests__/app-functions/services/services_v20231.spec.d.ts +1 -0
  85. package/dist/lib/__tests__/app-functions/services/services_v20231.spec.js +319 -0
  86. package/dist/lib/__tests__/app-functions/services/services_v20232.spec.d.ts +1 -0
  87. package/dist/lib/__tests__/app-functions/services/services_v20232.spec.js +302 -0
  88. package/dist/lib/__tests__/app-functions/setup.d.ts +1 -0
  89. package/dist/lib/__tests__/app-functions/setup.js +7 -0
  90. package/dist/lib/__tests__/app-functions/signing.spec.d.ts +1 -0
  91. package/dist/lib/__tests__/app-functions/signing.spec.js +460 -0
  92. package/dist/lib/__tests__/server.spec.js +24 -2
  93. package/dist/lib/app-functions/api/privateAppUserToken.d.ts +16 -0
  94. package/dist/lib/app-functions/api/privateAppUserToken.js +28 -0
  95. package/dist/lib/app-functions/config.d.ts +4 -0
  96. package/dist/lib/app-functions/config.js +48 -0
  97. package/dist/lib/app-functions/constants.d.ts +26 -0
  98. package/dist/lib/app-functions/constants.js +63 -0
  99. package/dist/lib/app-functions/context.d.ts +3 -0
  100. package/dist/lib/app-functions/context.js +65 -0
  101. package/dist/lib/app-functions/errorReporter.d.ts +22 -0
  102. package/dist/lib/app-functions/errorReporter.js +42 -0
  103. package/dist/lib/app-functions/errors.d.ts +44 -0
  104. package/dist/lib/app-functions/errors.js +82 -0
  105. package/dist/lib/app-functions/executor.d.ts +3 -0
  106. package/dist/lib/app-functions/executor.js +131 -0
  107. package/dist/lib/app-functions/index.d.ts +4 -0
  108. package/dist/lib/app-functions/index.js +4 -0
  109. package/dist/lib/app-functions/secrets.d.ts +5 -0
  110. package/dist/lib/app-functions/secrets.js +56 -0
  111. package/dist/lib/app-functions/services/AppFunctionExecutionService.d.ts +2 -0
  112. package/dist/lib/app-functions/services/AppFunctionExecutionService.js +55 -0
  113. package/dist/lib/app-functions/services/AppProxyService.d.ts +5 -0
  114. package/dist/lib/app-functions/services/AppProxyService.js +196 -0
  115. package/dist/lib/app-functions/services/PrivateAppUserTokenManager.d.ts +22 -0
  116. package/dist/lib/app-functions/services/PrivateAppUserTokenManager.js +185 -0
  117. package/dist/lib/app-functions/services/constants.d.ts +4 -0
  118. package/dist/lib/app-functions/services/constants.js +4 -0
  119. package/dist/lib/app-functions/services/index.d.ts +3 -0
  120. package/dist/lib/app-functions/services/index.js +3 -0
  121. package/dist/lib/app-functions/services/messages.d.ts +14 -0
  122. package/dist/lib/app-functions/services/messages.js +36 -0
  123. package/dist/lib/app-functions/signing.d.ts +29 -0
  124. package/dist/lib/app-functions/signing.js +51 -0
  125. package/dist/lib/app-functions/types.d.ts +172 -0
  126. package/dist/lib/app-functions/types.js +6 -0
  127. package/dist/lib/app-functions/utils.d.ts +15 -0
  128. package/dist/lib/app-functions/utils.js +28 -0
  129. package/dist/lib/server.js +15 -4
  130. package/dist/lib/types.d.ts +1 -1
  131. package/package.json +11 -7
@@ -0,0 +1,460 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Signature } from '@hubspot/api-client';
3
+ import { getSignatureHeaders, sign, formatForSigning, } from "../../app-functions/signing.js";
4
+ /**
5
+ * Validate signing of requests for headers
6
+ */
7
+ describe('signing', () => {
8
+ // Changing this secret will change all the signatures
9
+ const clientSecret = '12345678-aaaa-bbbb-cccc-ddddeeeeffff';
10
+ const nullBody = null;
11
+ const undefinedBody = undefined;
12
+ const nanBody = NaN;
13
+ const infinityBody = Infinity;
14
+ const numberBody = 98765;
15
+ const zeroBody = 0;
16
+ const trueBody = true;
17
+ const falseBody = false;
18
+ const emptyStringBody = '';
19
+ const stringBody = 'testing body string';
20
+ const functionBody = () => { };
21
+ const setBody = new Set();
22
+ const mapBody = new Map();
23
+ const emptyObjectBody = {};
24
+ const emptyObjectStringBody = JSON.stringify(emptyObjectBody);
25
+ const objectBody = { a: 1, b: 2 };
26
+ const objectStringBody = JSON.stringify(objectBody);
27
+ const arrayBody = [{}, null, 2, { a: 'b', c: 4 }, ['e', false], undefined, 4];
28
+ const arrayStringBody = JSON.stringify(arrayBody);
29
+ const emptyV2Signature = '99080134002180aee8d0884d21ad51288c43b57e9953e4db1e3757999b35c392';
30
+ const emptyV3Signature = 'zTfXjo07jfzGouo4wvyAj3qEMUTVJ0PPYPACrluQtQI=';
31
+ const objectV2Signature = 'bc131b34cd63dafbbd2e969e87167311f5f6c0cd543c90fe84c73496093cf4a2';
32
+ const objectV3Signature = 'aaknaCGCYJ4yqzuf1UKGqAWUfdcilJtm5nc/ZCudR9k=';
33
+ const emptyObjectV2Signature = 'e955f4008c9f8bc80a14a00cb92fcc062d8df2023ddb39e135625f32764a1e78';
34
+ const emptyObjectV3Signature = 'bRPTUf7UOx1OqVAME6JSNfst9KxQYF5nRIUwG/KcQhQ=';
35
+ const options = {
36
+ // Changing this timestamp will change all the signatures
37
+ timestamp: 1111111111,
38
+ clientSecret,
39
+ requestBody: emptyStringBody,
40
+ url: 'https://hubspot.com/?appId=1&portalId=2&userId=3',
41
+ method: 'GET',
42
+ signature: '',
43
+ };
44
+ afterEach(() => {
45
+ vi.resetAllMocks();
46
+ });
47
+ describe('formatForSigning', () => {
48
+ it('returns an empty string for an empty string', () => { });
49
+ it('returns a json stringified string (double quoted)', () => {
50
+ expect(formatForSigning(stringBody)).toEqual('"testing body string"');
51
+ });
52
+ it('returns a stringified zero for zero body', () => {
53
+ expect(formatForSigning(zeroBody)).toEqual('0');
54
+ });
55
+ it('returns a stringified number', () => {
56
+ expect(formatForSigning(numberBody)).toEqual('98765');
57
+ });
58
+ it('returns empty string for null body', () => {
59
+ expect(formatForSigning(nullBody)).toEqual('');
60
+ });
61
+ it('returns empty string for undefined body', () => {
62
+ expect(formatForSigning(undefined)).toEqual('');
63
+ });
64
+ it('returns stringified "true" for true body', () => {
65
+ expect(formatForSigning(trueBody)).toEqual('true');
66
+ });
67
+ it('returns stringified "false" for false body', () => {
68
+ expect(formatForSigning(falseBody)).toEqual('false');
69
+ });
70
+ it('returns empty string for NaN body', () => {
71
+ expect(formatForSigning(nanBody)).toEqual('');
72
+ });
73
+ it('returns empty string for Infinity body', () => {
74
+ expect(formatForSigning(infinityBody)).toEqual('');
75
+ });
76
+ it('returns empty string for function body', () => {
77
+ expect(formatForSigning(functionBody)).toEqual('');
78
+ });
79
+ it('returns stringified object for for Set body', () => {
80
+ expect(formatForSigning(setBody)).toEqual(emptyObjectStringBody);
81
+ });
82
+ it('returns stringified object for for Map body', () => {
83
+ expect(formatForSigning(mapBody)).toEqual(emptyObjectStringBody);
84
+ });
85
+ it('returns stringified empty object for empty object body', () => {
86
+ expect(formatForSigning(emptyObjectBody)).toEqual(emptyObjectStringBody);
87
+ });
88
+ it('returns stringified object for object body', () => {
89
+ expect(formatForSigning(objectBody)).toEqual(objectStringBody);
90
+ });
91
+ it('returns stringified array with undefineds replaced with nulls for array body', () => {
92
+ expect(formatForSigning(arrayBody)).toEqual(arrayStringBody);
93
+ });
94
+ });
95
+ describe('sign.v2', () => {
96
+ it('creates a proper v2 signature with empty body', () => {
97
+ expect(sign.v2(options)).toEqual(emptyV2Signature);
98
+ });
99
+ it('creates a proper v2 signature with stringified object body', () => {
100
+ expect(sign.v2({ ...options, requestBody: objectStringBody })).toEqual(objectV2Signature);
101
+ });
102
+ it('creates a signature that matches what the backend created with a request body', () => {
103
+ const backendOptions = {
104
+ ...options,
105
+ clientSecret: 'f6e94c08-85d0-4608-bcc6-e1850176e7b8',
106
+ requestBody: 'REQUEST_BODY',
107
+ method: 'POST',
108
+ url: 'https://www.hubspot.com/webhook',
109
+ };
110
+ // https://git.hubteam.com/HubSpot/WebhooksPlatform/blob/f6737bea75d89b2bcbc565ace0ba78e60bcdd8f2/WebhooksPlatformSecurity/src/test/java/com/hubspot/webhooks/platform/security/WebhookSignatureGeneratorTest.java#L40
111
+ expect(sign.v2(backendOptions)).toEqual('57bb01a6e6ac92919b5e715d68f40265328701bbf1cf3fd73772887069945766');
112
+ });
113
+ it('creates a signature that matches what the backend created without a request body', () => {
114
+ const backendOptions = {
115
+ ...options,
116
+ clientSecret: 'f6e94c08-85d0-4608-bcc6-e1850176e7b8',
117
+ requestBody: '',
118
+ method: 'POST',
119
+ url: 'https://www.hubspot.com/webhook',
120
+ };
121
+ // https://git.hubteam.com/HubSpot/WebhooksPlatform/blob/f6737bea75d89b2bcbc565ace0ba78e60bcdd8f2/WebhooksPlatformSecurity/src/test/java/com/hubspot/webhooks/platform/security/WebhookSignatureGeneratorTest.java#L54
122
+ expect(sign.v2(backendOptions)).toEqual('befaef2b7354f11bf651f6468e8b5cf6bd449048deaf057f712980afb2a682f5');
123
+ });
124
+ });
125
+ describe('sign.v3', () => {
126
+ it('creates a proper v3 signature with empty body', () => {
127
+ expect(sign.v3(options)).toEqual(emptyV3Signature);
128
+ });
129
+ it('creates a proper v3 signature with stringified object body', () => {
130
+ expect(sign.v3({ ...options, requestBody: objectStringBody })).toEqual(objectV3Signature);
131
+ });
132
+ it('creates a signature that matches what the backend created with a request body', () => {
133
+ const backendOptions = {
134
+ ...options,
135
+ clientSecret: 'f6e94c08-85d0-4608-bcc6-e1850176e7b8',
136
+ requestBody: 'REQUEST_BODY',
137
+ method: 'POST',
138
+ url: 'https://www.hubspot.com/webhook',
139
+ timestamp: 1632188626483,
140
+ };
141
+ // https://git.hubteam.com/HubSpot/WebhooksPlatform/blob/f6737bea75d89b2bcbc565ace0ba78e60bcdd8f2/WebhooksPlatformSecurity/src/test/java/com/hubspot/webhooks/platform/security/WebhookSignatureGeneratorTest.java#L67
142
+ expect(sign.v3(backendOptions)).toEqual('jf872r5vscVcxCiDRHA4rGrqe/8BPLNVkdJSHdMnGTU=');
143
+ });
144
+ it('creates a signature that matches what the backend created without a request body', () => {
145
+ const backendOptions = {
146
+ ...options,
147
+ clientSecret: 'f6e94c08-85d0-4608-bcc6-e1850176e7b8',
148
+ requestBody: '',
149
+ method: 'POST',
150
+ url: 'https://www.hubspot.com/webhook',
151
+ timestamp: 1632188626483,
152
+ };
153
+ // https://git.hubteam.com/HubSpot/WebhooksPlatform/blob/f6737bea75d89b2bcbc565ace0ba78e60bcdd8f2/WebhooksPlatformSecurity/src/test/java/com/hubspot/webhooks/platform/security/WebhookSignatureGeneratorTest.java#L81
154
+ expect(sign.v3(backendOptions)).toEqual('csXgk7UbrE9F8ZmMvbpuKBxqIvTSqU1EX9pI/SJtxSE=');
155
+ });
156
+ });
157
+ describe('getSignatureHeaders', () => {
158
+ let savedEnv;
159
+ beforeEach(() => {
160
+ // Store current env because we will be modifying it
161
+ savedEnv = JSON.stringify(process.env);
162
+ process.env.CLIENT_SECRET = clientSecret;
163
+ vi.spyOn(Date.prototype, 'getTime').mockReturnValue(options.timestamp);
164
+ vi.spyOn(Date, 'now').mockReturnValue(options.timestamp);
165
+ });
166
+ afterEach(() => {
167
+ process.env = JSON.parse(savedEnv);
168
+ });
169
+ it('does not create any signature headers if process.env.CLIENT_SECRET is missing', () => {
170
+ expect(getSignatureHeaders({
171
+ method: options.method,
172
+ url: options.url,
173
+ requestBody: objectBody,
174
+ })).toEqual({
175
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
176
+ 'X-HubSpot-Signature': objectV2Signature,
177
+ 'X-HubSpot-Signature-Version': 'v2',
178
+ 'X-HubSpot-Signature-v3': objectV3Signature,
179
+ });
180
+ delete process.env.CLIENT_SECRET;
181
+ expect(getSignatureHeaders({
182
+ method: options.method,
183
+ url: options.url,
184
+ requestBody: objectBody,
185
+ })).toBeUndefined();
186
+ });
187
+ it('creates proper request headers for empty string body', () => {
188
+ const doubleQuotedEmptyStringV2Signature = '8a129cb0bc3d2a8d72634bdbcbbe6a26e321d21f1832ece8e969dc25f14f446a';
189
+ const doubleQuotedEmptyStringV3Signature = 'vHLaaF6kLUy1XOa7O6osWuzV3v9EaxhIs/mJQbODE6g=';
190
+ expect(getSignatureHeaders({
191
+ method: options.method,
192
+ url: options.url,
193
+ requestBody: emptyStringBody,
194
+ })).toEqual({
195
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
196
+ 'X-HubSpot-Signature': doubleQuotedEmptyStringV2Signature,
197
+ 'X-HubSpot-Signature-Version': 'v2',
198
+ 'X-HubSpot-Signature-v3': doubleQuotedEmptyStringV3Signature,
199
+ });
200
+ });
201
+ it('creates proper request headers for string body', () => {
202
+ const doubleQuotedStringV2Signature = 'f47fe62f3ecf4db32766db490fea65cdb12bfc554e9562ce9ee94f787254661d';
203
+ const doubleQuotedStringV3Signature = 'q/dQ0wPPFe+wBtr3I7/6mPYSNG0pvqUGK4yhcso9If8=';
204
+ expect(getSignatureHeaders({
205
+ method: options.method,
206
+ url: options.url,
207
+ requestBody: stringBody,
208
+ })).toEqual({
209
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
210
+ 'X-HubSpot-Signature': doubleQuotedStringV2Signature,
211
+ 'X-HubSpot-Signature-Version': 'v2',
212
+ 'X-HubSpot-Signature-v3': doubleQuotedStringV3Signature,
213
+ });
214
+ });
215
+ it('creates proper request headers for zero body', () => {
216
+ const zeroV2Signature = '6d9ffe0ceeb63d9b18df5e1d2ad1175b9e3e21d873cd630008dc7bd9da21bc5a';
217
+ const zeroV3Signature = 'mOIoJRulruf3So7ER2nIiWx3qai8GJHEJSYPaVJHuuc=';
218
+ expect(getSignatureHeaders({
219
+ method: options.method,
220
+ url: options.url,
221
+ requestBody: zeroBody,
222
+ })).toEqual({
223
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
224
+ 'X-HubSpot-Signature': zeroV2Signature,
225
+ 'X-HubSpot-Signature-Version': 'v2',
226
+ 'X-HubSpot-Signature-v3': zeroV3Signature,
227
+ });
228
+ });
229
+ it('creates proper request headers for number body', () => {
230
+ const numberV2Signature = 'b467e410c49b405c2f4f1fe0902eb00d203f02a52c65eddfcc977d8c53a4f952';
231
+ const numberV3Signature = '5NPk47PxfF6GlSjdPs2+ElmetlE0HbWPcGjQK1LAJcs=';
232
+ expect(getSignatureHeaders({
233
+ method: options.method,
234
+ url: options.url,
235
+ requestBody: numberBody,
236
+ })).toEqual({
237
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
238
+ 'X-HubSpot-Signature': numberV2Signature,
239
+ 'X-HubSpot-Signature-Version': 'v2',
240
+ 'X-HubSpot-Signature-v3': numberV3Signature,
241
+ });
242
+ });
243
+ it('creates proper request headers for null body', () => {
244
+ expect(getSignatureHeaders({
245
+ method: options.method,
246
+ url: options.url,
247
+ requestBody: nullBody,
248
+ })).toEqual({
249
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
250
+ 'X-HubSpot-Signature': emptyV2Signature,
251
+ 'X-HubSpot-Signature-Version': 'v2',
252
+ 'X-HubSpot-Signature-v3': emptyV3Signature,
253
+ });
254
+ });
255
+ it('creates proper request headers for undefined body', () => {
256
+ expect(getSignatureHeaders({
257
+ method: options.method,
258
+ url: options.url,
259
+ requestBody: undefinedBody,
260
+ })).toEqual({
261
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
262
+ 'X-HubSpot-Signature': emptyV2Signature,
263
+ 'X-HubSpot-Signature-Version': 'v2',
264
+ 'X-HubSpot-Signature-v3': emptyV3Signature,
265
+ });
266
+ });
267
+ it('creates proper request headers for true body', () => {
268
+ const trueV2Signature = 'f1a7b1161e7b8d57dcda323dea73a4436ab09dffeedfab0ae292e5c76a332142';
269
+ const trueV3Signature = 'HAsym0s8w+iAGklVfrkwWGPJQdazvSTxxn0U6UOm+fI=';
270
+ expect(getSignatureHeaders({
271
+ method: options.method,
272
+ url: options.url,
273
+ requestBody: trueBody,
274
+ })).toEqual({
275
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
276
+ 'X-HubSpot-Signature': trueV2Signature,
277
+ 'X-HubSpot-Signature-Version': 'v2',
278
+ 'X-HubSpot-Signature-v3': trueV3Signature,
279
+ });
280
+ });
281
+ it('creates proper request headers for false body', () => {
282
+ const falseV2Signature = '6dff8314721fa2b9e388760a9520a32893b1540cbc6274367b2db42cfaa0473b';
283
+ const falseV3Signature = 'b4vAq63KKWYKqM3y/r1FuCGDzuSgWjMZ9POwBaqmAQs=';
284
+ expect(getSignatureHeaders({
285
+ method: options.method,
286
+ url: options.url,
287
+ requestBody: falseBody,
288
+ })).toEqual({
289
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
290
+ 'X-HubSpot-Signature': falseV2Signature,
291
+ 'X-HubSpot-Signature-Version': 'v2',
292
+ 'X-HubSpot-Signature-v3': falseV3Signature,
293
+ });
294
+ });
295
+ it('creates proper request headers for NaN body', () => {
296
+ expect(getSignatureHeaders({
297
+ method: options.method,
298
+ url: options.url,
299
+ requestBody: nanBody,
300
+ })).toEqual({
301
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
302
+ 'X-HubSpot-Signature': emptyV2Signature,
303
+ 'X-HubSpot-Signature-Version': 'v2',
304
+ 'X-HubSpot-Signature-v3': emptyV3Signature,
305
+ });
306
+ });
307
+ it('creates proper request headers for Infinity body', () => {
308
+ expect(getSignatureHeaders({
309
+ method: options.method,
310
+ url: options.url,
311
+ requestBody: infinityBody,
312
+ })).toEqual({
313
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
314
+ 'X-HubSpot-Signature': emptyV2Signature,
315
+ 'X-HubSpot-Signature-Version': 'v2',
316
+ 'X-HubSpot-Signature-v3': emptyV3Signature,
317
+ });
318
+ });
319
+ it('creates proper request headers for function body', () => {
320
+ expect(getSignatureHeaders({
321
+ method: options.method,
322
+ url: options.url,
323
+ requestBody: functionBody,
324
+ })).toEqual({
325
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
326
+ 'X-HubSpot-Signature': emptyV2Signature,
327
+ 'X-HubSpot-Signature-Version': 'v2',
328
+ 'X-HubSpot-Signature-v3': emptyV3Signature,
329
+ });
330
+ });
331
+ it('creates proper request headers for Set body', () => {
332
+ expect(getSignatureHeaders({
333
+ method: options.method,
334
+ url: options.url,
335
+ requestBody: setBody,
336
+ })).toEqual({
337
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
338
+ 'X-HubSpot-Signature': emptyObjectV2Signature,
339
+ 'X-HubSpot-Signature-Version': 'v2',
340
+ 'X-HubSpot-Signature-v3': emptyObjectV3Signature,
341
+ });
342
+ });
343
+ it('creates proper request headers for Map body', () => {
344
+ expect(getSignatureHeaders({
345
+ method: options.method,
346
+ url: options.url,
347
+ requestBody: mapBody,
348
+ })).toEqual({
349
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
350
+ 'X-HubSpot-Signature': emptyObjectV2Signature,
351
+ 'X-HubSpot-Signature-Version': 'v2',
352
+ 'X-HubSpot-Signature-v3': emptyObjectV3Signature,
353
+ });
354
+ });
355
+ it('creates proper request headers for empty object body', () => {
356
+ expect(getSignatureHeaders({
357
+ method: options.method,
358
+ url: options.url,
359
+ requestBody: emptyObjectBody,
360
+ })).toEqual({
361
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
362
+ 'X-HubSpot-Signature': emptyObjectV2Signature,
363
+ 'X-HubSpot-Signature-Version': 'v2',
364
+ 'X-HubSpot-Signature-v3': emptyObjectV3Signature,
365
+ });
366
+ });
367
+ it('creates proper request headers for object body', () => {
368
+ expect(getSignatureHeaders({
369
+ method: options.method,
370
+ url: options.url,
371
+ requestBody: objectBody,
372
+ })).toEqual({
373
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
374
+ 'X-HubSpot-Signature': objectV2Signature,
375
+ 'X-HubSpot-Signature-Version': 'v2',
376
+ 'X-HubSpot-Signature-v3': objectV3Signature,
377
+ });
378
+ });
379
+ it('creates proper request headers for array body', () => {
380
+ const arrayV2Signature = '7db684f2a4f85d0a10443e0fcb56439bbfe5cd3957321334ea612d4a222d12bb';
381
+ const arrayV3Signature = 'BbmvQxGycRrmC4kiTOSEFy6TL4g/kGX+U4mKQWbPgzg=';
382
+ expect(getSignatureHeaders({
383
+ method: options.method,
384
+ url: options.url,
385
+ requestBody: arrayBody,
386
+ })).toEqual({
387
+ 'X-HubSpot-Request-Timestamp': options.timestamp,
388
+ 'X-HubSpot-Signature': arrayV2Signature,
389
+ 'X-HubSpot-Signature-Version': 'v2',
390
+ 'X-HubSpot-Signature-v3': arrayV3Signature,
391
+ });
392
+ });
393
+ });
394
+ describe('getSignatureHeaders validated against hubspot api-client signing lib', () => {
395
+ let savedEnv;
396
+ beforeEach(() => {
397
+ // Store current env because we will be modifying it
398
+ savedEnv = JSON.stringify(process.env);
399
+ process.env.CLIENT_SECRET = clientSecret;
400
+ vi.spyOn(Date.prototype, 'getTime').mockReturnValue(options.timestamp);
401
+ vi.spyOn(Date, 'now').mockReturnValue(options.timestamp);
402
+ });
403
+ afterEach(() => {
404
+ process.env = JSON.parse(savedEnv);
405
+ });
406
+ [
407
+ nullBody,
408
+ undefinedBody,
409
+ nanBody,
410
+ infinityBody,
411
+ numberBody,
412
+ zeroBody,
413
+ trueBody,
414
+ falseBody,
415
+ emptyStringBody,
416
+ stringBody,
417
+ functionBody,
418
+ setBody,
419
+ mapBody,
420
+ emptyObjectBody,
421
+ emptyObjectStringBody,
422
+ objectBody,
423
+ objectStringBody,
424
+ arrayBody,
425
+ arrayStringBody,
426
+ ].forEach((requestBody) => {
427
+ it(`generates v2 signatures for \`${requestBody}\` body that validate with HubSpot's api-client`, () => {
428
+ const headers = getSignatureHeaders({
429
+ url: options.url,
430
+ method: options.method,
431
+ requestBody,
432
+ });
433
+ expect(Signature.isValid({
434
+ url: options.url,
435
+ method: options.method,
436
+ requestBody: formatForSigning(requestBody),
437
+ clientSecret,
438
+ signatureVersion: 'v2',
439
+ signature: headers['X-HubSpot-Signature'],
440
+ })).toBe(true);
441
+ });
442
+ it(`generates v3 signatures for \`${requestBody}\` body that validate with HubSpot's api-client`, () => {
443
+ const headers = getSignatureHeaders({
444
+ url: options.url,
445
+ method: options.method,
446
+ requestBody,
447
+ });
448
+ expect(Signature.isValid({
449
+ url: options.url,
450
+ method: options.method,
451
+ requestBody: formatForSigning(requestBody),
452
+ clientSecret,
453
+ signatureVersion: 'v3',
454
+ signature: headers['X-HubSpot-Signature-v3'],
455
+ timestamp: headers['X-HubSpot-Request-Timestamp'],
456
+ })).toBe(true);
457
+ });
458
+ });
459
+ });
460
+ });
@@ -1,10 +1,10 @@
1
1
  import { afterEach, describe, it, expect, vi, beforeEach } from 'vitest';
2
- import * as devServer from '@hubspot/app-functions-dev-server';
2
+ import * as devServer from "../app-functions/services/index.js";
3
3
  import startDevServer from "../server.js";
4
4
  import { DevServerState } from "../DevServerState.js";
5
5
  import { createMockLogger, createDevServerConfig, createMockViteDevServer, } from "./factories.js";
6
6
  const useMock = vi.fn();
7
- vi.mock('@hubspot/app-functions-dev-server', () => ({
7
+ vi.mock('../app-functions/services/index.ts', () => ({
8
8
  AppProxyService: vi.fn(() => vi.fn((req, res, next) => next())),
9
9
  AppFunctionExecutionService: vi.fn(() => vi.fn((req, res, next) => next())),
10
10
  }));
@@ -138,6 +138,28 @@ describe('server', () => {
138
138
  });
139
139
  expect(devServerState.extensionsWebSocket).toBeDefined();
140
140
  });
141
+ it('should broadcast shutdown messages before closing WebSocket', async () => {
142
+ const devServerState = new DevServerState(devServerConfig);
143
+ const { shutdown } = await startDevServer({
144
+ devServerState,
145
+ viteDevServer: createMockViteDevServer(),
146
+ });
147
+ const ws = devServerState.extensionsWebSocket;
148
+ const callOrder = [];
149
+ const broadcastSpy = vi.spyOn(ws, 'broadcast').mockImplementation(() => {
150
+ callOrder.push('broadcast');
151
+ });
152
+ const closeSpy = vi.spyOn(ws, 'close').mockImplementation(async () => {
153
+ callOrder.push('close');
154
+ });
155
+ await shutdown();
156
+ expect(broadcastSpy).toHaveBeenCalledWith(expect.objectContaining({
157
+ event: 'shutdown',
158
+ version: 1,
159
+ }));
160
+ expect(closeSpy).toHaveBeenCalledOnce();
161
+ expect(callOrder).toEqual(['broadcast', 'close']);
162
+ });
141
163
  it('should trigger WebSocket setup after initialization', async () => {
142
164
  const triggerSpy = vi.spyOn(DevServerState.prototype, 'triggerWebSocketSetup');
143
165
  const devServerState = new DevServerState(devServerConfig);
@@ -0,0 +1,16 @@
1
+ import type { PrivateAppUserToken } from '../types.ts';
2
+ import { HubSpotPromise } from '@hubspot/local-dev-lib/types/Http';
3
+ interface TokenOptions {
4
+ userTokenKey: string;
5
+ scopeGroups?: string[];
6
+ expiresAt?: string;
7
+ privateAppToken?: string;
8
+ }
9
+ interface UserOptions {
10
+ accountId: number;
11
+ appId: number;
12
+ }
13
+ export declare function fetchPrivateAppUserToken({ accountId, appId, }: UserOptions): HubSpotPromise<PrivateAppUserToken>;
14
+ export declare function createPrivateAppUserToken({ accountId, appId, scopeGroups, expiresAt, privateAppToken, }: UserOptions & Partial<TokenOptions>): HubSpotPromise<PrivateAppUserToken>;
15
+ export declare function updatePrivateAppUserToken({ accountId, appId, userTokenKey, scopeGroups, expiresAt, privateAppToken, }: TokenOptions & UserOptions): HubSpotPromise<PrivateAppUserToken>;
16
+ export {};
@@ -0,0 +1,28 @@
1
+ import { http } from '@hubspot/local-dev-lib/http';
2
+ const LOCALDEVAUTH_API_PRIVATE_APP_USER_TOKEN_PATH = 'localdevauth/v1/private-app/user-token';
3
+ export async function fetchPrivateAppUserToken({ accountId, appId, }) {
4
+ return await http.get(accountId, {
5
+ url: `${LOCALDEVAUTH_API_PRIVATE_APP_USER_TOKEN_PATH}/${appId}`,
6
+ });
7
+ }
8
+ export async function createPrivateAppUserToken({ accountId, appId, scopeGroups, expiresAt, privateAppToken, }) {
9
+ return await http.post(accountId, {
10
+ url: `${LOCALDEVAUTH_API_PRIVATE_APP_USER_TOKEN_PATH}/${appId}`,
11
+ data: {
12
+ ...(scopeGroups && { scopeGroups }),
13
+ ...(expiresAt && { expiresAt }),
14
+ ...(privateAppToken && { privateAppToken }),
15
+ },
16
+ });
17
+ }
18
+ export async function updatePrivateAppUserToken({ accountId, appId, userTokenKey, scopeGroups, expiresAt, privateAppToken, }) {
19
+ return await http.put(accountId, {
20
+ url: `${LOCALDEVAUTH_API_PRIVATE_APP_USER_TOKEN_PATH}/${appId}`,
21
+ data: {
22
+ privateAppUserTokenKey: userTokenKey,
23
+ ...(scopeGroups && { scopeGroups }),
24
+ ...(expiresAt && { expiresAt }),
25
+ ...(privateAppToken && { privateAppToken }),
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,4 @@
1
+ import { MultiFunctionInfo, ServiceConfiguration, SingleFunctionInfo } from './types.ts';
2
+ export declare function loadMultiFunctionInfo(config: ServiceConfiguration): MultiFunctionInfo;
3
+ export declare function loadFunctionInfo(config: ServiceConfiguration, functionName: string, appId?: number): SingleFunctionInfo;
4
+ export declare function buildServiceConfiguration(appPath: string, options: Partial<ServiceConfiguration>): ServiceConfiguration;
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { APP_FUNCTIONS_DIRNAME, APP_FUNCTIONS_MANIFEST_FILENAME, EXECUTION_TIMEOUT_MS, PLATFORM_VERSION, } from "./constants.js";
4
+ import { Reason, ExecutionError } from "./errors.js";
5
+ export function loadMultiFunctionInfo(config) {
6
+ const srcDir = path.join(config.app.path, APP_FUNCTIONS_DIRNAME);
7
+ if (!fs.existsSync(srcDir)) {
8
+ throw new Error(`Cannot locate app function directory: ${srcDir}`);
9
+ }
10
+ const stats = fs.lstatSync(srcDir);
11
+ if (!stats.isDirectory()) {
12
+ throw new Error(`Invalid app function directory: ${srcDir}`);
13
+ }
14
+ const manifestPath = path.join(srcDir, APP_FUNCTIONS_MANIFEST_FILENAME);
15
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, {
16
+ encoding: 'utf-8',
17
+ }));
18
+ const { appFunctions = {} } = manifest;
19
+ // TODO validation
20
+ return { appFunctions, srcDir };
21
+ }
22
+ export function loadFunctionInfo(config, functionName, appId) {
23
+ const { logger } = config;
24
+ const { appFunctions, srcDir } = loadMultiFunctionInfo(config);
25
+ const metadata = appFunctions[functionName];
26
+ if (!metadata) {
27
+ const manifestPath = path.join(srcDir, APP_FUNCTIONS_MANIFEST_FILENAME);
28
+ logger.error(`App function "${functionName}" is not declared in ${manifestPath}`);
29
+ throw new ExecutionError({
30
+ reason: Reason.FunctionNotFound,
31
+ functionName,
32
+ appId,
33
+ });
34
+ }
35
+ // TODO validation
36
+ return { ...metadata, srcDir, name: functionName };
37
+ }
38
+ export function buildServiceConfiguration(appPath, options) {
39
+ return {
40
+ accountId: options.accountId,
41
+ app: { path: appPath },
42
+ functionTimeoutMs: options.functionTimeoutMs ?? EXECUTION_TIMEOUT_MS,
43
+ hubspotApiOrigin: options.hubspotApiOrigin,
44
+ hubspotWebsiteOrigin: options.hubspotWebsiteOrigin,
45
+ logger: options.logger ?? console,
46
+ platformVersion: options.platformVersion ?? PLATFORM_VERSION.V20231,
47
+ };
48
+ }
@@ -0,0 +1,26 @@
1
+ import { ProxyServerError } from './types.ts';
2
+ export declare const APP_FUNCTIONS_DIRNAME = "app.functions";
3
+ export declare const APP_FUNCTIONS_MANIFEST_FILENAME = "serverless.json";
4
+ export declare const DEV_SERVER_DEFAULT_PORT = 6789;
5
+ export declare const EXECUTION_TIMEOUT_MS = 15000;
6
+ export declare const PRIVATE_APP_ACCESS_TOKEN = "PRIVATE_APP_ACCESS_TOKEN";
7
+ export declare const SECRETS_IN_CONTEXT: string[];
8
+ export declare const PLATFORM_VERSION: {
9
+ readonly V20231: "2023.1";
10
+ readonly V20232: "2023.2";
11
+ readonly V20251: "2025.1";
12
+ readonly V20252: "2025.2";
13
+ readonly UNSTABLE: "unstable";
14
+ };
15
+ export declare const defaultServerError: ProxyServerError;
16
+ export declare const validationError: Omit<ProxyServerError, 'message' | 'data'>;
17
+ export declare const axiosErrorMappings: {
18
+ [key: string]: ProxyServerError;
19
+ };
20
+ export declare const localProxyErrorMappings: {
21
+ BAD_GATEWAY: {
22
+ status: number;
23
+ category: string;
24
+ message: string;
25
+ };
26
+ };