@friggframework/core 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.0

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 (237) hide show
  1. package/CHANGELOG.md +66 -67
  2. package/CLAUDE.md +148 -124
  3. package/README.md +165 -130
  4. package/admin-scripts/index.js +52 -0
  5. package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
  6. package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
  7. package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
  8. package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
  9. package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
  10. package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
  11. package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
  12. package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
  13. package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
  14. package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
  15. package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
  16. package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
  17. package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
  18. package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
  19. package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
  20. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  21. package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
  22. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  23. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  24. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  25. package/application/commands/admin-script-commands.js +348 -0
  26. package/application/commands/integration-commands.js +12 -6
  27. package/application/index.js +5 -3
  28. package/assertions/get.js +17 -7
  29. package/associations/association.js +59 -57
  30. package/core/CLAUDE.md +222 -156
  31. package/core/Worker.js +33 -12
  32. package/credential/credential-router.js +262 -0
  33. package/credential/repositories/credential-repository-documentdb.js +71 -1
  34. package/credential/repositories/credential-repository-factory.js +4 -6
  35. package/credential/repositories/credential-repository-interface.js +8 -2
  36. package/credential/repositories/credential-repository-mongo.js +45 -4
  37. package/credential/repositories/credential-repository-postgres.js +44 -3
  38. package/credential/repositories/credential-repository.js +3 -1
  39. package/credential/use-cases/delete-credential-for-user.js +50 -0
  40. package/credential/use-cases/get-credential-for-user.js +21 -3
  41. package/credential/use-cases/list-credentials-for-user.js +31 -0
  42. package/credential/use-cases/reauthorize-credential.js +103 -0
  43. package/credential/use-cases/update-authentication-status.js +5 -2
  44. package/database/MONGODB_TRANSACTION_FIX.md +72 -52
  45. package/database/config.js +13 -59
  46. package/database/documentdb-encryption-service.js +29 -13
  47. package/database/documentdb-utils.js +21 -12
  48. package/database/encryption/README.md +53 -36
  49. package/database/encryption/encryption-schema-registry.js +29 -23
  50. package/database/encryption/field-encryption-service.js +15 -4
  51. package/database/encryption/logger.js +3 -2
  52. package/database/encryption/prisma-encryption-extension.js +5 -8
  53. package/database/prisma.js +22 -14
  54. package/database/repositories/health-check-repository-factory.js +7 -5
  55. package/database/repositories/health-check-repository-interface.js +15 -5
  56. package/database/repositories/health-check-repository-postgres.js +1 -1
  57. package/database/use-cases/check-database-health-use-case.js +3 -2
  58. package/database/use-cases/check-database-state-use-case.js +8 -7
  59. package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
  60. package/database/use-cases/get-migration-status-use-case.js +4 -2
  61. package/database/use-cases/run-database-migration-use-case.js +41 -9
  62. package/database/use-cases/test-encryption-use-case.js +4 -5
  63. package/database/use-cases/trigger-database-migration-use-case.js +9 -8
  64. package/database/utils/mongodb-collection-utils.js +11 -3
  65. package/database/utils/mongodb-schema-init.js +10 -4
  66. package/database/utils/prisma-runner.js +144 -74
  67. package/database/utils/prisma-schema-parser.js +2 -2
  68. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
  69. package/encrypt/Cryptor.js +54 -49
  70. package/encrypt/aes-encryption-key-provider.js +82 -0
  71. package/encrypt/encryption-key-provider-interface.js +50 -0
  72. package/errors/fetch-error.js +1 -1
  73. package/generated/prisma-mongodb/edge.js +59 -4
  74. package/generated/prisma-mongodb/index-browser.js +49 -1
  75. package/generated/prisma-mongodb/index.d.ts +4023 -61
  76. package/generated/prisma-mongodb/index.js +59 -4
  77. package/generated/prisma-mongodb/package.json +1 -1
  78. package/generated/prisma-mongodb/schema.prisma +87 -0
  79. package/generated/prisma-mongodb/wasm.js +59 -4
  80. package/generated/prisma-postgresql/edge.js +58 -4
  81. package/generated/prisma-postgresql/index-browser.js +48 -1
  82. package/generated/prisma-postgresql/index.d.ts +4738 -54
  83. package/generated/prisma-postgresql/index.js +58 -4
  84. package/generated/prisma-postgresql/package.json +1 -1
  85. package/generated/prisma-postgresql/schema.prisma +85 -0
  86. package/generated/prisma-postgresql/wasm.js +58 -4
  87. package/handlers/WEBHOOKS.md +104 -81
  88. package/handlers/app-definition-loader.js +53 -10
  89. package/handlers/app-handler-helpers.js +14 -3
  90. package/handlers/backend-utils.js +0 -1
  91. package/handlers/database-migration-handler.js +64 -45
  92. package/handlers/middleware/admin-auth.js +53 -0
  93. package/handlers/routers/HEALTHCHECK.md +170 -135
  94. package/handlers/routers/admin.js +474 -0
  95. package/handlers/routers/auth.js +51 -9
  96. package/handlers/routers/db-migration.handler.js +2 -3
  97. package/handlers/routers/db-migration.js +102 -66
  98. package/handlers/routers/docs.js +180 -0
  99. package/handlers/routers/health.js +84 -283
  100. package/handlers/routers/integration-defined-routers.js +44 -33
  101. package/handlers/routers/integration-webhook-routers.js +73 -52
  102. package/handlers/routers/middleware/loadUser.js +39 -0
  103. package/handlers/routers/middleware/requireAdmin.js +43 -0
  104. package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
  105. package/handlers/routers/user.js +30 -14
  106. package/handlers/routers/websocket.js +18 -4
  107. package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
  108. package/handlers/workers/db-migration.js +74 -35
  109. package/handlers/workers/integration-defined-workers.js +31 -20
  110. package/index.js +50 -16
  111. package/infrastructure/scheduler/index.js +15 -11
  112. package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
  113. package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
  114. package/integrations/WEBHOOK-QUICKSTART.md +21 -14
  115. package/integrations/index.js +4 -0
  116. package/integrations/integration-base.js +9 -6
  117. package/integrations/integration-router.js +1828 -157
  118. package/integrations/options.js +47 -20
  119. package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
  120. package/integrations/repositories/integration-repository-documentdb.js +53 -24
  121. package/integrations/repositories/integration-repository-factory.js +6 -6
  122. package/integrations/repositories/integration-repository-interface.js +27 -9
  123. package/integrations/repositories/integration-repository-postgres.js +3 -3
  124. package/integrations/repositories/process-repository-factory.js +4 -7
  125. package/integrations/repositories/process-repository-interface.js +9 -5
  126. package/integrations/repositories/process-repository-mongo.js +7 -3
  127. package/integrations/repositories/process-repository-postgres.js +2 -2
  128. package/integrations/tests/doubles/config-capturing-integration.js +9 -8
  129. package/integrations/tests/doubles/dummy-integration-class.js +81 -8
  130. package/integrations/tests/doubles/test-integration-repository.js +51 -11
  131. package/integrations/use-cases/create-integration.js +36 -32
  132. package/integrations/use-cases/create-process.js +20 -9
  133. package/integrations/use-cases/delete-integration-for-user.js +5 -4
  134. package/integrations/use-cases/execute-proxy-request.js +564 -0
  135. package/integrations/use-cases/get-integration-for-user.js +25 -10
  136. package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
  137. package/integrations/use-cases/get-integrations-for-user.js +4 -1
  138. package/integrations/use-cases/get-possible-integrations.js +1 -1
  139. package/integrations/use-cases/get-process.js +5 -6
  140. package/integrations/use-cases/index.js +3 -1
  141. package/integrations/use-cases/load-integration-context.js +1 -5
  142. package/integrations/use-cases/update-process-state.js +10 -8
  143. package/integrations/utils/map-integration-dto.js +9 -3
  144. package/jest-setup.js +1 -1
  145. package/jest-teardown.js +1 -1
  146. package/lambda/TimeoutCatcher.js +32 -30
  147. package/lambda/index.js +1 -1
  148. package/logs/index.js +3 -3
  149. package/modules/domain/entities/AuthorizationSession.js +131 -0
  150. package/modules/domain/entities/index.js +10 -0
  151. package/modules/module.js +4 -9
  152. package/modules/repositories/authorization-session-repository-factory.js +53 -0
  153. package/modules/repositories/authorization-session-repository-interface.js +75 -0
  154. package/modules/repositories/authorization-session-repository-mongo.js +169 -0
  155. package/modules/repositories/authorization-session-repository-postgres.js +170 -0
  156. package/modules/repositories/module-repository-documentdb.js +89 -41
  157. package/modules/repositories/module-repository-factory.js +4 -6
  158. package/modules/repositories/module-repository-interface.js +13 -0
  159. package/modules/repositories/module-repository-mongo.js +40 -5
  160. package/modules/repositories/module-repository-postgres.js +42 -5
  161. package/modules/repositories/module-repository.js +28 -0
  162. package/modules/requester/api-key.js +0 -2
  163. package/modules/requester/basic.js +0 -1
  164. package/modules/requester/oauth-2.js +3 -14
  165. package/modules/test/mock-api/definition.js +15 -10
  166. package/modules/test/mock-api/mocks/hubspot.js +36 -36
  167. package/modules/tests/doubles/test-module-factory.js +10 -4
  168. package/modules/tests/doubles/test-module-repository.js +1 -1
  169. package/modules/use-cases/delete-module-entity.js +23 -0
  170. package/modules/use-cases/get-authorization-requirements.js +110 -0
  171. package/modules/use-cases/get-entities-for-user.js +1 -1
  172. package/modules/use-cases/get-entity-options-by-id.js +8 -6
  173. package/modules/use-cases/get-module-entity-by-id.js +16 -0
  174. package/modules/use-cases/get-module.js +12 -9
  175. package/modules/use-cases/process-authorization-callback.js +12 -6
  176. package/modules/use-cases/process-authorization-step.js +160 -0
  177. package/modules/use-cases/refresh-entity-options.js +8 -6
  178. package/modules/use-cases/start-authorization-session.js +86 -0
  179. package/modules/use-cases/test-module-auth.js +8 -6
  180. package/modules/use-cases/update-module-entity.js +27 -0
  181. package/modules/utils/map-module-dto.js +2 -2
  182. package/openapi/openapi-spec-generator.js +370 -0
  183. package/openapi/openapi-v1.yaml +1360 -0
  184. package/openapi/openapi-v2.yaml +1535 -0
  185. package/package.json +7 -9
  186. package/prisma-mongodb/schema.prisma +87 -0
  187. package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
  188. package/prisma-postgresql/schema.prisma +85 -0
  189. package/providers/index.js +13 -0
  190. package/providers/resolve-provider.js +93 -0
  191. package/queues/index.js +19 -0
  192. package/queues/providers/index.js +7 -0
  193. package/queues/providers/netlify-background-provider.js +76 -0
  194. package/queues/providers/qstash-queue-provider.js +122 -0
  195. package/queues/queue-client-interface.js +55 -0
  196. package/queues/queue-provider-factory.js +93 -0
  197. package/queues/queue-provider.js +46 -0
  198. package/queues/queuer-util.js +33 -25
  199. package/syncs/repositories/sync-repository-documentdb.js +39 -19
  200. package/syncs/repositories/sync-repository-factory.js +4 -4
  201. package/syncs/repositories/sync-repository-interface.js +6 -2
  202. package/syncs/repositories/sync-repository-postgres.js +19 -13
  203. package/syncs/sync.js +104 -100
  204. package/token/repositories/token-repository-factory.js +4 -6
  205. package/types/assertions/index.d.ts +72 -72
  206. package/types/core/index.d.ts +98 -53
  207. package/types/errors/index.d.ts +61 -54
  208. package/types/eslint-config/index.d.ts +40 -40
  209. package/types/lambda/index.d.ts +26 -26
  210. package/types/logs/index.d.ts +4 -4
  211. package/types/prettier-config/index.d.ts +5 -5
  212. package/types/test-environment/index.d.ts +14 -14
  213. package/types/tsconfig.json +95 -93
  214. package/user/repositories/user-repository-factory.js +4 -4
  215. package/user/tests/doubles/test-user-repository.js +5 -2
  216. package/user/use-cases/authenticate-user.js +2 -4
  217. package/user/use-cases/authenticate-with-shared-secret.js +1 -1
  218. package/user/use-cases/create-individual-user.js +8 -7
  219. package/user/use-cases/create-organization-user.js +1 -1
  220. package/user/use-cases/create-token-for-user-id.js +1 -1
  221. package/user/use-cases/delete-user.js +47 -0
  222. package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
  223. package/user/use-cases/get-user-from-bearer-token.js +7 -3
  224. package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
  225. package/user/use-cases/login-user.js +8 -8
  226. package/user/user.js +18 -4
  227. package/utils/backend-path.js +2 -2
  228. package/utils/index.js +4 -1
  229. package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
  230. package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
  231. package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
  232. package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
  233. package/websocket/repositories/websocket-connection-repository.js +26 -20
  234. package/websocket/websocket-message-sender-interface.js +38 -0
  235. package/database/adapters/lambda-invoker.js +0 -97
  236. package/database/repositories/migration-status-repository-s3.js +0 -137
  237. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
