@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,196 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import axios, { isAxiosError } from 'axios';
4
+ import crypto from 'node:crypto';
5
+ import { axiosErrorMappings, defaultServerError, localProxyErrorMappings, } from "../constants.js";
6
+ import { getSignatureHeaders } from "../signing.js";
7
+ import { reportError } from "../errorReporter.js";
8
+ export const mapToLocalUrl = (localDevUrlMapping, requestUri, allowedUrls, logger) => {
9
+ const url = new URL(requestUri);
10
+ if (allowedUrls && !allowedUrls.includes(url.origin)) {
11
+ logger.warn(`'${url.origin}' is not in the 'allowedUrls' list in the application configuration file. Uploading the project in it's current state may result in request failures.`);
12
+ }
13
+ const localMapping = localDevUrlMapping[url.origin] || localDevUrlMapping[`${url.origin}/`];
14
+ if (!localMapping) {
15
+ return requestUri;
16
+ }
17
+ const parsedLocalMapping = new URL(localMapping);
18
+ url.host = parsedLocalMapping.host;
19
+ url.protocol = parsedLocalMapping.protocol;
20
+ // The local mapping pathname is just slash, there is no need to merge
21
+ if (parsedLocalMapping.pathname !== '/') {
22
+ // If the local pathname ends with '/', remove it to avoid '//' in the path
23
+ const localPathname = parsedLocalMapping.pathname.endsWith('/')
24
+ ? parsedLocalMapping.pathname.slice(0, 1)
25
+ : parsedLocalMapping.pathname;
26
+ url.pathname = `${localPathname}${url.pathname}`;
27
+ }
28
+ const result = url.toString();
29
+ // If the result ends with a '/' and the user didn't add it, remove it
30
+ if (result.endsWith('/') && !localMapping.endsWith('/')) {
31
+ return result.slice(0, -1);
32
+ }
33
+ return result;
34
+ };
35
+ export const extractErrorData = (e) => {
36
+ // If the error is not an AxiosError, it was an error with the local proxy
37
+ // Note: reporting is handled by the caller to provide full context
38
+ if (!isAxiosError(e)) {
39
+ return defaultServerError;
40
+ }
41
+ // Check if we have a mapping, otherwise fallback to the the default error
42
+ let errorData = axiosErrorMappings[e.code || ''] || defaultServerError;
43
+ if (e.response) {
44
+ const { status, data } = e.response;
45
+ errorData = { ...errorData, status, data };
46
+ }
47
+ return errorData;
48
+ };
49
+ export const AppProxyService = ({ localDevUrlMapping, logger, accountId, allowedUrls, }) => {
50
+ const app = express();
51
+ app.use(cors());
52
+ app.use(express.json());
53
+ app.post('/proxy', async (req, res) => {
54
+ const correlationId = crypto.randomUUID();
55
+ const { requestUri, method, requestTimeoutMillis, requestBody, requestHeaders, } =
56
+ /* eslint-disable-next-line no-unsafe-optional-chaining */
57
+ req?.body;
58
+ try {
59
+ logger.info(`Request to ${requestUri} started, method=${method}, correlationId=${correlationId}`);
60
+ const url = mapToLocalUrl(localDevUrlMapping, requestUri, allowedUrls, logger);
61
+ logger.info(url === requestUri
62
+ ? `No local mapping found, using ${requestUri}, correlationId=${correlationId}`
63
+ : `Mapping ${requestUri} -> ${url}, correlationId=${correlationId}`);
64
+ const headerKeys = Object.keys(requestHeaders || {});
65
+ const tooManyHeaders = headerKeys.length > 1;
66
+ /**
67
+ * Mirroring what the backend is doing
68
+ * https://git.hubteam.com/HubSpot/CRMExtensibility/blob/082c4468415dfa8492766e8eec59c072aed9a652/CRMExtensibilityExecutionService/src/main/java/com/hubspot/crm/extensibility/service/util/proxy/ProxyError.java#L23
69
+ */
70
+ if (tooManyHeaders) {
71
+ const status = 400;
72
+ const message = "Only 'Authorization' header is allowed";
73
+ logger.warn(message);
74
+ res.status(status).send({
75
+ category: 'VALIDATION_ERROR',
76
+ status: 'error',
77
+ message,
78
+ context: {
79
+ correlationId: [correlationId],
80
+ httpMethod: [method],
81
+ portalId: [`${accountId}`],
82
+ requestUri: [requestUri],
83
+ },
84
+ });
85
+ return;
86
+ }
87
+ const nonAuthHeader = headerKeys.some((header) => 'authorization' !== header.toLowerCase());
88
+ /**
89
+ * Mirroring what the backend is doing
90
+ * https://git.hubteam.com/HubSpot/CRMExtensibility/blob/082c4468415dfa8492766e8eec59c072aed9a652/CRMExtensibilityExecutionService/src/main/java/com/hubspot/crm/extensibility/service/util/proxy/ProxyError.java#L15
91
+ */
92
+ if (nonAuthHeader) {
93
+ const status = 400;
94
+ const message = "Invalid Request Headers provided. Only 'Authorization' header is allowed from hubspot.fetch()";
95
+ logger.warn(message);
96
+ res.status(status).send({
97
+ category: 'VALIDATION_ERROR',
98
+ status: 'error',
99
+ message,
100
+ context: {
101
+ correlationId: [correlationId],
102
+ httpMethod: [method],
103
+ portalId: [`${accountId}`],
104
+ requestUri: [requestUri],
105
+ },
106
+ });
107
+ return;
108
+ }
109
+ const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
110
+ const shouldIncludeBody = methodsWithBody.includes(method.toUpperCase()) &&
111
+ requestBody !== undefined &&
112
+ requestBody !== null;
113
+ const { status, data: responseBody } = await axios.request({
114
+ url,
115
+ method,
116
+ timeout: requestTimeoutMillis,
117
+ ...(shouldIncludeBody && { data: JSON.stringify(requestBody) }),
118
+ headers: {
119
+ ...requestHeaders,
120
+ ...getSignatureHeaders({
121
+ method,
122
+ url,
123
+ ...(shouldIncludeBody && { requestBody }),
124
+ }),
125
+ ...(shouldIncludeBody && { 'content-type': 'application/json' }),
126
+ },
127
+ maxRedirects: 0,
128
+ });
129
+ res.status(status).send({
130
+ status: 'success',
131
+ responseBody,
132
+ context: {
133
+ correlationId: [correlationId],
134
+ httpMethod: [method],
135
+ appServerStatusCode: [`${status}`],
136
+ portalId: [`${accountId}`],
137
+ requestUri: [requestUri],
138
+ },
139
+ });
140
+ }
141
+ catch (e) {
142
+ const { status, message, category, data: responseBody, } = extractErrorData(e);
143
+ // Report non-Axios errors (dev server bugs)
144
+ if (!isAxiosError(e)) {
145
+ reportError(e, {
146
+ errorType: 'local_proxy_error',
147
+ accountId,
148
+ requestUri,
149
+ method,
150
+ correlationId,
151
+ });
152
+ }
153
+ // This is mirroring current backend proxy behavior
154
+ if (status >= 300 && status <= 599) {
155
+ const { status: proxyStatus, message: proxyMessage, category: proxyCategory, } = localProxyErrorMappings.BAD_GATEWAY;
156
+ res.status(proxyStatus).send({
157
+ status: 'error',
158
+ context: {
159
+ portalId: [`${accountId}`],
160
+ httpMethod: [method],
161
+ requestUri: [requestUri],
162
+ correlationId: [correlationId],
163
+ appServerStatusCode: [`${status}`],
164
+ },
165
+ message: proxyMessage,
166
+ responseBody,
167
+ errors: [
168
+ {
169
+ message: `Unacceptable HTTP status ${status} calling ${requestUri}`,
170
+ },
171
+ ],
172
+ category: proxyCategory,
173
+ });
174
+ return;
175
+ }
176
+ res.status(status).send({
177
+ status: 'error',
178
+ message,
179
+ category,
180
+ context: {
181
+ correlationId: [correlationId],
182
+ httpMethod: [method],
183
+ appServerStatusCode: [`${status}`],
184
+ portalId: [`${accountId}`],
185
+ requestUri: [requestUri],
186
+ },
187
+ responseBody,
188
+ errors: responseBody ? [responseBody] : undefined,
189
+ });
190
+ }
191
+ finally {
192
+ logger.info(`Request completed, correlationId=${correlationId}`);
193
+ }
194
+ });
195
+ return app;
196
+ };
@@ -0,0 +1,22 @@
1
+ import type { ServiceConfiguration, PrivateAppUserToken } from '../types.ts';
2
+ export interface GetPrivateAppUserTokenOptions {
3
+ scopeGroups?: string[];
4
+ privateAppToken?: string;
5
+ }
6
+ export declare class PrivateAppUserTokenManager {
7
+ accountId: number;
8
+ private tokenMap;
9
+ private enabled;
10
+ private logger;
11
+ constructor(accountId: number, logger: ServiceConfiguration['logger']);
12
+ init(): Promise<void>;
13
+ isEnabled(): boolean;
14
+ cleanup(): void;
15
+ getPrivateAppUserToken(appId: number, { scopeGroups, privateAppToken }?: GetPrivateAppUserTokenOptions): Promise<PrivateAppUserToken | undefined>;
16
+ private cacheToken;
17
+ private validateToken;
18
+ private createNewToken;
19
+ private updateToken;
20
+ private getExistingToken;
21
+ static doesUserTokenContainAppTokenScopes(privateAppUserToken: PrivateAppUserToken): boolean;
22
+ }
@@ -0,0 +1,185 @@
1
+ import { scopesOnAccessToken } from '@hubspot/local-dev-lib/personalAccessKey';
2
+ import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
3
+ import { fetchPrivateAppUserToken, createPrivateAppUserToken, updatePrivateAppUserToken, } from "../api/privateAppUserToken.js";
4
+ import { USER_TOKEN_READ, USER_TOKEN_WRITE, TOKEN_TIME_TO_LIVE, TOKEN_REFRESH_THRESHOLD, } from "./constants.js";
5
+ import { generateCahedTokeMessage, generateMissingScopesMessage, generateTokensEnableMessage, generateTokensNotEnabledMessage, generateTokenCachedMessage, generateTokenOpErrorMessage, generateCreateTokenMessage, generateTokenRefreshMessage, generateMissingScopesMoreContextMessage, } from "./messages.js";
6
+ import { reportError } from "../errorReporter.js";
7
+ const MINUTES_TO_MS = 60000;
8
+ function getTokenExpirationDate() {
9
+ return new Date(Date.now() + TOKEN_TIME_TO_LIVE * MINUTES_TO_MS).toISOString();
10
+ }
11
+ function getExpDateWithThreshold(expiresAt) {
12
+ /*
13
+ * Subtracting the threshold from the expiration date to ensure
14
+ * that the token is still valid when it's used. If we waint until
15
+ * the expiration date, we could fire a request with an expired token.
16
+ */
17
+ return new Date(new Date(expiresAt).getTime() - TOKEN_REFRESH_THRESHOLD * MINUTES_TO_MS);
18
+ }
19
+ export class PrivateAppUserTokenManager {
20
+ accountId;
21
+ tokenMap;
22
+ enabled;
23
+ logger;
24
+ constructor(accountId, logger) {
25
+ this.accountId = accountId;
26
+ this.tokenMap = new Map();
27
+ this.enabled = false;
28
+ this.logger = logger;
29
+ }
30
+ async init() {
31
+ const scopeGroups = new Set(await scopesOnAccessToken(this.accountId));
32
+ if (scopeGroups.has(USER_TOKEN_READ) && scopeGroups.has(USER_TOKEN_WRITE)) {
33
+ this.logger.info(generateTokensEnableMessage(this.accountId));
34
+ this.enabled = true;
35
+ return;
36
+ }
37
+ this.logger.info(generateMissingScopesMessage(this.accountId));
38
+ this.logger.debug(generateMissingScopesMoreContextMessage(this.accountId));
39
+ }
40
+ isEnabled() {
41
+ return this.enabled;
42
+ }
43
+ cleanup() {
44
+ this.tokenMap.clear();
45
+ }
46
+ async getPrivateAppUserToken(appId, { scopeGroups = [], privateAppToken } = {}) {
47
+ if (!this.isEnabled()) {
48
+ this.logger.debug(generateTokensNotEnabledMessage(this.accountId, appId));
49
+ return;
50
+ }
51
+ try {
52
+ const tokenInCache = this.tokenMap.get(appId);
53
+ // we have a token in cache and it's valid
54
+ if (tokenInCache && this.validateToken(tokenInCache.token, scopeGroups)) {
55
+ this.logger.debug(generateCahedTokeMessage(appId));
56
+ return tokenInCache.token;
57
+ }
58
+ let token = await this.getExistingToken(appId);
59
+ // token exists but it's not valid. Refresh it
60
+ if (token && !this.validateToken(token, scopeGroups)) {
61
+ token = await this.updateToken({
62
+ appId,
63
+ scopeGroups,
64
+ privateAppToken,
65
+ userTokenKey: token.userTokenKey,
66
+ });
67
+ }
68
+ // token doesn't exist. Create a new one.
69
+ else if (token === null) {
70
+ token = await this.createNewToken(appId, scopeGroups, privateAppToken);
71
+ }
72
+ this.cacheToken(appId, token, scopeGroups);
73
+ return token;
74
+ }
75
+ catch (err) {
76
+ reportError(err, {
77
+ operation: 'get_private_app_user_token',
78
+ appId,
79
+ accountId: this.accountId,
80
+ errorType: 'token_manager_error',
81
+ });
82
+ let messageDetail = 'Unknown error';
83
+ if (err instanceof Error) {
84
+ messageDetail = err.message;
85
+ }
86
+ throw new Error(generateTokenOpErrorMessage({
87
+ operation: 'get',
88
+ appId,
89
+ accountId: this.accountId,
90
+ messageDetail,
91
+ }), { cause: err });
92
+ }
93
+ }
94
+ cacheToken(appId, token, requestedScopeGroups) {
95
+ if (!token) {
96
+ throw new Error(generateTokenOpErrorMessage({
97
+ appId,
98
+ accountId: this.accountId,
99
+ operation: 'refresh',
100
+ }));
101
+ }
102
+ const cachedValue = {
103
+ token,
104
+ requestedScopeGroups,
105
+ };
106
+ this.tokenMap.set(appId, cachedValue);
107
+ this.logger.debug(generateTokenCachedMessage(appId, token.expiresAt));
108
+ }
109
+ validateToken(token, scopeGroups) {
110
+ if (!token) {
111
+ return false;
112
+ }
113
+ const isTokenActive = new Date() < getExpDateWithThreshold(token.expiresAt);
114
+ const hasAllScopes = Array.isArray(scopeGroups) &&
115
+ scopeGroups.every((scopeGroup) => token.scopeGroups.includes(scopeGroup));
116
+ return isTokenActive && hasAllScopes;
117
+ }
118
+ async createNewToken(appId, scopeGroups, privateAppToken) {
119
+ this.logger.debug(generateCreateTokenMessage(appId));
120
+ const response = await createPrivateAppUserToken({
121
+ appId,
122
+ scopeGroups,
123
+ privateAppToken,
124
+ accountId: this.accountId,
125
+ expiresAt: getTokenExpirationDate(),
126
+ });
127
+ if (response.status === 200) {
128
+ return response.data;
129
+ }
130
+ throw new Error(generateTokenOpErrorMessage({
131
+ appId,
132
+ operation: 'create',
133
+ accountId: this.accountId,
134
+ }));
135
+ }
136
+ async updateToken({ appId, userTokenKey, scopeGroups, privateAppToken, }) {
137
+ this.logger.debug(generateTokenRefreshMessage(appId));
138
+ const response = await updatePrivateAppUserToken({
139
+ appId,
140
+ userTokenKey,
141
+ scopeGroups,
142
+ privateAppToken,
143
+ accountId: this.accountId,
144
+ expiresAt: getTokenExpirationDate(),
145
+ });
146
+ if (response.status === 200) {
147
+ return response.data;
148
+ }
149
+ throw new Error(generateTokenOpErrorMessage({
150
+ appId,
151
+ accountId: this.accountId,
152
+ operation: 'refresh',
153
+ }));
154
+ }
155
+ async getExistingToken(appId) {
156
+ try {
157
+ const response = await fetchPrivateAppUserToken({
158
+ accountId: this.accountId,
159
+ appId,
160
+ });
161
+ if (response.status === 200) {
162
+ return response.data;
163
+ }
164
+ return null;
165
+ }
166
+ catch (err) {
167
+ if (isHubSpotHttpError(err) && err?.status === 404) {
168
+ return null;
169
+ }
170
+ throw err;
171
+ }
172
+ }
173
+ static doesUserTokenContainAppTokenScopes(privateAppUserToken) {
174
+ const privateAppToken = privateAppUserToken.privateAppTokenInfo;
175
+ // if the private app token is not present, we can't compare the scopes
176
+ if (!privateAppToken)
177
+ return false;
178
+ const privateAppTokenScopes = privateAppToken.scopeGroups ?? [];
179
+ const privateAppUserTokenScopes = privateAppUserToken.scopeGroups ?? [];
180
+ const areBothEmpty = privateAppUserTokenScopes.length === 0 &&
181
+ privateAppTokenScopes.length === 0;
182
+ return (areBothEmpty ||
183
+ privateAppTokenScopes.every((scope) => privateAppUserTokenScopes.includes(scope)));
184
+ }
185
+ }
@@ -0,0 +1,4 @@
1
+ export declare const USER_TOKEN_READ = "developer.private_app.temporary_token.read";
2
+ export declare const USER_TOKEN_WRITE = "developer.private_app.temporary_token.write";
3
+ export declare const TOKEN_TIME_TO_LIVE = 15;
4
+ export declare const TOKEN_REFRESH_THRESHOLD = 5;
@@ -0,0 +1,4 @@
1
+ export const USER_TOKEN_READ = 'developer.private_app.temporary_token.read';
2
+ export const USER_TOKEN_WRITE = 'developer.private_app.temporary_token.write';
3
+ export const TOKEN_TIME_TO_LIVE = 15; // minutes
4
+ export const TOKEN_REFRESH_THRESHOLD = 5; // minutes
@@ -0,0 +1,3 @@
1
+ import { AppFunctionExecutionService } from './AppFunctionExecutionService.ts';
2
+ import { AppProxyService } from './AppProxyService.ts';
3
+ export { AppFunctionExecutionService, AppProxyService };
@@ -0,0 +1,3 @@
1
+ import { AppFunctionExecutionService } from "./AppFunctionExecutionService.js";
2
+ import { AppProxyService } from "./AppProxyService.js";
3
+ export { AppFunctionExecutionService, AppProxyService };
@@ -0,0 +1,14 @@
1
+ export declare function generateMissingScopesMessage(accountId: number): string;
2
+ export declare function generateTokensEnableMessage(accountId: number): string;
3
+ export declare function generateMissingScopesMoreContextMessage(accountId: number): string;
4
+ export declare function generateTokensNotEnabledMessage(accountId: number, appId: number): string;
5
+ export declare function generateCahedTokeMessage(appId: number): string;
6
+ export declare function generateTokenCachedMessage(appId: number, expiresAt: string): string;
7
+ export declare function generateCreateTokenMessage(appId: number): string;
8
+ export declare function generateTokenOpErrorMessage({ appId, accountId, messageDetail, operation, }: {
9
+ appId: number;
10
+ accountId: number;
11
+ messageDetail?: string;
12
+ operation?: 'create' | 'get' | 'refresh';
13
+ }): string;
14
+ export declare function generateTokenRefreshMessage(appId: number): string;
@@ -0,0 +1,36 @@
1
+ export function generateMissingScopesMessage(accountId) {
2
+ return `Your account is missing the "App functions" scope, so some new features won’t work. To fix this, regenerate the Personal Access Key for account: ${accountId}`;
3
+ }
4
+ export function generateTokensEnableMessage(accountId) {
5
+ return `Private app user tokens are enabled for accountId: ${accountId}`;
6
+ }
7
+ export function generateMissingScopesMoreContextMessage(accountId) {
8
+ return `
9
+ Heads up! Private app user tokens are now supported for serverless functions, but it looks like the account (ID: ${accountId}) is missing the required "App functions" scope.
10
+
11
+ To fix this:
12
+ 1. Deactivate the existing Personal Access Key for this account.
13
+ 2. Generate a new Personal Access Key with the "App functions" scope enabled.
14
+ 3. Run "hs auth" and reauthenticate the account (ID: ${accountId}).
15
+
16
+ Once updated, you won't need a "PRIVATE_APP_ACCESS_TOKEN" for local development.
17
+ `;
18
+ }
19
+ export function generateTokensNotEnabledMessage(accountId, appId) {
20
+ return `Private app user tokens are not enabled for accountId ${accountId}, skipping call to fetch for appId ${appId}`;
21
+ }
22
+ export function generateCahedTokeMessage(appId) {
23
+ return `Using cached private app user token for appId ${appId}`;
24
+ }
25
+ export function generateTokenCachedMessage(appId, expiresAt) {
26
+ return `Token for appId ${appId} expiring at ${expiresAt} is in cache.`;
27
+ }
28
+ export function generateCreateTokenMessage(appId) {
29
+ return `Creating new private app user token for appId ${appId}`;
30
+ }
31
+ export function generateTokenOpErrorMessage({ appId, accountId, messageDetail = '', operation = 'create', }) {
32
+ return `Unable to ${operation} private app user token for appId ${appId} on accountId ${accountId}. ${messageDetail}`;
33
+ }
34
+ export function generateTokenRefreshMessage(appId) {
35
+ return `Refreshing private app user token for appId ${appId}`;
36
+ }
@@ -0,0 +1,29 @@
1
+ interface SignV2Params {
2
+ method: string;
3
+ url: string;
4
+ requestBody: string;
5
+ clientSecret: string;
6
+ signature: '';
7
+ }
8
+ interface SignV3Params extends SignV2Params {
9
+ timestamp: number;
10
+ }
11
+ interface Sign {
12
+ v2: (options: SignV2Params) => string;
13
+ v3: (options: SignV3Params) => string;
14
+ }
15
+ export declare const sign: Sign;
16
+ interface SignatureHeaders {
17
+ 'X-HubSpot-Signature': string;
18
+ 'X-HubSpot-Signature-Version': 'v2';
19
+ 'X-HubSpot-Request-Timestamp': number;
20
+ 'X-HubSpot-Signature-v3': string;
21
+ }
22
+ type GetSignatureHeaders = (params: {
23
+ method: string;
24
+ url: string;
25
+ requestBody?: unknown;
26
+ }) => SignatureHeaders | undefined;
27
+ export declare const formatForSigning: (body: unknown) => string;
28
+ export declare const getSignatureHeaders: GetSignatureHeaders;
29
+ export {};
@@ -0,0 +1,51 @@
1
+ import { Signature } from '@hubspot/api-client';
2
+ // Only exported for testing
3
+ export const sign = {
4
+ v2: ({ method, ...options }) => Signature.getSignature(method, 'v2', options),
5
+ v3: ({ method, ...options }) => Signature.getSignature(method, 'v3', options),
6
+ };
7
+ export const formatForSigning = (body) => {
8
+ if (body === null) {
9
+ return '';
10
+ }
11
+ if (typeof body === 'number' &&
12
+ (!Number.isFinite(body) || Number.isNaN(body))) {
13
+ return '';
14
+ }
15
+ // Anything that stringifies to undefined will be an empty string
16
+ return JSON.stringify(body) || '';
17
+ };
18
+ // This is only used for signing local dev proxy requests
19
+ export const getSignatureHeaders = ({ method, url, requestBody, }) => {
20
+ const clientSecret = process.env.CLIENT_SECRET;
21
+ if (!clientSecret) {
22
+ return;
23
+ }
24
+ const timestamp = new Date().getTime();
25
+ const signingProps = {
26
+ method,
27
+ url,
28
+ requestBody: formatForSigning(requestBody),
29
+ clientSecret,
30
+ /**
31
+ * I'm not sure why the signing function requires this
32
+ *
33
+ * https://github.com/HubSpot/hubspot-api-nodejs/blob/bfaa106af7199adbce5f766b1eb35a73f3c72499/src/utils/ISignatureOptions.ts
34
+ *
35
+ * But it doesn't use it for signing
36
+ *
37
+ * https://github.com/HubSpot/hubspot-api-nodejs/blob/bfaa106af7199adbce5f766b1eb35a73f3c72499/src/utils/signature.ts#L19-L32
38
+ *
39
+ */
40
+ signature: '',
41
+ };
42
+ return {
43
+ 'X-HubSpot-Signature': sign.v2(signingProps),
44
+ 'X-HubSpot-Signature-Version': 'v2',
45
+ 'X-HubSpot-Request-Timestamp': timestamp,
46
+ 'X-HubSpot-Signature-v3': sign.v3({
47
+ ...signingProps,
48
+ timestamp,
49
+ }),
50
+ };
51
+ };