@@ -0,0 +1,564 @@
1
+ const Boom = require('@hapi/boom');
2
+
3
+ /**
4
+ * Use case for proxying HTTP requests through an entity's or credential's API connection
5
+ *
6
+ * This use case handles:
7
+ * - Entity and credential validation
8
+ * - Authentication state verification
9
+ * - HTTP method validation
10
+ * - Request forwarding to upstream API
11
+ * - Error mapping and response formatting
12
+ *
13
+ * @class ExecuteProxyRequest
14
+ */
15
+ class ExecuteProxyRequest {
16
+ /**
17
+ * @param {Object} params - Configuration parameters
18
+ * @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for entity data
19
+ * @param {import('../../credential/repositories/credential-repository-interface').CredentialRepositoryInterface} params.credentialRepository - Repository for credential data
20
+ * @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Factory for creating module instances
21
+ * @param {Array} params.moduleDefinitions - Array of module definitions
22
+ */
23
+ constructor({
24
+ moduleRepository,
25
+ credentialRepository,
26
+ moduleFactory,
27
+ moduleDefinitions,
28
+ }) {
29
+ this.moduleRepository = moduleRepository;
30
+ this.credentialRepository = credentialRepository;
31
+ this.moduleFactory = moduleFactory;
32
+ this.moduleDefinitions = moduleDefinitions;
33
+
34
+ // Valid HTTP methods for proxy requests
35
+ this.VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
36
+ }
37
+
38
+ /**
39
+ * Execute proxy request through an entity
40
+ *
41
+ * @param {string} entityId - Entity ID to proxy through
42
+ * @param {string} userId - User ID making the request
43
+ * @param {Object} proxyRequest - Proxy request parameters
44
+ * @param {string} proxyRequest.method - HTTP method (GET, POST, PUT, PATCH, DELETE)
45
+ * @param {string} proxyRequest.path - API path to call
46
+ * @param {Object} [proxyRequest.query] - Query parameters
47
+ * @param {Object} [proxyRequest.headers] - Request headers
48
+ * @param {*} [proxyRequest.body] - Request body
49
+ * @returns {Promise<Object>} Proxy response with status, headers, and data
50
+ */
51
+ async executeViaEntity(entityId, userId, proxyRequest) {
52
+ // Validate request
53
+ this._validateProxyRequest(proxyRequest);
54
+
55
+ // Load entity for user (validates ownership)
56
+ const entity = await this.moduleRepository.findByIdForUser(
57
+ entityId,
58
+ userId
59
+ );
60
+
61
+ if (!entity) {
62
+ throw Boom.notFound('Entity not found');
63
+ }
64
+
65
+ // Load credential
66
+ const credential = await this._loadAndValidateCredential(
67
+ entity.credential
68
+ );
69
+
70
+ // Get module instance with API client
71
+ const moduleInstance = await this._getModuleInstance(entityId, userId);
72
+
73
+ // Execute proxy request
74
+ return await this._executeProxyRequest(
75
+ moduleInstance.api,
76
+ proxyRequest
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Execute proxy request through a credential directly
82
+ *
83
+ * @param {string} credentialId - Credential ID to proxy through
84
+ * @param {string} userId - User ID making the request
85
+ * @param {Object} proxyRequest - Proxy request parameters
86
+ * @returns {Promise<Object>} Proxy response with status, headers, and data
87
+ */
88
+ async executeViaCredential(credentialId, userId, proxyRequest) {
89
+ // Validate request
90
+ this._validateProxyRequest(proxyRequest);
91
+
92
+ // Load credential for user (validates ownership)
93
+ const credential = await this.credentialRepository.findByIdForUser(
94
+ credentialId,
95
+ userId
96
+ );
97
+
98
+ if (!credential) {
99
+ throw Boom.notFound('Credential not found');
100
+ }
101
+
102
+ // Validate credential is usable
103
+ this._validateCredentialAuth(credential);
104
+
105
+ // Get API instance for credential
106
+ const moduleInstance = await this._getModuleInstanceFromCredential(
107
+ credential,
108
+ userId
109
+ );
110
+
111
+ // Execute proxy request
112
+ return await this._executeProxyRequest(
113
+ moduleInstance.api,
114
+ proxyRequest
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Validate proxy request parameters
120
+ *
121
+ * @private
122
+ * @param {Object} proxyRequest - Request to validate
123
+ * @throws {Boom.badRequest} When validation fails
124
+ */
125
+ _validateProxyRequest(proxyRequest) {
126
+ // Validate method
127
+ if (!proxyRequest.method) {
128
+ throw Boom.badRequest('Missing Parameter: method is required.');
129
+ }
130
+
131
+ if (!this.VALID_METHODS.includes(proxyRequest.method)) {
132
+ throw Boom.badRequest(
133
+ `Invalid method. method must be one of: ${this.VALID_METHODS.join(
134
+ ', '
135
+ )}`
136
+ );
137
+ }
138
+
139
+ // Validate path
140
+ if (!proxyRequest.path) {
141
+ throw Boom.badRequest('Missing Parameter: path is required.');
142
+ }
143
+
144
+ if (
145
+ typeof proxyRequest.path !== 'string' ||
146
+ proxyRequest.path.trim() === ''
147
+ ) {
148
+ throw Boom.badRequest('path must be a non-empty string');
149
+ }
150
+
151
+ if (!proxyRequest.path.startsWith('/')) {
152
+ throw Boom.badRequest('path must start with /');
153
+ }
154
+
155
+ // Validate query parameters (if provided)
156
+ if (proxyRequest.query !== undefined && proxyRequest.query !== null) {
157
+ if (
158
+ typeof proxyRequest.query !== 'object' ||
159
+ Array.isArray(proxyRequest.query)
160
+ ) {
161
+ throw Boom.badRequest('query must be an object');
162
+ }
163
+
164
+ // Validate each query parameter value type
165
+ for (const [key, value] of Object.entries(proxyRequest.query)) {
166
+ const valueType = typeof value;
167
+ const isValidType =
168
+ valueType === 'string' ||
169
+ valueType === 'number' ||
170
+ valueType === 'boolean' ||
171
+ Array.isArray(value);
172
+
173
+ if (!isValidType) {
174
+ throw Boom.badRequest(
175
+ `Invalid query parameter "${key}". Query parameters must be string, number, boolean, or array.`
176
+ );
177
+ }
178
+
179
+ // If array, validate all items are strings
180
+ if (Array.isArray(value)) {
181
+ for (const item of value) {
182
+ if (typeof item !== 'string') {
183
+ throw Boom.badRequest(
184
+ `Invalid query parameter "${key}". Query array items must be strings.`
185
+ );
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Validate headers (if provided)
193
+ if (
194
+ proxyRequest.headers !== undefined &&
195
+ proxyRequest.headers !== null
196
+ ) {
197
+ if (
198
+ typeof proxyRequest.headers !== 'object' ||
199
+ Array.isArray(proxyRequest.headers)
200
+ ) {
201
+ throw Boom.badRequest('headers must be an object');
202
+ }
203
+
204
+ // Validate each header value is a string
205
+ for (const [key, value] of Object.entries(proxyRequest.headers)) {
206
+ if (typeof value !== 'string') {
207
+ throw Boom.badRequest(
208
+ `Invalid header "${key}". Headers must be strings.`
209
+ );
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Load and validate credential
217
+ *
218
+ * @private
219
+ * @param {string} credentialId - Credential ID to load
220
+ * @returns {Promise<Object>} Credential object
221
+ * @throws {Boom} When credential is invalid
222
+ */
223
+ async _loadAndValidateCredential(credentialId) {
224
+ if (!credentialId) {
225
+ throw Boom.badRequest('Entity has no credential associated');
226
+ }
227
+
228
+ const credential = await this.credentialRepository.findById(
229
+ credentialId
230
+ );
231
+
232
+ if (!credential) {
233
+ throw Boom.notFound('Credential not found');
234
+ }
235
+
236
+ this._validateCredentialAuth(credential);
237
+
238
+ return credential;
239
+ }
240
+
241
+ /**
242
+ * Validate credential has valid authentication data
243
+ *
244
+ * @private
245
+ * @param {Object} credential - Credential to validate
246
+ * @throws {Boom.unauthorized} When credential is invalid
247
+ */
248
+ _validateCredentialAuth(credential) {
249
+ // Check credential status
250
+ if (credential.status && credential.status !== 'AUTHORIZED') {
251
+ throw Boom.unauthorized(
252
+ 'Credential is not authorized. Please reauthorize your connection.',
253
+ 'INVALID_CREDENTIALS'
254
+ );
255
+ }
256
+
257
+ // Check credential has auth data
258
+ if (!credential.data || !credential.data.access_token) {
259
+ throw Boom.unauthorized(
260
+ 'Credential is missing required authentication data',
261
+ 'INVALID_CREDENTIALS'
262
+ );
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get module instance with API client
268
+ *
269
+ * @private
270
+ * @param {string} entityId - Entity ID
271
+ * @param {string} userId - User ID
272
+ * @returns {Promise<Object>} Module instance with API client
273
+ */
274
+ async _getModuleInstance(entityId, userId) {
275
+ try {
276
+ const moduleInstance = await this.moduleFactory.getModuleInstance(
277
+ entityId,
278
+ userId
279
+ );
280
+
281
+ if (!moduleInstance || !moduleInstance.api) {
282
+ throw Boom.internal(
283
+ 'Failed to initialize API client for entity'
284
+ );
285
+ }
286
+
287
+ return moduleInstance;
288
+ } catch (error) {
289
+ if (Boom.isBoom(error)) {
290
+ throw error;
291
+ }
292
+ throw Boom.internal('Failed to load module instance', error);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Get module instance from credential
298
+ *
299
+ * @private
300
+ * @param {Object} credential - Credential object
301
+ * @param {string} userId - User ID
302
+ * @returns {Promise<Object>} Module instance with API client
303
+ */
304
+ async _getModuleInstanceFromCredential(credential, userId) {
305
+ try {
306
+ // Find module definition for this credential type
307
+ const moduleDefinition = this.moduleDefinitions.find(
308
+ (def) => def.moduleName === credential.type
309
+ );
310
+
311
+ if (!moduleDefinition) {
312
+ throw Boom.badRequest(
313
+ `Unknown credential type: ${credential.type}`
314
+ );
315
+ }
316
+
317
+ // Create API instance directly from credential
318
+ const ModuleDefinition = moduleDefinition.definition;
319
+ const api = new ModuleDefinition.Api(credential);
320
+
321
+ return { api };
322
+ } catch (error) {
323
+ if (Boom.isBoom(error)) {
324
+ throw error;
325
+ }
326
+ throw Boom.internal(
327
+ 'Failed to initialize API client from credential',
328
+ error
329
+ );
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Sensitive headers that should be stripped from outgoing requests
335
+ * @private
336
+ */
337
+ static SENSITIVE_REQUEST_HEADERS = ['authorization', 'cookie', 'x-api-key'];
338
+
339
+ /**
340
+ * Sensitive headers that should be stripped from upstream responses
341
+ * @private
342
+ */
343
+ static SENSITIVE_RESPONSE_HEADERS = [
344
+ 'authorization',
345
+ 'set-cookie',
346
+ 'x-api-key',
347
+ ];
348
+
349
+ /**
350
+ * Strip sensitive headers from request headers
351
+ * @private
352
+ * @param {Object} headers - Request headers
353
+ * @returns {Object} Sanitized headers
354
+ */
355
+ _sanitizeRequestHeaders(headers) {
356
+ if (!headers) return {};
357
+ const sanitized = { ...headers };
358
+ for (const key of Object.keys(sanitized)) {
359
+ if (
360
+ ExecuteProxyRequest.SENSITIVE_REQUEST_HEADERS.includes(
361
+ key.toLowerCase()
362
+ )
363
+ ) {
364
+ delete sanitized[key];
365
+ }
366
+ }
367
+ return sanitized;
368
+ }
369
+
370
+ /**
371
+ * Strip sensitive headers from response headers
372
+ * @private
373
+ * @param {Object} headers - Response headers
374
+ * @returns {Object} Sanitized headers
375
+ */
376
+ _sanitizeResponseHeaders(headers) {
377
+ if (!headers) return {};
378
+ const sanitized = { ...headers };
379
+ for (const key of Object.keys(sanitized)) {
380
+ if (
381
+ ExecuteProxyRequest.SENSITIVE_RESPONSE_HEADERS.includes(
382
+ key.toLowerCase()
383
+ )
384
+ ) {
385
+ delete sanitized[key];
386
+ }
387
+ }
388
+ return sanitized;
389
+ }
390
+
391
+ /**
392
+ * Execute the actual proxy request through the API client
393
+ *
394
+ * @private
395
+ * @param {Object} apiClient - API client instance (Requester)
396
+ * @param {Object} proxyRequest - Proxy request parameters
397
+ * @returns {Promise<Object>} Formatted proxy response
398
+ */
399
+ async _executeProxyRequest(apiClient, proxyRequest) {
400
+ try {
401
+ // Sanitize request headers (strip Authorization, etc.)
402
+ const sanitizedHeaders = this._sanitizeRequestHeaders(
403
+ proxyRequest.headers
404
+ );
405
+
406
+ // Make the upstream API request
407
+ const upstreamResponse = await apiClient.request({
408
+ method: proxyRequest.method,
409
+ url: proxyRequest.path,
410
+ query: proxyRequest.query,
411
+ headers: sanitizedHeaders,
412
+ body: proxyRequest.body,
413
+ });
414
+
415
+ // Return successful response with sanitized headers
416
+ return {
417
+ success: true,
418
+ status: upstreamResponse.status,
419
+ headers: this._sanitizeResponseHeaders(
420
+ upstreamResponse.headers
421
+ ),
422
+ data: upstreamResponse.data,
423
+ };
424
+ } catch (error) {
425
+ // Map upstream errors to proxy error responses
426
+ return this._mapUpstreamError(error);
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Map upstream API errors to standardized proxy error responses
432
+ *
433
+ * @private
434
+ * @param {Error} error - Upstream error
435
+ * @returns {Object} Formatted error response
436
+ * @throws {Boom} Rethrows as Boom error with appropriate status
437
+ */
438
+ _mapUpstreamError(error) {
439
+ // Check if this is a timeout error
440
+ if (error.code === 'ETIMEDOUT' || error.type === 'request-timeout') {
441
+ throw Boom.gatewayTimeout('Request to upstream API timed out', {
442
+ code: 'TIMEOUT',
443
+ details: null,
444
+ });
445
+ }
446
+
447
+ // Check if this is a network error
448
+ if (
449
+ error.code === 'ENOTFOUND' ||
450
+ error.code === 'ECONNREFUSED' ||
451
+ error.type === 'system'
452
+ ) {
453
+ throw Boom.badGateway('Failed to connect to upstream API', {
454
+ code: 'NETWORK_ERROR',
455
+ details: {
456
+ error: error.message,
457
+ },
458
+ });
459
+ }
460
+
461
+ // Check if we have an HTTP response from upstream
462
+ if (!error.response) {
463
+ // Unknown error without response
464
+ throw Boom.internal('Unexpected error calling upstream API', {
465
+ code: 'UNKNOWN_ERROR',
466
+ details: {
467
+ error: error.message,
468
+ },
469
+ });
470
+ }
471
+
472
+ const { status, data } = error.response;
473
+
474
+ // Map by status code
475
+ switch (status) {
476
+ case 401: {
477
+ // Check if this is specifically a token expiration
478
+ const isExpired =
479
+ data?.error === 'token_expired' ||
480
+ data?.error === 'expired_token' ||
481
+ (data?.error_description &&
482
+ data.error_description
483
+ .toLowerCase()
484
+ .includes('expired'));
485
+
486
+ const code = isExpired ? 'EXPIRED_TOKEN' : 'INVALID_AUTH';
487
+ const message = isExpired
488
+ ? 'Access token has expired'
489
+ : 'Authentication credentials are invalid or expired';
490
+
491
+ // Note: Boom.unauthorized(message, scheme, attributes) - second param is WWW-Authenticate scheme
492
+ // We pass null for scheme and set data manually
493
+ const boomError = Boom.unauthorized(message);
494
+ boomError.data = {
495
+ code,
496
+ details: data,
497
+ upstreamStatus: status,
498
+ };
499
+ throw boomError;
500
+ }
501
+
502
+ case 403:
503
+ throw Boom.forbidden(
504
+ 'Insufficient permissions for this operation',
505
+ {
506
+ code: 'PERMISSION_DENIED',
507
+ details: data,
508
+ upstreamStatus: status,
509
+ }
510
+ );
511
+
512
+ case 404:
513
+ throw Boom.notFound('Resource not found', {
514
+ code: 'NOT_FOUND',
515
+ details: data,
516
+ upstreamStatus: status,
517
+ });
518
+
519
+ case 429:
520
+ throw Boom.tooManyRequests('Rate limit exceeded for this API', {
521
+ code: 'RATE_LIMITED',
522
+ details: data,
523
+ upstreamStatus: status,
524
+ });
525
+
526
+ case 503:
527
+ throw Boom.serverUnavailable(
528
+ 'Upstream service is unavailable',
529
+ {
530
+ code: 'SERVICE_UNAVAILABLE',
531
+ details: data,
532
+ upstreamStatus: status,
533
+ }
534
+ );
535
+
536
+ default: {
537
+ // For all other errors (400, 500, etc.)
538
+ const boomError =
539
+ status >= 500
540
+ ? Boom.internal('Upstream API returned an error', {
541
+ code: 'UPSTREAM_ERROR',
542
+ details: data,
543
+ upstreamStatus: status,
544
+ })
545
+ : Boom.badRequest('Upstream API returned an error', {
546
+ code: 'UPSTREAM_ERROR',
547
+ details: data,
548
+ upstreamStatus: status,
549
+ });
550
+
551
+ // Override status to match upstream
552
+ boomError.output.statusCode = status;
553
+ // For 5xx errors, Boom.internal uses a generic message, so override it
554
+ if (status >= 500) {
555
+ boomError.output.payload.message =
556
+ 'Upstream API returned an error';
557
+ }
558
+ throw boomError;
559
+ }
560
+ }
561
+ }
562
+ }
563
+
564
+ module.exports = { ExecuteProxyRequest };
@@ -1,5 +1,7 @@
1
1
  // Removed Integration wrapper - using IntegrationBase directly
2
- const { mapIntegrationClassToIntegrationDTO } = require('../utils/map-integration-dto');
2
+ const {
3
+ mapIntegrationClassToIntegrationDTO,
4
+ } = require('../utils/map-integration-dto');
3
5
  const Boom = require('@hapi/boom');
4
6
 
5
7
  /**
@@ -15,8 +17,12 @@ class GetIntegrationForUser {
15
17
  * @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management.
16
18
  * @param {import('../../modules/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for module and entity data operations.
17
19
  */
18
- constructor({ integrationRepository, integrationClasses, moduleFactory, moduleRepository }) {
19
-
20
+ constructor({
21
+ integrationRepository,
22
+ integrationClasses,
23
+ moduleFactory,
24
+ moduleRepository,
25
+ }) {
20
26
  /**
21
27
  * @type {import('../integration-repository-interface').IntegrationRepositoryInterface}
22
28
  */
@@ -36,19 +42,28 @@ class GetIntegrationForUser {
36
42
  * @throws {Boom.forbidden} When user does not have access to the integration.
37
43
  */
38
44
  async execute(integrationId, userId) {
39
- const integrationRecord = await this.integrationRepository.findIntegrationById(integrationId);
40
- const entities = await this.moduleRepository.findEntitiesByIds(integrationRecord.entitiesIds);
45
+ const integrationRecord =
46
+ await this.integrationRepository.findIntegrationById(integrationId);
47
+ const entities = await this.moduleRepository.findEntitiesByIds(
48
+ integrationRecord.entitiesIds
49
+ );
41
50
 
42
51
  if (!integrationRecord) {
43
- throw Boom.notFound(`Integration with id of ${integrationId} does not exist`);
52
+ throw Boom.notFound(
53
+ `Integration with id of ${integrationId} does not exist`
54
+ );
44
55
  }
45
56
 
46
57
  if (integrationRecord.userId.toString() !== userId.toString()) {
47
- throw Boom.forbidden('User does not have access to this integration');
58
+ throw Boom.forbidden(
59
+ 'User does not have access to this integration'
60
+ );
48
61
  }
49
62
 
50
63
  const integrationClass = this.integrationClasses.find(
51
- (integrationClass) => integrationClass.Definition.name === integrationRecord.config.type
64
+ (integrationClass) =>
65
+ integrationClass.Definition.name ===
66
+ integrationRecord.config.type
52
67
  );
53
68
 
54
69
  const modules = [];
@@ -68,11 +83,11 @@ class GetIntegrationForUser {
68
83
  status: integrationRecord.status,
69
84
  version: integrationRecord.version,
70
85
  messages: integrationRecord.messages,
71
- modules
86
+ modules,
72
87
  });
73
88
 
74
89
  return mapIntegrationClassToIntegrationDTO(integrationInstance);
75
90
  }
76
91
  }
77
92
 
78
- module.exports = { GetIntegrationForUser };
93
+ module.exports = { GetIntegrationForUser };
@@ -14,7 +14,6 @@ class GetIntegrationInstanceByDefinition {
14
14
  * @param {import('../../modules/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for module and entity data operations.
15
15
  */
16
16
  constructor({ integrationRepository, moduleFactory, moduleRepository }) {
17
-
18
17
  /**
19
18
  * @type {import('../integration-repository-interface').IntegrationRepositoryInterface}
20
19
  */
@@ -30,13 +29,20 @@ class GetIntegrationInstanceByDefinition {
30
29
  * @throws {Boom.notFound} When integration with the specified definition does not exist.
31
30
  */
32
31
  async execute(integrationClass) {
33
- const integrationRecord = await this.integrationRepository.findIntegrationByName(integrationClass.Definition.name);
32
+ const integrationRecord =
33
+ await this.integrationRepository.findIntegrationByName(
34
+ integrationClass.Definition.name
35
+ );
34
36
 
35
37
  if (!integrationRecord) {
36
- throw Boom.notFound(`Integration with name of ${integrationClass.Definition.name} does not exist`);
38
+ throw Boom.notFound(
39
+ `Integration with name of ${integrationClass.Definition.name} does not exist`
40
+ );
37
41
  }
38
42
 
39
- const entities = await this.moduleRepository.findEntitiesByIds(integrationRecord.entitiesIds);
43
+ const entities = await this.moduleRepository.findEntitiesByIds(
44
+ integrationRecord.entitiesIds
45
+ );
40
46
 
41
47
  const modules = [];
42
48
  for (const entity of entities) {
@@ -55,13 +61,13 @@ class GetIntegrationInstanceByDefinition {
55
61
  status: integrationRecord.status,
56
62
  version: integrationRecord.version,
57
63
  messages: integrationRecord.messages,
58
- modules
64
+ modules,
59
65
  });
60
66
 
61
67
  await integrationInstance.initialize();
62
68
 
63
- return integrationInstance
69
+ return integrationInstance;
64
70
  }
65
71
  }
66
72
 
67
- module.exports = { GetIntegrationInstanceByDefinition };
73
+ module.exports = { GetIntegrationInstanceByDefinition };
@@ -71,7 +71,10 @@ class GetIntegrationsForUser {
71
71
  config: integrationRecord.config,
72
72
  status: integrationRecord.status,
73
73
  version: integrationRecord.version,
74
- messages: integrationRecord.messages || { errors: [], warnings: [] },
74
+ messages: integrationRecord.messages || {
75
+ errors: [],
76
+ warnings: [],
77
+ },
75
78
  modules,
76
79
  options: integrationClass.getOptionDetails(),
77
80
  };
@@ -24,4 +24,4 @@ class GetPossibleIntegrations {
24
24
  }
25
25
  }
26
26
 
27
- module.exports = { GetPossibleIntegrations };
27
+ module.exports = { GetPossibleIntegrations };