@friggframework/core 2.0.0--canary.522.cbd3d5a.0 → 2.0.0--canary.517.35ee143.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 (225) hide show
  1. package/CHANGELOG.md +67 -66
  2. package/CLAUDE.md +124 -148
  3. package/README.md +130 -165
  4. package/admin-scripts/index.js +16 -30
  5. package/admin-scripts/repositories/{admin-api-key-repository-documentdb.js → admin-process-repository-documentdb.js} +5 -5
  6. package/admin-scripts/repositories/{admin-api-key-repository-factory.js → admin-process-repository-factory.js} +15 -19
  7. package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
  8. package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
  9. package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
  10. package/admin-scripts/repositories/script-schedule-repository-factory.js +2 -6
  11. package/admin-scripts/repositories/script-schedule-repository-interface.js +14 -36
  12. package/admin-scripts/repositories/script-schedule-repository-mongo.js +13 -25
  13. package/admin-scripts/repositories/script-schedule-repository-postgres.js +13 -25
  14. package/application/commands/admin-script-commands.js +102 -266
  15. package/application/commands/integration-commands.js +6 -12
  16. package/application/index.js +3 -1
  17. package/assertions/get.js +4 -1
  18. package/associations/association.js +57 -59
  19. package/associations/model.js +41 -41
  20. package/core/CLAUDE.md +156 -222
  21. package/core/Worker.js +1 -5
  22. package/credential/repositories/credential-repository-documentdb.js +1 -71
  23. package/credential/repositories/credential-repository-interface.js +2 -8
  24. package/credential/repositories/credential-repository-mongo.js +4 -45
  25. package/credential/repositories/credential-repository-postgres.js +3 -44
  26. package/credential/repositories/credential-repository.js +4 -6
  27. package/credential/use-cases/get-credential-for-user.js +3 -21
  28. package/credential/use-cases/update-authentication-status.js +2 -5
  29. package/database/MONGODB_TRANSACTION_FIX.md +52 -72
  30. package/database/adapters/lambda-invoker.js +7 -8
  31. package/database/config.js +13 -15
  32. package/database/documentdb-encryption-service.js +13 -29
  33. package/database/documentdb-utils.js +12 -21
  34. package/database/encryption/README.md +36 -53
  35. package/database/encryption/encryption-schema-registry.js +9 -21
  36. package/database/encryption/field-encryption-service.js +4 -15
  37. package/database/encryption/logger.js +2 -3
  38. package/database/encryption/prisma-encryption-extension.js +8 -5
  39. package/database/index.js +2 -4
  40. package/database/models/IndividualUser.js +15 -15
  41. package/database/models/OrganizationUser.js +4 -6
  42. package/database/models/UserModel.js +2 -2
  43. package/database/models/WebsocketConnection.js +1 -4
  44. package/database/mongoose.js +2 -2
  45. package/database/prisma.js +14 -22
  46. package/database/repositories/health-check-repository-documentdb.js +7 -22
  47. package/database/repositories/health-check-repository-factory.js +3 -9
  48. package/database/repositories/health-check-repository-interface.js +5 -15
  49. package/database/repositories/health-check-repository-mongodb.js +6 -9
  50. package/database/repositories/health-check-repository-postgres.js +1 -1
  51. package/database/repositories/migration-status-repository-s3.js +5 -10
  52. package/database/use-cases/check-database-health-use-case.js +2 -3
  53. package/database/use-cases/check-database-state-use-case.js +7 -9
  54. package/database/use-cases/get-database-state-via-worker-use-case.js +12 -15
  55. package/database/use-cases/get-migration-status-use-case.js +2 -4
  56. package/database/use-cases/run-database-migration-use-case.js +9 -41
  57. package/database/use-cases/test-encryption-use-case.js +5 -4
  58. package/database/use-cases/trigger-database-migration-use-case.js +8 -9
  59. package/database/utils/mongodb-collection-utils.js +3 -11
  60. package/database/utils/mongodb-schema-init.js +4 -10
  61. package/database/utils/prisma-runner.js +74 -144
  62. package/database/utils/prisma-schema-parser.js +2 -2
  63. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +80 -135
  64. package/encrypt/Cryptor.js +2 -8
  65. package/errors/fetch-error.js +1 -1
  66. package/generated/prisma-mongodb/edge.js +16 -59
  67. package/generated/prisma-mongodb/index-browser.js +12 -62
  68. package/generated/prisma-mongodb/index.d.ts +4850 -8136
  69. package/generated/prisma-mongodb/index.js +16 -59
  70. package/generated/prisma-mongodb/package.json +1 -1
  71. package/generated/prisma-mongodb/schema.prisma +33 -75
  72. package/generated/prisma-mongodb/wasm.js +16 -59
  73. package/generated/prisma-postgresql/edge.js +15 -59
  74. package/generated/prisma-postgresql/index-browser.js +11 -62
  75. package/generated/prisma-postgresql/index.d.ts +5963 -8930
  76. package/generated/prisma-postgresql/index.js +15 -59
  77. package/generated/prisma-postgresql/package.json +1 -1
  78. package/generated/prisma-postgresql/schema.prisma +33 -73
  79. package/generated/prisma-postgresql/wasm.js +15 -59
  80. package/handlers/WEBHOOKS.md +81 -104
  81. package/handlers/app-definition-loader.js +1 -1
  82. package/handlers/app-handler-helpers.js +2 -14
  83. package/handlers/backend-utils.js +1 -0
  84. package/handlers/database-migration-handler.js +45 -64
  85. package/handlers/middleware/admin-auth.js +53 -0
  86. package/handlers/routers/HEALTHCHECK.md +135 -170
  87. package/handlers/routers/auth.js +2 -23
  88. package/handlers/routers/db-migration.handler.js +3 -2
  89. package/handlers/routers/db-migration.js +33 -69
  90. package/handlers/routers/health.js +8 -23
  91. package/handlers/routers/integration-defined-routers.js +4 -4
  92. package/handlers/routers/integration-webhook-routers.js +7 -17
  93. package/handlers/routers/websocket.js +3 -9
  94. package/handlers/use-cases/check-integrations-health-use-case.js +6 -13
  95. package/handlers/workers/db-migration.js +26 -54
  96. package/index.js +9 -5
  97. package/integrations/WEBHOOK-QUICKSTART.md +14 -21
  98. package/integrations/index.js +0 -4
  99. package/integrations/integration-base.js +6 -9
  100. package/integrations/integration-router.js +146 -1817
  101. package/integrations/options.js +20 -47
  102. package/integrations/repositories/integration-repository-documentdb.js +24 -53
  103. package/integrations/repositories/integration-repository-factory.js +2 -6
  104. package/integrations/repositories/integration-repository-interface.js +9 -27
  105. package/integrations/repositories/integration-repository-postgres.js +3 -3
  106. package/integrations/repositories/process-repository-factory.js +1 -0
  107. package/integrations/repositories/process-repository-interface.js +5 -9
  108. package/integrations/repositories/process-repository-mongo.js +3 -7
  109. package/integrations/repositories/process-repository-postgres.js +2 -2
  110. package/integrations/tests/doubles/config-capturing-integration.js +8 -9
  111. package/integrations/tests/doubles/dummy-integration-class.js +8 -81
  112. package/integrations/tests/doubles/test-integration-repository.js +11 -51
  113. package/integrations/use-cases/create-integration.js +32 -36
  114. package/integrations/use-cases/create-process.js +9 -20
  115. package/integrations/use-cases/delete-integration-for-user.js +4 -5
  116. package/integrations/use-cases/get-integration-for-user.js +10 -25
  117. package/integrations/use-cases/get-integration-instance-by-definition.js +7 -13
  118. package/integrations/use-cases/get-integrations-for-user.js +1 -4
  119. package/integrations/use-cases/get-possible-integrations.js +1 -1
  120. package/integrations/use-cases/get-process.js +6 -5
  121. package/integrations/use-cases/index.js +1 -3
  122. package/integrations/use-cases/load-integration-context.js +5 -1
  123. package/integrations/use-cases/update-process-state.js +8 -10
  124. package/integrations/utils/map-integration-dto.js +3 -9
  125. package/jest-setup.js +1 -1
  126. package/jest-teardown.js +1 -1
  127. package/lambda/TimeoutCatcher.js +30 -32
  128. package/lambda/index.js +1 -1
  129. package/logs/index.js +3 -3
  130. package/modules/module.js +1 -3
  131. package/modules/repositories/module-repository-documentdb.js +48 -114
  132. package/modules/repositories/module-repository-interface.js +0 -13
  133. package/modules/repositories/module-repository-mongo.js +6 -46
  134. package/modules/repositories/module-repository-postgres.js +6 -48
  135. package/modules/repositories/module-repository.js +1 -34
  136. package/modules/requester/api-key.js +2 -0
  137. package/modules/requester/basic.js +1 -0
  138. package/modules/requester/oauth-2.js +2 -3
  139. package/modules/test/mock-api/definition.js +10 -15
  140. package/modules/test/mock-api/mocks/hubspot.js +36 -36
  141. package/modules/tests/doubles/test-module-factory.js +4 -10
  142. package/modules/tests/doubles/test-module-repository.js +1 -1
  143. package/modules/use-cases/get-entities-for-user.js +1 -1
  144. package/modules/use-cases/get-entity-options-by-id.js +6 -8
  145. package/modules/use-cases/get-module.js +9 -12
  146. package/modules/use-cases/process-authorization-callback.js +6 -12
  147. package/modules/use-cases/refresh-entity-options.js +6 -8
  148. package/modules/use-cases/test-module-auth.js +6 -8
  149. package/modules/utils/map-module-dto.js +2 -2
  150. package/package.json +5 -7
  151. package/prisma-mongodb/schema.prisma +33 -75
  152. package/prisma-postgresql/schema.prisma +33 -73
  153. package/queues/queuer-util.js +1 -5
  154. package/syncs/model.js +51 -51
  155. package/syncs/repositories/sync-repository-documentdb.js +19 -39
  156. package/syncs/repositories/sync-repository-interface.js +2 -6
  157. package/syncs/repositories/sync-repository-postgres.js +13 -19
  158. package/syncs/sync.js +100 -104
  159. package/token/repositories/token-repository-factory.js +3 -1
  160. package/types/assertions/index.d.ts +72 -72
  161. package/types/associations/index.d.ts +57 -57
  162. package/types/core/index.d.ts +53 -59
  163. package/types/database/index.d.ts +2 -2
  164. package/types/encrypt/index.d.ts +3 -3
  165. package/types/errors/index.d.ts +54 -61
  166. package/types/eslint-config/index.d.ts +40 -40
  167. package/types/integrations/index.d.ts +183 -185
  168. package/types/lambda/index.d.ts +26 -26
  169. package/types/logs/index.d.ts +4 -4
  170. package/types/module-plugin/index.d.ts +237 -239
  171. package/types/prettier-config/index.d.ts +5 -5
  172. package/types/syncs/index.d.ts +108 -102
  173. package/types/test-environment/index.d.ts +14 -14
  174. package/types/tsconfig.json +93 -95
  175. package/user/tests/doubles/test-user-repository.js +2 -5
  176. package/user/use-cases/authenticate-user.js +4 -2
  177. package/user/use-cases/authenticate-with-shared-secret.js +1 -1
  178. package/user/use-cases/create-individual-user.js +7 -8
  179. package/user/use-cases/create-organization-user.js +1 -1
  180. package/user/use-cases/create-token-for-user-id.js +1 -1
  181. package/user/use-cases/get-user-from-adopter-jwt.js +2 -0
  182. package/user/use-cases/get-user-from-bearer-token.js +3 -7
  183. package/user/use-cases/get-user-from-x-frigg-headers.js +11 -14
  184. package/user/use-cases/login-user.js +8 -8
  185. package/user/user.js +4 -18
  186. package/utils/backend-path.js +2 -2
  187. package/utils/index.js +1 -4
  188. package/websocket/repositories/websocket-connection-repository-documentdb.js +8 -21
  189. package/websocket/repositories/websocket-connection-repository-mongo.js +4 -8
  190. package/websocket/repositories/websocket-connection-repository-postgres.js +4 -8
  191. package/websocket/repositories/websocket-connection-repository.js +4 -8
  192. package/admin-scripts/repositories/admin-api-key-repository-interface.js +0 -121
  193. package/admin-scripts/repositories/admin-api-key-repository-mongo.js +0 -155
  194. package/admin-scripts/repositories/admin-api-key-repository-postgres.js +0 -189
  195. package/admin-scripts/repositories/script-execution-repository-documentdb.js +0 -21
  196. package/admin-scripts/repositories/script-execution-repository-factory.js +0 -55
  197. package/admin-scripts/repositories/script-execution-repository-interface.js +0 -193
  198. package/admin-scripts/repositories/script-execution-repository-mongo.js +0 -278
  199. package/admin-scripts/repositories/script-execution-repository-postgres.js +0 -320
  200. package/credential/credential-router.js +0 -262
  201. package/credential/use-cases/delete-credential-for-user.js +0 -50
  202. package/credential/use-cases/list-credentials-for-user.js +0 -31
  203. package/credential/use-cases/reauthorize-credential.js +0 -103
  204. package/handlers/routers/admin.js +0 -450
  205. package/handlers/routers/docs.js +0 -180
  206. package/handlers/routers/middleware/loadUser.js +0 -39
  207. package/handlers/routers/middleware/requireAdmin.js +0 -41
  208. package/handlers/routers/middleware/requireLoggedInUser.js +0 -19
  209. package/integrations/use-cases/execute-proxy-request.js +0 -564
  210. package/modules/domain/entities/AuthorizationSession.js +0 -131
  211. package/modules/domain/entities/index.js +0 -10
  212. package/modules/repositories/authorization-session-repository-factory.js +0 -55
  213. package/modules/repositories/authorization-session-repository-interface.js +0 -75
  214. package/modules/repositories/authorization-session-repository-mongo.js +0 -169
  215. package/modules/repositories/authorization-session-repository-postgres.js +0 -170
  216. package/modules/use-cases/delete-module-entity.js +0 -23
  217. package/modules/use-cases/get-authorization-requirements.js +0 -110
  218. package/modules/use-cases/get-module-entity-by-id.js +0 -16
  219. package/modules/use-cases/process-authorization-step.js +0 -160
  220. package/modules/use-cases/start-authorization-session.js +0 -86
  221. package/modules/use-cases/update-module-entity.js +0 -27
  222. package/openapi/openapi-spec-generator.js +0 -370
  223. package/openapi/openapi-v1.yaml +0 -1360
  224. package/openapi/openapi-v2.yaml +0 -1535
  225. package/user/use-cases/delete-user.js +0 -47
@@ -17,15 +17,6 @@ const {
17
17
  const {
18
18
  GetCredentialForUser,
19
19
  } = require('../credential/use-cases/get-credential-for-user');
20
- const {
21
- ListCredentialsForUser,
22
- } = require('../credential/use-cases/list-credentials-for-user');
23
- const {
24
- DeleteCredentialForUser,
25
- } = require('../credential/use-cases/delete-credential-for-user');
26
- const {
27
- ReauthorizeCredential,
28
- } = require('../credential/use-cases/reauthorize-credential');
29
20
  const { CreateIntegration } = require('./use-cases/create-integration');
30
21
  const { ModuleFactory } = require('../modules/module-factory');
31
22
  const {
@@ -78,19 +69,6 @@ const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
78
69
  const {
79
70
  ProcessAuthorizationCallback,
80
71
  } = require('../modules/use-cases/process-authorization-callback');
81
- const {
82
- createAuthorizationSessionRepository,
83
- } = require('../modules/repositories/authorization-session-repository-factory');
84
- const {
85
- StartAuthorizationSessionUseCase,
86
- } = require('../modules/use-cases/start-authorization-session');
87
- const {
88
- ProcessAuthorizationStepUseCase,
89
- } = require('../modules/use-cases/process-authorization-step');
90
- const {
91
- GetAuthorizationRequirementsUseCase,
92
- } = require('../modules/use-cases/get-authorization-requirements');
93
- const { ExecuteProxyRequest } = require('./use-cases/execute-proxy-request');
94
72
 
95
73
  function createIntegrationRouter() {
96
74
  const { integrations: integrationClasses, userConfig } =
@@ -99,7 +77,6 @@ function createIntegrationRouter() {
99
77
  const integrationRepository = createIntegrationRepository();
100
78
  const credentialRepository = createCredentialRepository();
101
79
  const userRepository = createUserRepository();
102
- const authSessionRepository = createAuthorizationSessionRepository();
103
80
 
104
81
  const getUserFromBearerToken = new GetUserFromBearerToken({
105
82
  userRepository,
@@ -126,18 +103,10 @@ function createIntegrationRouter() {
126
103
  userConfig,
127
104
  });
128
105
 
129
- // Support both integration classes and direct module definitions (for testing)
130
- const isModuleDefinitionFormat =
131
- integrationClasses &&
132
- integrationClasses[0] &&
133
- integrationClasses[0].moduleName &&
134
- integrationClasses[0].definition;
135
-
136
106
  const moduleFactory = new ModuleFactory({
137
107
  moduleRepository,
138
- moduleDefinitions: isModuleDefinitionFormat
139
- ? integrationClasses
140
- : getModulesDefinitionFromIntegrationClasses(integrationClasses),
108
+ moduleDefinitions:
109
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
141
110
  });
142
111
  const deleteIntegrationForUser = new DeleteIntegrationForUser({
143
112
  integrationRepository,
@@ -156,32 +125,16 @@ function createIntegrationRouter() {
156
125
  credentialRepository,
157
126
  });
158
127
 
159
- const listCredentialsForUser = new ListCredentialsForUser({
160
- credentialRepository,
161
- });
162
-
163
- const deleteCredentialForUser = new DeleteCredentialForUser({
164
- credentialRepository,
165
- });
166
-
167
- const reauthorizeCredential = new ReauthorizeCredential({
168
- credentialRepository,
169
- moduleRepository,
170
- });
171
-
172
128
  const createIntegration = new CreateIntegration({
173
129
  integrationRepository,
174
130
  integrationClasses,
175
131
  moduleFactory,
176
132
  });
177
133
 
178
- const moduleDefinitions = isModuleDefinitionFormat
179
- ? integrationClasses
180
- : getModulesDefinitionFromIntegrationClasses(integrationClasses);
181
-
182
134
  const getEntitiesForUser = new GetEntitiesForUser({
183
135
  moduleRepository,
184
- moduleDefinitions,
136
+ moduleDefinitions:
137
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
185
138
  });
186
139
 
187
140
  const getIntegrationInstance = new GetIntegrationInstance({
@@ -197,31 +150,37 @@ function createIntegrationRouter() {
197
150
  });
198
151
 
199
152
  const getModuleInstanceFromType = new GetModuleInstanceFromType({
200
- moduleDefinitions,
153
+ moduleDefinitions:
154
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
201
155
  });
202
156
 
203
157
  const getEntityOptionsByType = new GetEntityOptionsByType({
204
- moduleDefinitions,
158
+ moduleDefinitions:
159
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
205
160
  });
206
161
 
207
162
  const testModuleAuth = new TestModuleAuth({
208
163
  moduleRepository,
209
- moduleDefinitions,
164
+ moduleDefinitions:
165
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
210
166
  });
211
167
 
212
168
  const getModule = new GetModule({
213
169
  moduleRepository,
214
- moduleDefinitions,
170
+ moduleDefinitions:
171
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
215
172
  });
216
173
 
217
174
  const getEntityOptionsById = new GetEntityOptionsById({
218
175
  moduleRepository,
219
- moduleDefinitions,
176
+ moduleDefinitions:
177
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
220
178
  });
221
179
 
222
180
  const refreshEntityOptions = new RefreshEntityOptions({
223
181
  moduleRepository,
224
- moduleDefinitions,
182
+ moduleDefinitions:
183
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
225
184
  });
226
185
 
227
186
  const getPossibleIntegrations = new GetPossibleIntegrations({
@@ -231,28 +190,8 @@ function createIntegrationRouter() {
231
190
  const processAuthorizationCallback = new ProcessAuthorizationCallback({
232
191
  moduleRepository,
233
192
  credentialRepository,
234
- moduleDefinitions,
235
- });
236
-
237
- const startAuthorizationSession = new StartAuthorizationSessionUseCase({
238
- authSessionRepository,
239
- });
240
-
241
- const processAuthorizationStep = new ProcessAuthorizationStepUseCase({
242
- authSessionRepository,
243
- moduleDefinitions,
244
- });
245
-
246
- const getAuthorizationRequirements =
247
- new GetAuthorizationRequirementsUseCase({
248
- moduleDefinitions,
249
- });
250
-
251
- const executeProxyRequest = new ExecuteProxyRequest({
252
- moduleRepository,
253
- credentialRepository,
254
- moduleFactory,
255
- moduleDefinitions,
193
+ moduleDefinitions:
194
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
256
195
  });
257
196
 
258
197
  const router = express();
@@ -275,22 +214,6 @@ function createIntegrationRouter() {
275
214
  getEntityOptionsById,
276
215
  refreshEntityOptions,
277
216
  processAuthorizationCallback,
278
- moduleDefinitions,
279
- startAuthorizationSession,
280
- processAuthorizationStep,
281
- getAuthorizationRequirements,
282
- moduleRepository,
283
- credentialRepository,
284
- authSessionRepository,
285
- executeProxyRequest,
286
- });
287
- setCredentialRoutes(router, authenticateUser, {
288
- listCredentialsForUser,
289
- getCredentialForUser,
290
- deleteCredentialForUser,
291
- reauthorizeCredential,
292
- getAuthorizationRequirements,
293
- moduleDefinitions,
294
217
  });
295
218
  return router;
296
219
  }
@@ -335,84 +258,20 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
335
258
  updateIntegration,
336
259
  getPossibleIntegrations,
337
260
  } = useCases;
338
-
339
- // =========================================================================
340
- // v1 API Routes (backwards compatible - legacy format)
341
- // =========================================================================
342
-
343
- // GET /api/integrations - v1 format: combined response with entities and integrations
344
261
  router.route('/api/integrations').get(
345
262
  catchAsyncError(async (req, res) => {
346
263
  const user = await authenticateUser.execute(req);
347
264
  const userId = user.getId();
348
-
349
- // v1 returns everything in one call
350
- const [integrations, options, authorized] = await Promise.all([
351
- getIntegrationsForUser.execute(userId),
352
- getPossibleIntegrations.execute(),
353
- getEntitiesForUser.execute(userId),
354
- ]);
355
-
356
- res.json({
265
+ const integrations = await getIntegrationsForUser.execute(userId);
266
+ const results = {
357
267
  entities: {
358
- options,
359
- authorized,
268
+ options: await getPossibleIntegrations.execute(),
269
+ authorized: await getEntitiesForUser.execute(userId),
360
270
  },
361
- integrations,
362
- });
363
- })
364
- );
365
-
366
- // GET /api/integrations/options - Get available integration options (v1 compatible)
367
- router.route('/api/integrations/options').get(
368
- catchAsyncError(async (req, res) => {
369
- const options = await getPossibleIntegrations.execute();
370
- res.json({ integrations: options });
371
- })
372
- );
373
-
374
- // GET /api/entities - Get user's connected entities/accounts (v1 compatible)
375
- router.route('/api/entities').get(
376
- catchAsyncError(async (req, res) => {
377
- const user = await authenticateUser.execute(req);
378
- const userId = user.getId();
379
- const entities = await getEntitiesForUser.execute(userId);
380
-
381
- res.json({ entities });
382
- })
383
- );
384
-
385
- // =========================================================================
386
- // v2 API Routes (new clean format)
387
- // =========================================================================
388
-
389
- // GET /api/v2/integrations - v2 format: only integrations
390
- router.route('/api/v2/integrations').get(
391
- catchAsyncError(async (req, res) => {
392
- const user = await authenticateUser.execute(req);
393
- const userId = user.getId();
394
- const integrations = await getIntegrationsForUser.execute(userId);
395
-
396
- res.json({ integrations });
397
- })
398
- );
399
-
400
- // GET /api/v2/integrations/options - Get available integration options
401
- router.route('/api/v2/integrations/options').get(
402
- catchAsyncError(async (req, res) => {
403
- const options = await getPossibleIntegrations.execute();
404
- res.json({ integrations: options });
405
- })
406
- );
407
-
408
- // GET /api/v2/entities - Get user's connected entities/accounts
409
- router.route('/api/v2/entities').get(
410
- catchAsyncError(async (req, res) => {
411
- const user = await authenticateUser.execute(req);
412
- const userId = user.getId();
413
- const entities = await getEntitiesForUser.execute(userId);
271
+ integrations: integrations,
272
+ };
414
273
 
415
- res.json({ entities });
274
+ res.json(results);
416
275
  })
417
276
  );
418
277
 
@@ -621,1717 +480,187 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
621
480
  }
622
481
  })
623
482
  );
483
+ }
624
484
 
625
- // =========================================================================
626
- // v2 API Routes - Integration endpoints (can evolve independently from v1)
627
- // =========================================================================
485
+ /**
486
+ * Sets up entity-related routes for the integration router
487
+ * @param {Object} router - Express router instance
488
+ * @param {import('../user/use-cases/authenticate-user').AuthenticateUser} authenticateUser - Use case for multi-mode user authentication
489
+ */
490
+ function setEntityRoutes(router, authenticateUser, useCases) {
491
+ const {
492
+ getCredentialForUser,
493
+ getModuleInstanceFromType,
494
+ getEntityOptionsByType,
495
+ testModuleAuth,
496
+ getModule,
497
+ getEntityOptionsById,
498
+ refreshEntityOptions,
499
+ processAuthorizationCallback,
500
+ } = useCases;
628
501
 
629
- // POST /api/v2/integrations - Create integration (v2)
630
- router.route('/api/v2/integrations').post(
502
+ router.route('/api/authorize').get(
631
503
  catchAsyncError(async (req, res) => {
632
504
  const user = await authenticateUser.execute(req);
633
505
  const userId = user.getId();
634
- const params = checkRequiredParams(req.body, [
635
- 'entities',
636
- 'config',
637
- ]);
638
-
639
- get(params.config, 'type');
640
-
641
- const integration = await createIntegration.execute(
642
- params.entities,
506
+ const params = checkRequiredParams(req.query, ['entityType']);
507
+ const module = await getModuleInstanceFromType.execute(
643
508
  userId,
644
- params.config
509
+ params.entityType
645
510
  );
511
+ const areRequirementsValid =
512
+ module.validateAuthorizationRequirements();
513
+ if (!areRequirementsValid) {
514
+ throw new Error(
515
+ `Error: Entity of type ${params.entityType} requires a valid url`
516
+ );
517
+ }
646
518
 
647
- res.status(201).json(integration);
519
+ res.json(module.getAuthorizationRequirements());
648
520
  })
649
521
  );
650
522
 
651
- // PATCH /api/v2/integrations/:integrationId - Update integration (v2)
652
- router.route('/api/v2/integrations/:integrationId').patch(
523
+ router.route('/api/authorize').post(
653
524
  catchAsyncError(async (req, res) => {
654
525
  const user = await authenticateUser.execute(req);
655
526
  const userId = user.getId();
656
- const params = checkRequiredParams(req.body, ['config']);
527
+ const params = checkRequiredParams(req.body, [
528
+ 'entityType',
529
+ 'data',
530
+ ]);
657
531
 
658
- const integration = await updateIntegration.execute(
659
- req.params.integrationId,
532
+ const entityDetails = await processAuthorizationCallback.execute(
660
533
  userId,
661
- params.config
534
+ params.entityType,
535
+ params.data
662
536
  );
663
- res.json(integration);
537
+
538
+ res.json(entityDetails);
664
539
  })
665
540
  );
666
541
 
667
- // DELETE /api/v2/integrations/:integrationId - Delete integration (v2)
668
- router.route('/api/v2/integrations/:integrationId').delete(
542
+ router.route('/api/entity').post(
669
543
  catchAsyncError(async (req, res) => {
670
544
  const user = await authenticateUser.execute(req);
671
- const params = checkRequiredParams(req.params, ['integrationId']);
672
- await deleteIntegrationForUser.execute(
673
- params.integrationId,
674
- user.getId()
545
+ const userId = user.getId();
546
+ const params = checkRequiredParams(req.body, [
547
+ 'entityType',
548
+ 'data',
549
+ ]);
550
+ checkRequiredParams(req.body.data, ['credential_id']);
551
+
552
+ // May want to pass along the user ID as well so credential ID's can't be fished???
553
+ const credential = await getCredentialForUser.execute(
554
+ params.data.credential_id,
555
+ userId
675
556
  );
676
- res.status(204).json({});
557
+
558
+ if (!credential) {
559
+ throw Boom.badRequest('Invalid credential ID');
560
+ }
561
+
562
+ const module = await getModuleInstanceFromType.execute(
563
+ userId,
564
+ params.entityType
565
+ );
566
+ const entityDetails = await module.getEntityDetails(
567
+ module.api,
568
+ null,
569
+ null,
570
+ userId
571
+ );
572
+
573
+ res.json(await module.findOrCreateEntity(entityDetails));
677
574
  })
678
575
  );
679
576
 
680
- // GET /api/v2/integrations/:integrationId - Get single integration (v2)
681
- router.route('/api/v2/integrations/:integrationId').get(
577
+ router.route('/api/entity/options/:credentialId').get(
682
578
  catchAsyncError(async (req, res) => {
683
579
  const user = await authenticateUser.execute(req);
684
-
685
- if (!user) {
686
- throw Boom.forbidden('User not found');
580
+ const userId = user.getId();
581
+ // TODO May want to pass along the user ID as well so credential ID's can't be fished???
582
+ // TODO **flagging this for review** -MW
583
+ const credential = await getCredentialForUser.execute(
584
+ req.params.credentialId,
585
+ userId
586
+ );
587
+ if (credential.userId.toString() !== userId) {
588
+ throw Boom.forbidden('Credential does not belong to user');
687
589
  }
688
590
 
689
- const params = checkRequiredParams(req.params, ['integrationId']);
690
- const integration = await getIntegrationInstance.execute(
691
- params.integrationId,
692
- user.getId()
591
+ const params = checkRequiredParams(req.query, ['entityType']);
592
+ const entityOptions = await getEntityOptionsByType.execute(
593
+ userId,
594
+ params.entityType
693
595
  );
694
596
 
695
- res.json({
696
- id: integration.id,
697
- entities: integration.entities,
698
- status: integration.status,
699
- config: integration.config,
700
- });
597
+ res.json(entityOptions);
701
598
  })
702
599
  );
703
600
 
704
- // GET /api/v2/integrations/:integrationId/config/options - Get config options (v2)
705
- router.route('/api/v2/integrations/:integrationId/config/options').get(
601
+ router.route('/api/entities/:entityId/test-auth').get(
706
602
  catchAsyncError(async (req, res) => {
707
603
  const user = await authenticateUser.execute(req);
708
- const params = checkRequiredParams(req.params, ['integrationId']);
709
- const integration = await getIntegrationInstance.execute(
710
- params.integrationId,
711
- user.getId()
604
+ const params = checkRequiredParams(req.params, ['entityId']);
605
+ const testAuthResponse = await testModuleAuth.execute(
606
+ params.entityId,
607
+ user // Pass User object for proper validation
712
608
  );
713
- res.json(await integration.send('GET_CONFIG_OPTIONS'));
609
+
610
+ if (!testAuthResponse) {
611
+ res.status(400);
612
+ res.json({
613
+ errors: [
614
+ {
615
+ title: 'Authentication Error',
616
+ message: `There was an error with your Entity. Please reconnect/re-authenticate, or reach out to Support for assistance.`,
617
+ timestamp: Date.now(),
618
+ },
619
+ ],
620
+ });
621
+ } else {
622
+ res.json({ status: 'ok' });
623
+ }
714
624
  })
715
625
  );
716
626
 
717
- // POST /api/v2/integrations/:integrationId/config/options/refresh (v2)
718
- router
719
- .route('/api/v2/integrations/:integrationId/config/options/refresh')
720
- .post(
721
- catchAsyncError(async (req, res) => {
722
- const user = await authenticateUser.execute(req);
723
- const params = checkRequiredParams(req.params, [
724
- 'integrationId',
725
- ]);
726
- const integration = await getIntegrationInstance.execute(
727
- params.integrationId,
728
- user.getId()
729
- );
627
+ router.route('/api/entities/:entityId').get(
628
+ catchAsyncError(async (req, res) => {
629
+ const user = await authenticateUser.execute(req);
630
+ const params = checkRequiredParams(req.params, ['entityId']);
631
+ const module = await getModule.execute(params.entityId, user); // Pass User object
730
632
 
731
- res.json(
732
- await integration.send('REFRESH_CONFIG_OPTIONS', req.body)
733
- );
734
- })
735
- );
633
+ res.json(module);
634
+ })
635
+ );
736
636
 
737
- // ALL /api/v2/integrations/:integrationId/actions - Get user actions (v2)
738
- router.route('/api/v2/integrations/:integrationId/actions').all(
637
+ router.route('/api/entities/:entityId/options').post(
739
638
  catchAsyncError(async (req, res) => {
740
639
  const user = await authenticateUser.execute(req);
741
- const params = checkRequiredParams(req.params, ['integrationId']);
742
- const integration = await getIntegrationInstance.execute(
743
- params.integrationId,
744
- user.getId()
640
+ const params = checkRequiredParams(req.params, ['entityId']);
641
+
642
+ const entityOptions = await getEntityOptionsById.execute(
643
+ params.entityId,
644
+ user // Pass User object
745
645
  );
746
- res.json(await integration.send('GET_USER_ACTIONS', req.body));
646
+
647
+ res.json(entityOptions);
747
648
  })
748
649
  );
749
650
 
750
- // ALL /api/v2/integrations/:integrationId/actions/:actionId/options (v2)
751
- router
752
- .route('/api/v2/integrations/:integrationId/actions/:actionId/options')
753
- .all(
754
- catchAsyncError(async (req, res) => {
755
- const user = await authenticateUser.execute(req);
756
- const params = checkRequiredParams(req.params, [
757
- 'integrationId',
758
- 'actionId',
759
- ]);
760
- const integration = await getIntegrationInstance.execute(
761
- params.integrationId,
762
- user.getId()
763
- );
764
-
765
- res.json(
766
- await integration.send('GET_USER_ACTION_OPTIONS', {
767
- actionId: params.actionId,
768
- data: req.body,
769
- })
770
- );
771
- })
772
- );
773
-
774
- // POST /api/v2/integrations/:integrationId/actions/:actionId/options/refresh (v2)
775
- router
776
- .route(
777
- '/api/v2/integrations/:integrationId/actions/:actionId/options/refresh'
778
- )
779
- .post(
780
- catchAsyncError(async (req, res) => {
781
- const user = await authenticateUser.execute(req);
782
- const params = checkRequiredParams(req.params, [
783
- 'integrationId',
784
- 'actionId',
785
- ]);
786
- const integration = await getIntegrationInstance.execute(
787
- params.integrationId,
788
- user.getId()
789
- );
790
-
791
- res.json(
792
- await integration.send('REFRESH_USER_ACTION_OPTIONS', {
793
- actionId: params.actionId,
794
- data: req.body,
795
- })
796
- );
797
- })
798
- );
799
-
800
- // POST /api/v2/integrations/:integrationId/actions/:actionId - Execute action (v2)
801
- router.route('/api/v2/integrations/:integrationId/actions/:actionId').post(
802
- catchAsyncError(async (req, res) => {
803
- const user = await authenticateUser.execute(req);
804
- const params = checkRequiredParams(req.params, [
805
- 'integrationId',
806
- 'actionId',
807
- ]);
808
- const integration = await getIntegrationInstance.execute(
809
- params.integrationId,
810
- user.getId()
811
- );
812
- res.json(await integration.send(params.actionId, req.body));
813
- })
814
- );
815
-
816
- // GET /api/v2/integrations/:integrationId/test-auth - Test auth (v2)
817
- router.route('/api/v2/integrations/:integrationId/test-auth').get(
818
- catchAsyncError(async (req, res) => {
819
- const user = await authenticateUser.execute(req);
820
- const params = checkRequiredParams(req.params, ['integrationId']);
821
- const instance = await getIntegrationInstance.execute(
822
- params.integrationId,
823
- user.getId()
824
- );
825
-
826
- if (!instance) {
827
- throw Boom.notFound();
828
- }
829
-
830
- const start = Date.now();
831
- await instance.testAuth();
832
- const errors = instance.record.messages?.errors?.filter(
833
- ({ timestamp }) => timestamp >= start
834
- );
835
-
836
- if (errors?.length) {
837
- res.status(400);
838
- res.json({ errors });
839
- } else {
840
- res.json({ status: 'ok' });
841
- }
842
- })
843
- );
844
- }
845
-
846
- /**
847
- * Sets up entity-related routes for the integration router
848
- * @param {Object} router - Express router instance
849
- * @param {import('../user/use-cases/authenticate-user').AuthenticateUser} authenticateUser - Use case for multi-mode user authentication
850
- */
851
- function setEntityRoutes(router, authenticateUser, useCases) {
852
- const {
853
- getCredentialForUser,
854
- getModuleInstanceFromType,
855
- getEntityOptionsByType,
856
- testModuleAuth,
857
- getModule,
858
- getEntityOptionsById,
859
- refreshEntityOptions,
860
- processAuthorizationCallback,
861
- moduleDefinitions,
862
- startAuthorizationSession,
863
- processAuthorizationStep,
864
- getAuthorizationRequirements,
865
- moduleRepository,
866
- credentialRepository,
867
- authSessionRepository,
868
- executeProxyRequest,
869
- } = useCases;
870
-
871
- // GET /api/authorize - Get authorization requirements (supports multi-step)
872
- router.route('/api/authorize').get(
873
- catchAsyncError(async (req, res) => {
874
- const user = await authenticateUser.execute(req);
875
- const userId = user.getId();
876
- const params = checkRequiredParams(req.query, ['entityType']);
877
- const step = parseInt(req.query.step || '1', 10);
878
- const sessionId = req.query.sessionId;
879
-
880
- // Validate session if step > 1
881
- if (step > 1 && !sessionId) {
882
- throw Boom.badRequest('sessionId required for step > 1');
883
- }
884
-
885
- // Check if module supports multi-step auth
886
- const requirements = await getAuthorizationRequirements.execute(
887
- params.entityType,
888
- step
889
- );
890
-
891
- // Generate session ID for multi-step flows on step 1
892
- if (requirements.isMultiStep && step === 1) {
893
- const crypto = require('crypto');
894
- requirements.sessionId = crypto.randomUUID();
895
- } else if (sessionId) {
896
- requirements.sessionId = sessionId;
897
- }
898
-
899
- // Validate requirements for backward compatibility
900
- if (!requirements.isMultiStep) {
901
- const module = await getModuleInstanceFromType.execute(
902
- userId,
903
- params.entityType
904
- );
905
- const areRequirementsValid =
906
- module.validateAuthorizationRequirements();
907
- if (!areRequirementsValid) {
908
- throw new Error(
909
- `Error: Entity of type ${params.entityType} requires a valid url`
910
- );
911
- }
912
- }
913
-
914
- res.json(requirements);
915
- })
916
- );
917
-
918
- // POST /api/authorize - Process authorization (supports multi-step)
919
- router.route('/api/authorize').post(
920
- catchAsyncError(async (req, res) => {
921
- const user = await authenticateUser.execute(req);
922
- const userId = user.getId();
923
- const params = checkRequiredParams(req.body, [
924
- 'entityType',
925
- 'data',
926
- ]);
927
- const step = parseInt(req.body.step || '1', 10);
928
- const sessionId = req.body.sessionId;
929
- const isGlobal = req.body.isGlobal || false;
930
-
931
- // Find module definition to check step count
932
- const moduleDefinition = moduleDefinitions.find(
933
- (def) => def.moduleName === params.entityType
934
- );
935
-
936
- if (!moduleDefinition) {
937
- throw Boom.badRequest(
938
- `Unknown entity type: ${params.entityType}`
939
- );
940
- }
941
-
942
- const ModuleDefinition = moduleDefinition.definition;
943
- const stepCount = ModuleDefinition.getAuthStepCount
944
- ? ModuleDefinition.getAuthStepCount()
945
- : 1;
946
-
947
- // Single-step flow - use existing ProcessAuthorizationCallback
948
- if (stepCount === 1) {
949
- const entityDetails =
950
- await processAuthorizationCallback.execute(
951
- userId,
952
- params.entityType,
953
- params.data,
954
- isGlobal
955
- );
956
-
957
- return res.json(entityDetails);
958
- }
959
-
960
- // Multi-step flow
961
- if (!sessionId) {
962
- throw Boom.badRequest(
963
- 'sessionId required for multi-step authorization'
964
- );
965
- }
966
-
967
- let session;
968
-
969
- if (step === 1) {
970
- // Create new session for step 1
971
- session = await startAuthorizationSession.execute(
972
- userId,
973
- params.entityType,
974
- stepCount
975
- );
976
-
977
- // Override with client-provided sessionId
978
- session.sessionId = sessionId;
979
- await useCases.authSessionRepository?.update(session);
980
- }
981
-
982
- // Process this step
983
- const result = await processAuthorizationStep.execute(
984
- sessionId,
985
- userId,
986
- step,
987
- params.data
988
- );
989
-
990
- if (result.completed) {
991
- // Final step - create entity using standard flow
992
- const entityDetails =
993
- await processAuthorizationCallback.execute(
994
- userId,
995
- params.entityType,
996
- result.authData,
997
- isGlobal
998
- );
999
-
1000
- return res.json(entityDetails);
1001
- }
1002
-
1003
- // Return next step requirements
1004
- res.json({
1005
- step: result.nextStep,
1006
- totalSteps: result.totalSteps,
1007
- sessionId: result.sessionId,
1008
- requirements: result.requirements,
1009
- message: result.message,
1010
- });
1011
- })
1012
- );
1013
-
1014
- router.route('/api/entities').post(
1015
- catchAsyncError(async (req, res) => {
1016
- const user = await authenticateUser.execute(req);
1017
- const userId = user.getId();
1018
- const params = checkRequiredParams(req.body, [
1019
- 'entityType',
1020
- 'data',
1021
- ]);
1022
- checkRequiredParams(req.body.data, ['credential_id']);
1023
-
1024
- // May want to pass along the user ID as well so credential ID's can't be fished???
1025
- const credential = await getCredentialForUser.execute(
1026
- params.data.credential_id,
1027
- userId
1028
- );
1029
-
1030
- if (!credential) {
1031
- throw Boom.badRequest('Invalid credential ID');
1032
- }
1033
-
1034
- const module = await getModuleInstanceFromType.execute(
1035
- userId,
1036
- params.entityType
1037
- );
1038
- const entityDetails = await module.getEntityDetails(
1039
- module.api,
1040
- null,
1041
- null,
1042
- userId
1043
- );
1044
-
1045
- res.json(await module.findOrCreateEntity(entityDetails));
1046
- })
1047
- );
1048
-
1049
- // GET /api/entities/types - List all available entity types
1050
- // NOTE: This route MUST come before /api/entities/:entityId
1051
- router.route('/api/entities/types').get(
1052
- catchAsyncError(async (req, res) => {
1053
- await authenticateUser.execute(req);
1054
-
1055
- // Map module definitions to entity type format
1056
- const types = moduleDefinitions.map((moduleDef) => {
1057
- const Definition = moduleDef.definition;
1058
-
1059
- return {
1060
- type: moduleDef.moduleName,
1061
- name:
1062
- typeof Definition.getDisplayName === 'function'
1063
- ? Definition.getDisplayName()
1064
- : moduleDef.moduleName,
1065
- description:
1066
- typeof Definition.getDescription === 'function'
1067
- ? Definition.getDescription()
1068
- : undefined,
1069
- authType:
1070
- typeof Definition.getAuthType === 'function'
1071
- ? Definition.getAuthType()
1072
- : 'oauth2',
1073
- isMultiStep:
1074
- typeof Definition.getAuthStepCount === 'function'
1075
- ? Definition.getAuthStepCount() > 1
1076
- : false,
1077
- stepCount:
1078
- typeof Definition.getAuthStepCount === 'function'
1079
- ? Definition.getAuthStepCount()
1080
- : 1,
1081
- capabilities:
1082
- typeof Definition.getCapabilities === 'function'
1083
- ? Definition.getCapabilities()
1084
- : undefined,
1085
- };
1086
- });
1087
-
1088
- // Sort by name
1089
- types.sort((a, b) => a.name.localeCompare(b.name));
1090
-
1091
- res.json({ types });
1092
- })
1093
- );
1094
-
1095
- // GET /api/entities/types/:typeName - Get details for specific entity type
1096
- // NOTE: This route MUST come before /api/entities/:entityId
1097
- router.route('/api/entities/types/:typeName').get(
1098
- catchAsyncError(async (req, res) => {
1099
- await authenticateUser.execute(req);
1100
- const { typeName } = req.params;
1101
-
1102
- // Validate type name format
1103
- if (!/^[a-z0-9-_]+$/i.test(typeName)) {
1104
- throw Boom.badRequest('Invalid type name format');
1105
- }
1106
-
1107
- // Find module definition
1108
- const moduleDef = moduleDefinitions.find(
1109
- (def) => def.moduleName === typeName
1110
- );
1111
-
1112
- if (!moduleDef) {
1113
- throw Boom.notFound(`Entity type '${typeName}' not found`);
1114
- }
1115
-
1116
- const Definition = moduleDef.definition;
1117
-
1118
- const entityType = {
1119
- type: moduleDef.moduleName,
1120
- name:
1121
- typeof Definition.getDisplayName === 'function'
1122
- ? Definition.getDisplayName()
1123
- : moduleDef.moduleName,
1124
- description:
1125
- typeof Definition.getDescription === 'function'
1126
- ? Definition.getDescription()
1127
- : undefined,
1128
- authType:
1129
- typeof Definition.getAuthType === 'function'
1130
- ? Definition.getAuthType()
1131
- : 'oauth2',
1132
- isMultiStep:
1133
- typeof Definition.getAuthStepCount === 'function'
1134
- ? Definition.getAuthStepCount() > 1
1135
- : false,
1136
- stepCount:
1137
- typeof Definition.getAuthStepCount === 'function'
1138
- ? Definition.getAuthStepCount()
1139
- : 1,
1140
- capabilities:
1141
- typeof Definition.getCapabilities === 'function'
1142
- ? Definition.getCapabilities()
1143
- : undefined,
1144
- };
1145
-
1146
- res.json(entityType);
1147
- })
1148
- );
1149
-
1150
- // GET /api/entities/types/:typeName/requirements - Get auth requirements for entity type
1151
- // NOTE: This route MUST come before /api/entities/:entityId
1152
- router.route('/api/entities/types/:typeName/requirements').get(
1153
- catchAsyncError(async (req, res) => {
1154
- await authenticateUser.execute(req);
1155
- const { typeName } = req.params;
1156
- const step = parseInt(req.query.step || '1', 10);
1157
- const sessionId = req.query.sessionId;
1158
-
1159
- // Validate step
1160
- if (step < 1) {
1161
- throw Boom.badRequest('step must be >= 1');
1162
- }
1163
-
1164
- // Find module definition
1165
- const moduleDef = moduleDefinitions.find(
1166
- (def) => def.moduleName === typeName
1167
- );
1168
-
1169
- if (!moduleDef) {
1170
- throw Boom.notFound(`Entity type '${typeName}' not found`);
1171
- }
1172
-
1173
- const Definition = moduleDef.definition;
1174
- const stepCount =
1175
- typeof Definition.getAuthStepCount === 'function'
1176
- ? Definition.getAuthStepCount()
1177
- : 1;
1178
-
1179
- // Validate step is within range
1180
- if (step > stepCount) {
1181
- throw Boom.badRequest(
1182
- `step ${step} exceeds total steps (${stepCount})`
1183
- );
1184
- }
1185
-
1186
- // Validate sessionId for step > 1
1187
- if (step > 1 && !sessionId) {
1188
- throw Boom.badRequest('sessionId required for step > 1');
1189
- }
1190
-
1191
- // For multi-step, validate sessionId format
1192
- if (step > 1 && sessionId) {
1193
- // Basic validation - sessionId should be a non-empty string
1194
- if (typeof sessionId !== 'string' || sessionId.trim() === '') {
1195
- throw Boom.badRequest('Invalid sessionId format');
1196
- }
1197
- }
1198
-
1199
- // Get requirements from module definition
1200
- const requirements = await getAuthorizationRequirements.execute(
1201
- typeName,
1202
- step
1203
- );
1204
-
1205
- // Add sessionId for multi-step flows
1206
- if (stepCount > 1) {
1207
- if (step === 1) {
1208
- // Generate new sessionId for step 1
1209
- const crypto = require('crypto');
1210
- requirements.sessionId = crypto.randomUUID();
1211
- } else {
1212
- // Use provided sessionId for subsequent steps
1213
- requirements.sessionId = sessionId;
1214
- }
1215
- }
1216
-
1217
- res.json(requirements);
1218
- })
1219
- );
1220
-
1221
- // GET /api/entities/options/:credentialId - Get entity options for credential
1222
- // NOTE: This route MUST come before /api/entities/:entityId
1223
- router.route('/api/entities/options/:credentialId').get(
1224
- catchAsyncError(async (req, res) => {
1225
- const user = await authenticateUser.execute(req);
1226
- const userId = user.getId();
1227
- // TODO May want to pass along the user ID as well so credential ID's can't be fished???
1228
- // TODO **flagging this for review** -MW
1229
- const credential = await getCredentialForUser.execute(
1230
- req.params.credentialId,
1231
- userId
1232
- );
1233
- if (credential.userId.toString() !== userId) {
1234
- throw Boom.forbidden('Credential does not belong to user');
1235
- }
1236
-
1237
- const params = checkRequiredParams(req.query, ['entityType']);
1238
- const entityOptions = await getEntityOptionsByType.execute(
1239
- userId,
1240
- params.entityType
1241
- );
1242
-
1243
- res.json(entityOptions);
1244
- })
1245
- );
1246
-
1247
- // GET /api/entities/:entityId/test-auth - Test authentication for entity
1248
- router.route('/api/entities/:entityId/test-auth').get(
1249
- catchAsyncError(async (req, res) => {
1250
- const user = await authenticateUser.execute(req);
1251
- const params = checkRequiredParams(req.params, ['entityId']);
1252
- const testAuthResponse = await testModuleAuth.execute(
1253
- params.entityId,
1254
- user // Pass User object for proper validation
1255
- );
1256
-
1257
- if (!testAuthResponse) {
1258
- res.status(400);
1259
- res.json({
1260
- errors: [
1261
- {
1262
- title: 'Authentication Error',
1263
- message: `There was an error with your Entity. Please reconnect/re-authenticate, or reach out to Support for assistance.`,
1264
- timestamp: Date.now(),
1265
- },
1266
- ],
1267
- });
1268
- } else {
1269
- res.json({ status: 'ok' });
1270
- }
1271
- })
1272
- );
1273
-
1274
- // GET /api/entities/:entityId - Get entity by ID
1275
- router.route('/api/entities/:entityId').get(
1276
- catchAsyncError(async (req, res) => {
1277
- const user = await authenticateUser.execute(req);
1278
- const params = checkRequiredParams(req.params, ['entityId']);
1279
- const module = await getModule.execute(params.entityId, user); // Pass User object
1280
-
1281
- res.json(module);
1282
- })
1283
- );
1284
-
1285
- // POST /api/entities/:entityId/options - Get entity options by ID
1286
- router.route('/api/entities/:entityId/options').post(
1287
- catchAsyncError(async (req, res) => {
1288
- const user = await authenticateUser.execute(req);
1289
- const params = checkRequiredParams(req.params, ['entityId']);
1290
-
1291
- const entityOptions = await getEntityOptionsById.execute(
1292
- params.entityId,
1293
- user // Pass User object
1294
- );
1295
-
1296
- res.json(entityOptions);
1297
- })
1298
- );
1299
-
1300
- // POST /api/entities/:entityId/options/refresh - Refresh entity options
1301
- router.route('/api/entities/:entityId/options/refresh').post(
1302
- catchAsyncError(async (req, res) => {
1303
- const user = await authenticateUser.execute(req);
1304
- const params = checkRequiredParams(req.params, ['entityId']);
1305
- const updatedOptions = await refreshEntityOptions.execute(
1306
- params.entityId,
1307
- user, // Pass User object
1308
- req.body
1309
- );
1310
-
1311
- res.json(updatedOptions);
1312
- })
1313
- );
1314
-
1315
- // POST /api/entities/:id/reauthorize - Reauthorize specific entity
1316
- router.route('/api/entities/:id/reauthorize').post(
1317
- catchAsyncError(async (req, res) => {
1318
- const user = await authenticateUser.execute(req);
1319
- const userId = user.getId();
1320
- const entityId = req.params.id;
1321
-
1322
- // Validate data parameter
1323
- const params = checkRequiredParams(req.body, ['data']);
1324
- if (typeof params.data !== 'object' || Array.isArray(params.data)) {
1325
- throw Boom.badRequest('data must be an object');
1326
- }
1327
-
1328
- const step = parseInt(req.body.step || '1', 10);
1329
- const sessionId = req.body.sessionId;
1330
-
1331
- // Validate step
1332
- if (step < 1) {
1333
- throw Boom.badRequest('step must be >= 1');
1334
- }
1335
-
1336
- // Get entity
1337
- const entity = await moduleRepository?.findById(entityId);
1338
- if (!entity) {
1339
- throw Boom.notFound('Entity not found');
1340
- }
1341
-
1342
- // Check ownership
1343
- if (entity.userId.toString() !== userId) {
1344
- throw Boom.forbidden(
1345
- 'User is not authorized to access this entity'
1346
- );
1347
- }
1348
-
1349
- // Get credential
1350
- const credential = await credentialRepository?.findById(
1351
- entity.credentialId
1352
- );
1353
-
1354
- // Find module definition
1355
- const moduleDef = moduleDefinitions.find(
1356
- (def) => def.moduleName === entity.type
1357
- );
1358
-
1359
- if (!moduleDef) {
1360
- throw Boom.badRequest(`Unknown entity type: ${entity.type}`);
1361
- }
1362
-
1363
- const Definition = moduleDef.definition;
1364
- const stepCount =
1365
- typeof Definition.getAuthStepCount === 'function'
1366
- ? Definition.getAuthStepCount()
1367
- : 1;
1368
-
1369
- // Validate step is within range
1370
- if (step > stepCount) {
1371
- throw Boom.badRequest(
1372
- `step ${step} exceeds total steps (${stepCount})`
1373
- );
1374
- }
1375
-
1376
- // Single-step reauthorization
1377
- if (stepCount === 1) {
1378
- try {
1379
- // Process reauthorization using existing flow
1380
- const result = await processAuthorizationCallback.execute(
1381
- userId,
1382
- entity.type,
1383
- params.data
1384
- );
1385
-
1386
- // Update entity status
1387
- const updatedEntity = await moduleRepository?.update({
1388
- id: entityId,
1389
- authIsValid: true,
1390
- });
1391
-
1392
- res.json({
1393
- success: true,
1394
- credential_id: result.credential_id,
1395
- entity_id: entityId,
1396
- authIsValid: true,
1397
- });
1398
- } catch (error) {
1399
- throw Boom.badRequest(
1400
- error.message || 'Reauthorization failed'
1401
- );
1402
- }
1403
- } else {
1404
- // Multi-step reauthorization
1405
- if (step > 1 && !sessionId) {
1406
- throw Boom.badRequest('sessionId required for step > 1');
1407
- }
1408
-
1409
- let session;
1410
-
1411
- if (step === 1) {
1412
- // Create new session for step 1
1413
- session = await startAuthorizationSession.execute(
1414
- userId,
1415
- entity.type,
1416
- stepCount
1417
- );
1418
-
1419
- // Override with client-provided sessionId
1420
- if (sessionId) {
1421
- session.sessionId = sessionId;
1422
- await authSessionRepository?.update(session);
1423
- }
1424
- }
1425
-
1426
- // Process this step
1427
- const result = await processAuthorizationStep.execute(
1428
- sessionId || session?.sessionId,
1429
- userId,
1430
- step,
1431
- params.data
1432
- );
1433
-
1434
- if (result.completed) {
1435
- // Final step - update credential and entity
1436
- try {
1437
- const authResult =
1438
- await processAuthorizationCallback.execute(
1439
- userId,
1440
- entity.type,
1441
- result.authData
1442
- );
1443
-
1444
- // Update entity status
1445
- await moduleRepository?.update({
1446
- id: entityId,
1447
- authIsValid: true,
1448
- });
1449
-
1450
- return res.json({
1451
- success: true,
1452
- credential_id: authResult.credential_id,
1453
- entity_id: entityId,
1454
- authIsValid: true,
1455
- });
1456
- } catch (error) {
1457
- throw Boom.badRequest(
1458
- error.message || 'Reauthorization failed'
1459
- );
1460
- }
1461
- }
1462
-
1463
- // Return next step requirements
1464
- res.json({
1465
- step: result.nextStep,
1466
- totalSteps: result.totalSteps,
1467
- sessionId: result.sessionId,
1468
- requirements: result.requirements,
1469
- message: result.message,
1470
- });
1471
- }
1472
- })
1473
- );
1474
-
1475
- // POST /api/entities/:id/proxy - Proxy request through entity's API connection
1476
- router.route('/api/entities/:id/proxy').post(
1477
- catchAsyncError(async (req, res) => {
1478
- const user = await authenticateUser.execute(req);
1479
- const userId = user.getId();
1480
- const entityId = req.params.id;
1481
-
1482
- try {
1483
- // Execute proxy request via entity
1484
- const proxyResponse =
1485
- await executeProxyRequest.executeViaEntity(
1486
- entityId,
1487
- userId,
1488
- req.body
1489
- );
1490
-
1491
- // Return success response
1492
- res.status(200).json(proxyResponse);
1493
- } catch (error) {
1494
- // Handle Boom errors
1495
- if (Boom.isBoom(error)) {
1496
- const statusCode = error.output.statusCode;
1497
- const errorData = error.data || {};
1498
-
1499
- // Build error response matching proxyErrorResponse schema
1500
- const errorResponse = {
1501
- success: false,
1502
- status: statusCode,
1503
- error: {
1504
- code:
1505
- errorData.code ||
1506
- _getErrorCodeFromStatus(statusCode),
1507
- message:
1508
- error.output.payload.message || error.message,
1509
- details: errorData.details || null,
1510
- },
1511
- };
1512
-
1513
- // Add upstreamStatus if present
1514
- if (errorData.upstreamStatus) {
1515
- errorResponse.error.upstreamStatus =
1516
- errorData.upstreamStatus;
1517
- }
1518
-
1519
- return res.status(statusCode).json(errorResponse);
1520
- }
1521
-
1522
- // Unknown error - return 500
1523
- res.status(500).json({
1524
- success: false,
1525
- status: 500,
1526
- error: {
1527
- code: 'INTERNAL_ERROR',
1528
- message: 'An unexpected error occurred',
1529
- details: null,
1530
- },
1531
- });
1532
- }
1533
- })
1534
- );
1535
-
1536
- // POST /api/credentials/:id/proxy - Proxy request through credential's API connection
1537
- router.route('/api/credentials/:id/proxy').post(
1538
- catchAsyncError(async (req, res) => {
1539
- const user = await authenticateUser.execute(req);
1540
- const userId = user.getId();
1541
- const credentialId = req.params.id;
1542
-
1543
- try {
1544
- // Execute proxy request via credential
1545
- const proxyResponse =
1546
- await executeProxyRequest.executeViaCredential(
1547
- credentialId,
1548
- userId,
1549
- req.body
1550
- );
1551
-
1552
- // Return success response
1553
- res.status(200).json(proxyResponse);
1554
- } catch (error) {
1555
- // Handle Boom errors
1556
- if (Boom.isBoom(error)) {
1557
- const statusCode = error.output.statusCode;
1558
- const errorData = error.data || {};
1559
-
1560
- // Build error response matching proxyErrorResponse schema
1561
- const errorResponse = {
1562
- success: false,
1563
- status: statusCode,
1564
- error: {
1565
- code:
1566
- errorData.code ||
1567
- _getErrorCodeFromStatus(statusCode),
1568
- message:
1569
- error.output.payload.message || error.message,
1570
- details: errorData.details || null,
1571
- },
1572
- };
1573
-
1574
- // Add upstreamStatus if present
1575
- if (errorData.upstreamStatus) {
1576
- errorResponse.error.upstreamStatus =
1577
- errorData.upstreamStatus;
1578
- }
1579
-
1580
- return res.status(statusCode).json(errorResponse);
1581
- }
1582
-
1583
- // Unknown error - return 500
1584
- res.status(500).json({
1585
- success: false,
1586
- status: 500,
1587
- error: {
1588
- code: 'INTERNAL_ERROR',
1589
- message: 'An unexpected error occurred',
1590
- details: null,
1591
- },
1592
- });
1593
- }
1594
- })
1595
- );
1596
-
1597
- // =========================================================================
1598
- // v1 Legacy Aliases (backwards compatibility for singular /api/entity)
1599
- // =========================================================================
1600
-
1601
- // POST /api/entity - v1 legacy alias for POST /api/entities
1602
- router.route('/api/entity').post(
1603
- catchAsyncError(async (req, res) => {
1604
- const user = await authenticateUser.execute(req);
1605
- const userId = user.getId();
1606
- const params = checkRequiredParams(req.body, [
1607
- 'entityType',
1608
- 'data',
1609
- ]);
1610
- checkRequiredParams(req.body.data, ['credential_id']);
1611
-
1612
- const credential = await getCredentialForUser.execute(
1613
- params.data.credential_id,
1614
- userId
1615
- );
1616
-
1617
- if (!credential) {
1618
- throw Boom.badRequest('Invalid credential ID');
1619
- }
1620
-
1621
- const module = await getModuleInstanceFromType.execute(
1622
- userId,
1623
- params.entityType
1624
- );
1625
- const entityDetails = await module.getEntityDetails(
1626
- module.api,
1627
- null,
1628
- null,
1629
- userId
1630
- );
1631
-
1632
- res.json(await module.findOrCreateEntity(entityDetails));
1633
- })
1634
- );
1635
-
1636
- // GET /api/entity/options/:credentialId - v1 legacy alias
1637
- router.route('/api/entity/options/:credentialId').get(
1638
- catchAsyncError(async (req, res) => {
1639
- const user = await authenticateUser.execute(req);
1640
- const userId = user.getId();
1641
- const credential = await getCredentialForUser.execute(
1642
- req.params.credentialId,
1643
- userId
1644
- );
1645
- if (credential.userId.toString() !== userId) {
1646
- throw Boom.forbidden('Credential does not belong to user');
1647
- }
1648
-
1649
- const params = checkRequiredParams(req.query, ['entityType']);
1650
- const entityOptions = await getEntityOptionsByType.execute(
1651
- userId,
1652
- params.entityType
1653
- );
1654
-
1655
- res.json(entityOptions);
1656
- })
1657
- );
1658
-
1659
- // =========================================================================
1660
- // v2 API Routes - Entity endpoints (can evolve independently from v1)
1661
- // =========================================================================
1662
-
1663
- // GET /api/v2/authorize - Get authorization requirements (v2)
1664
- router.route('/api/v2/authorize').get(
1665
- catchAsyncError(async (req, res) => {
1666
- const user = await authenticateUser.execute(req);
1667
- const userId = user.getId();
1668
- const params = checkRequiredParams(req.query, ['entityType']);
1669
- const step = parseInt(req.query.step || '1', 10);
1670
- const sessionId = req.query.sessionId;
1671
-
1672
- if (step > 1 && !sessionId) {
1673
- throw Boom.badRequest('sessionId required for step > 1');
1674
- }
1675
-
1676
- const requirements = await getAuthorizationRequirements.execute(
1677
- params.entityType,
1678
- step
1679
- );
1680
-
1681
- if (requirements.isMultiStep && step === 1 && !sessionId) {
1682
- const session = await startAuthorizationSession.execute(
1683
- userId,
1684
- params.entityType,
1685
- {
1686
- step: 1,
1687
- totalSteps: requirements.totalSteps,
1688
- }
1689
- );
1690
- requirements.sessionId = session.id;
1691
- } else if (sessionId) {
1692
- requirements.sessionId = sessionId;
1693
- }
1694
-
1695
- res.json(requirements);
1696
- })
1697
- );
1698
-
1699
- // POST /api/v2/authorize - Process authorization callback (v2)
1700
- router.route('/api/v2/authorize').post(
1701
- catchAsyncError(async (req, res) => {
1702
- const user = await authenticateUser.execute(req);
1703
- const userId = user.getId();
1704
- const params = checkRequiredParams(req.body, [
1705
- 'entityType',
1706
- 'data',
1707
- ]);
1708
- const step = parseInt(req.body.step || '1', 10);
1709
- const sessionId = req.body.sessionId;
1710
-
1711
- if (step > 1 && !sessionId) {
1712
- throw Boom.badRequest('sessionId required for step > 1');
1713
- }
1714
-
1715
- const result = await processAuthorizationStep.execute(
1716
- userId,
1717
- params.entityType,
1718
- params.data,
1719
- step,
1720
- sessionId
1721
- );
1722
-
1723
- if (result.isComplete) {
1724
- res.json({
1725
- status: 'complete',
1726
- entity: result.entity,
1727
- credential: result.credential,
1728
- });
1729
- } else {
1730
- res.json({
1731
- status: 'pending',
1732
- step: result.nextStep,
1733
- totalSteps: result.totalSteps,
1734
- sessionId: result.sessionId,
1735
- requirements: result.requirements,
1736
- message: result.message,
1737
- });
1738
- }
1739
- })
1740
- );
1741
-
1742
- // POST /api/v2/entities - Create entity (v2)
1743
- router.route('/api/v2/entities').post(
1744
- catchAsyncError(async (req, res) => {
1745
- const user = await authenticateUser.execute(req);
1746
- const userId = user.getId();
1747
- const params = checkRequiredParams(req.body, [
1748
- 'entityType',
1749
- 'data',
1750
- ]);
1751
- checkRequiredParams(req.body.data, ['credential_id']);
1752
-
1753
- const credential = await getCredentialForUser.execute(
1754
- params.data.credential_id,
1755
- userId
1756
- );
1757
-
1758
- if (!credential) {
1759
- throw Boom.badRequest('Invalid credential ID');
1760
- }
1761
-
1762
- const module = await getModuleInstanceFromType.execute(
1763
- userId,
1764
- params.entityType
1765
- );
1766
- const entityDetails = await module.getEntityDetails(
1767
- module.api,
1768
- null,
1769
- null,
1770
- userId
1771
- );
1772
-
1773
- res.json(await module.findOrCreateEntity(entityDetails));
1774
- })
1775
- );
1776
-
1777
- // GET /api/v2/entities/types - List available entity types (v2)
1778
- router.route('/api/v2/entities/types').get(
1779
- catchAsyncError(async (req, res) => {
1780
- await authenticateUser.execute(req);
1781
-
1782
- const types = moduleDefinitions.map((moduleDef) => {
1783
- const Definition = moduleDef.definition;
1784
- return {
1785
- type: moduleDef.moduleName,
1786
- name:
1787
- typeof Definition.getDisplayName === 'function'
1788
- ? Definition.getDisplayName()
1789
- : moduleDef.moduleName,
1790
- description:
1791
- typeof Definition.getDescription === 'function'
1792
- ? Definition.getDescription()
1793
- : undefined,
1794
- authType:
1795
- typeof Definition.getAuthType === 'function'
1796
- ? Definition.getAuthType()
1797
- : 'oauth2',
1798
- isMultiStep:
1799
- typeof Definition.getAuthStepCount === 'function'
1800
- ? Definition.getAuthStepCount() > 1
1801
- : false,
1802
- stepCount:
1803
- typeof Definition.getAuthStepCount === 'function'
1804
- ? Definition.getAuthStepCount()
1805
- : 1,
1806
- capabilities:
1807
- typeof Definition.getCapabilities === 'function'
1808
- ? Definition.getCapabilities()
1809
- : undefined,
1810
- };
1811
- });
1812
-
1813
- res.json({ types });
1814
- })
1815
- );
1816
-
1817
- // GET /api/v2/entities/types/:typeName - Get entity type details (v2)
1818
- router.route('/api/v2/entities/types/:typeName').get(
1819
- catchAsyncError(async (req, res) => {
1820
- await authenticateUser.execute(req);
1821
- const { typeName } = req.params;
1822
-
1823
- const moduleDef = moduleDefinitions.find(
1824
- (def) => def.moduleName === typeName
1825
- );
1826
-
1827
- if (!moduleDef) {
1828
- throw Boom.notFound(`Entity type '${typeName}' not found`);
1829
- }
1830
-
1831
- const Definition = moduleDef.definition;
1832
- res.json({
1833
- type: moduleDef.moduleName,
1834
- name:
1835
- typeof Definition.getDisplayName === 'function'
1836
- ? Definition.getDisplayName()
1837
- : moduleDef.moduleName,
1838
- description:
1839
- typeof Definition.getDescription === 'function'
1840
- ? Definition.getDescription()
1841
- : undefined,
1842
- authType:
1843
- typeof Definition.getAuthType === 'function'
1844
- ? Definition.getAuthType()
1845
- : 'oauth2',
1846
- isMultiStep:
1847
- typeof Definition.getAuthStepCount === 'function'
1848
- ? Definition.getAuthStepCount() > 1
1849
- : false,
1850
- stepCount:
1851
- typeof Definition.getAuthStepCount === 'function'
1852
- ? Definition.getAuthStepCount()
1853
- : 1,
1854
- capabilities:
1855
- typeof Definition.getCapabilities === 'function'
1856
- ? Definition.getCapabilities()
1857
- : undefined,
1858
- });
1859
- })
1860
- );
1861
-
1862
- // GET /api/v2/entities/types/:typeName/requirements - Get auth requirements (v2)
1863
- router.route('/api/v2/entities/types/:typeName/requirements').get(
1864
- catchAsyncError(async (req, res) => {
1865
- const user = await authenticateUser.execute(req);
1866
- const userId = user.getId();
1867
- const { typeName } = req.params;
1868
- const step = parseInt(req.query.step || '1', 10);
1869
- const sessionId = req.query.sessionId;
1870
-
1871
- if (step > 1 && !sessionId) {
1872
- throw Boom.badRequest('sessionId required for step > 1');
1873
- }
1874
-
1875
- const requirements = await getAuthorizationRequirements.execute(
1876
- typeName,
1877
- step
1878
- );
1879
-
1880
- if (requirements.isMultiStep && step === 1 && !sessionId) {
1881
- const session = await startAuthorizationSession.execute(
1882
- userId,
1883
- typeName,
1884
- {
1885
- step: 1,
1886
- totalSteps: requirements.totalSteps,
1887
- }
1888
- );
1889
- requirements.sessionId = session.id;
1890
- } else if (sessionId) {
1891
- requirements.sessionId = sessionId;
1892
- }
1893
-
1894
- res.json(requirements);
1895
- })
1896
- );
1897
-
1898
- // GET /api/v2/entities/options/:credentialId - Get entity options (v2)
1899
- router.route('/api/v2/entities/options/:credentialId').get(
1900
- catchAsyncError(async (req, res) => {
1901
- const user = await authenticateUser.execute(req);
1902
- const userId = user.getId();
1903
- const credential = await getCredentialForUser.execute(
1904
- req.params.credentialId,
1905
- userId
1906
- );
1907
- if (credential.userId.toString() !== userId) {
1908
- throw Boom.forbidden('Credential does not belong to user');
1909
- }
1910
-
1911
- const params = checkRequiredParams(req.query, ['entityType']);
1912
- const entityOptions = await getEntityOptionsByType.execute(
1913
- userId,
1914
- params.entityType
1915
- );
1916
-
1917
- res.json(entityOptions);
1918
- })
1919
- );
1920
-
1921
- // GET /api/v2/entities/:entityId/test-auth - Test auth (v2)
1922
- router.route('/api/v2/entities/:entityId/test-auth').get(
1923
- catchAsyncError(async (req, res) => {
1924
- const user = await authenticateUser.execute(req);
1925
- const params = checkRequiredParams(req.params, ['entityId']);
1926
- const testAuthResponse = await testModuleAuth.execute(
1927
- params.entityId,
1928
- user
1929
- );
1930
-
1931
- if (!testAuthResponse) {
1932
- res.status(400);
1933
- res.json({
1934
- errors: [
1935
- {
1936
- title: 'Authentication Error',
1937
- message:
1938
- 'There was an error with your Entity. Please reconnect/re-authenticate, or reach out to Support for assistance.',
1939
- timestamp: Date.now(),
1940
- },
1941
- ],
1942
- });
1943
- } else {
1944
- res.json({ status: 'ok' });
1945
- }
1946
- })
1947
- );
1948
-
1949
- // GET /api/v2/entities/:entityId - Get entity by ID (v2)
1950
- router.route('/api/v2/entities/:entityId').get(
1951
- catchAsyncError(async (req, res) => {
1952
- const user = await authenticateUser.execute(req);
1953
- const params = checkRequiredParams(req.params, ['entityId']);
1954
- const module = await getModule.execute(params.entityId, user);
1955
-
1956
- res.json(module);
1957
- })
1958
- );
1959
-
1960
- // POST /api/v2/entities/:entityId/options - Get entity options (v2)
1961
- router.route('/api/v2/entities/:entityId/options').post(
1962
- catchAsyncError(async (req, res) => {
1963
- const user = await authenticateUser.execute(req);
1964
- const params = checkRequiredParams(req.params, ['entityId']);
1965
-
1966
- const entityOptions = await getEntityOptionsById.execute(
1967
- params.entityId,
1968
- user
1969
- );
1970
-
1971
- res.json(entityOptions);
1972
- })
1973
- );
1974
-
1975
- // POST /api/v2/entities/:entityId/options/refresh - Refresh entity options (v2)
1976
- router.route('/api/v2/entities/:entityId/options/refresh').post(
651
+ router.route('/api/entities/:entityId/options/refresh').post(
1977
652
  catchAsyncError(async (req, res) => {
1978
653
  const user = await authenticateUser.execute(req);
1979
654
  const params = checkRequiredParams(req.params, ['entityId']);
1980
655
  const updatedOptions = await refreshEntityOptions.execute(
1981
656
  params.entityId,
1982
- user,
657
+ user, // Pass User object
1983
658
  req.body
1984
659
  );
1985
660
 
1986
661
  res.json(updatedOptions);
1987
662
  })
1988
663
  );
1989
-
1990
- // POST /api/v2/entities/:id/reauthorize - Reauthorize entity (v2)
1991
- router.route('/api/v2/entities/:id/reauthorize').post(
1992
- catchAsyncError(async (req, res) => {
1993
- const user = await authenticateUser.execute(req);
1994
- const userId = user.getId();
1995
- const entityId = req.params.id;
1996
-
1997
- const params = checkRequiredParams(req.body, ['data']);
1998
- if (typeof params.data !== 'object' || Array.isArray(params.data)) {
1999
- throw Boom.badRequest('data must be an object');
2000
- }
2001
-
2002
- const step = parseInt(req.body.step || '1', 10);
2003
- const sessionId = req.body.sessionId;
2004
-
2005
- const entity = await moduleRepository.findEntityById(entityId);
2006
- if (!entity) {
2007
- throw Boom.notFound('Entity not found');
2008
- }
2009
-
2010
- const credential = await credentialRepository.findCredentialById(
2011
- entity.credentialId
2012
- );
2013
- if (!credential || credential.userId.toString() !== userId) {
2014
- throw Boom.forbidden('Access denied');
2015
- }
2016
-
2017
- const result = await processAuthorizationStep.execute(
2018
- userId,
2019
- entity.type,
2020
- params.data,
2021
- step,
2022
- sessionId
2023
- );
2024
-
2025
- if (result.isComplete) {
2026
- if (result.credential) {
2027
- try {
2028
- await credentialRepository.updateCredential(
2029
- credential.id,
2030
- {
2031
- data: result.credential.data,
2032
- authIsValid: true,
2033
- }
2034
- );
2035
- await moduleRepository.updateEntity(entityId, {
2036
- authIsValid: true,
2037
- });
2038
- } catch (error) {
2039
- throw Boom.badRequest(
2040
- error.message || 'Reauthorization failed'
2041
- );
2042
- }
2043
- }
2044
-
2045
- res.json({
2046
- step: result.nextStep,
2047
- totalSteps: result.totalSteps,
2048
- sessionId: result.sessionId,
2049
- requirements: result.requirements,
2050
- message: result.message,
2051
- });
2052
- }
2053
- })
2054
- );
2055
-
2056
- // POST /api/v2/entities/:id/proxy - Proxy request (v2)
2057
- router.route('/api/v2/entities/:id/proxy').post(
2058
- catchAsyncError(async (req, res) => {
2059
- const user = await authenticateUser.execute(req);
2060
- const userId = user.getId();
2061
- const entityId = req.params.id;
2062
-
2063
- try {
2064
- const proxyResponse =
2065
- await executeProxyRequest.executeViaEntity(
2066
- entityId,
2067
- userId,
2068
- req.body
2069
- );
2070
-
2071
- res.status(200).json(proxyResponse);
2072
- } catch (error) {
2073
- if (Boom.isBoom(error)) {
2074
- const statusCode = error.output.statusCode;
2075
- const errorData = error.data || {};
2076
-
2077
- const errorResponse = {
2078
- success: false,
2079
- status: statusCode,
2080
- error: {
2081
- code:
2082
- errorData.code ||
2083
- _getErrorCodeFromStatus(statusCode),
2084
- message:
2085
- error.output.payload.message || error.message,
2086
- details: errorData.details || null,
2087
- },
2088
- };
2089
-
2090
- if (errorData.upstreamStatus) {
2091
- errorResponse.error.upstreamStatus =
2092
- errorData.upstreamStatus;
2093
- }
2094
-
2095
- return res.status(statusCode).json(errorResponse);
2096
- }
2097
-
2098
- res.status(500).json({
2099
- success: false,
2100
- status: 500,
2101
- error: {
2102
- code: 'INTERNAL_ERROR',
2103
- message: 'An unexpected error occurred',
2104
- details: null,
2105
- },
2106
- });
2107
- }
2108
- })
2109
- );
2110
- }
2111
-
2112
- /**
2113
- * Sets up credential-related routes for the integration router
2114
- * @param {Object} router - Express router instance
2115
- * @param {import('../user/use-cases/authenticate-user').AuthenticateUser} authenticateUser - Authentication use case
2116
- * @param {Object} useCases - Credential use cases
2117
- */
2118
- function setCredentialRoutes(router, authenticateUser, useCases) {
2119
- const {
2120
- listCredentialsForUser,
2121
- getCredentialForUser,
2122
- deleteCredentialForUser,
2123
- reauthorizeCredential,
2124
- getAuthorizationRequirements,
2125
- moduleDefinitions,
2126
- } = useCases;
2127
-
2128
- /**
2129
- * Sanitize credential object by removing sensitive data
2130
- * @param {Object} credential - Credential object
2131
- * @returns {Object} Sanitized credential
2132
- */
2133
- function sanitizeCredential(credential) {
2134
- const { data, ...safe } = credential;
2135
- return safe;
2136
- }
2137
-
2138
- // GET /api/credentials - List user's credentials
2139
- router.route('/api/credentials').get(
2140
- catchAsyncError(async (req, res) => {
2141
- const user = await authenticateUser.execute(req);
2142
- const userId = user.getId();
2143
-
2144
- const credentials = await listCredentialsForUser.execute(userId);
2145
- const sanitized = credentials.map(sanitizeCredential);
2146
-
2147
- res.json({ credentials: sanitized });
2148
- })
2149
- );
2150
-
2151
- // GET /api/credentials/:id - Get single credential
2152
- router.route('/api/credentials/:id').get(
2153
- catchAsyncError(async (req, res) => {
2154
- const user = await authenticateUser.execute(req);
2155
- const userId = user.getId();
2156
-
2157
- const credential = await getCredentialForUser.execute(
2158
- req.params.id,
2159
- userId
2160
- );
2161
-
2162
- res.json(sanitizeCredential(credential));
2163
- })
2164
- );
2165
-
2166
- // DELETE /api/credentials/:id - Delete credential
2167
- router.route('/api/credentials/:id').delete(
2168
- catchAsyncError(async (req, res) => {
2169
- const user = await authenticateUser.execute(req);
2170
- const userId = user.getId();
2171
-
2172
- await deleteCredentialForUser.execute(req.params.id, userId);
2173
-
2174
- res.json({ success: true });
2175
- })
2176
- );
2177
-
2178
- // GET /api/credentials/:id/reauthorize - Get reauth requirements
2179
- router.route('/api/credentials/:id/reauthorize').get(
2180
- catchAsyncError(async (req, res) => {
2181
- const user = await authenticateUser.execute(req);
2182
- const userId = user.getId();
2183
-
2184
- // Verify ownership
2185
- const credential = await getCredentialForUser.execute(
2186
- req.params.id,
2187
- userId
2188
- );
2189
-
2190
- // Get authorization requirements for this credential's type
2191
- const step = parseInt(req.query.step || '1', 10);
2192
- const requirements = await getAuthorizationRequirements.execute(
2193
- credential.type,
2194
- step
2195
- );
2196
-
2197
- res.json(requirements);
2198
- })
2199
- );
2200
-
2201
- // POST /api/credentials/:id/reauthorize - Submit reauth data
2202
- router.route('/api/credentials/:id/reauthorize').post(
2203
- catchAsyncError(async (req, res) => {
2204
- const user = await authenticateUser.execute(req);
2205
- const userId = user.getId();
2206
-
2207
- const params = checkRequiredParams(req.body, ['data']);
2208
- const step = parseInt(req.body.step || '1', 10);
2209
- const sessionId = req.body.sessionId || null;
2210
-
2211
- const result = await reauthorizeCredential.execute(
2212
- req.params.id,
2213
- userId,
2214
- params.data,
2215
- step,
2216
- sessionId
2217
- );
2218
-
2219
- res.json(result);
2220
- })
2221
- );
2222
-
2223
- // ========================================
2224
- // V2 CREDENTIAL ROUTES
2225
- // ========================================
2226
- // These v2 routes are separate from v1 so v2 can evolve independently
2227
- // while v1 remains frozen for backwards compatibility.
2228
-
2229
- // GET /api/v2/credentials - v2: List user's credentials
2230
- router.route('/api/v2/credentials').get(
2231
- catchAsyncError(async (req, res) => {
2232
- const user = await authenticateUser.execute(req);
2233
- const userId = user.getId();
2234
-
2235
- const credentials = await listCredentialsForUser.execute(userId);
2236
- const sanitized = credentials.map(sanitizeCredential);
2237
-
2238
- res.json({ credentials: sanitized });
2239
- })
2240
- );
2241
-
2242
- // GET /api/v2/credentials/:id - v2: Get single credential
2243
- router.route('/api/v2/credentials/:id').get(
2244
- catchAsyncError(async (req, res) => {
2245
- const user = await authenticateUser.execute(req);
2246
- const userId = user.getId();
2247
-
2248
- const credential = await getCredentialForUser.execute(
2249
- req.params.id,
2250
- userId
2251
- );
2252
-
2253
- res.json(sanitizeCredential(credential));
2254
- })
2255
- );
2256
-
2257
- // DELETE /api/v2/credentials/:id - v2: Delete credential
2258
- router.route('/api/v2/credentials/:id').delete(
2259
- catchAsyncError(async (req, res) => {
2260
- const user = await authenticateUser.execute(req);
2261
- const userId = user.getId();
2262
-
2263
- await deleteCredentialForUser.execute(req.params.id, userId);
2264
-
2265
- res.json({ success: true });
2266
- })
2267
- );
2268
-
2269
- // GET /api/v2/credentials/:id/reauthorize - v2: Get reauth requirements
2270
- router.route('/api/v2/credentials/:id/reauthorize').get(
2271
- catchAsyncError(async (req, res) => {
2272
- const user = await authenticateUser.execute(req);
2273
- const userId = user.getId();
2274
-
2275
- // Verify ownership
2276
- const credential = await getCredentialForUser.execute(
2277
- req.params.id,
2278
- userId
2279
- );
2280
-
2281
- // Get authorization requirements for this credential's type
2282
- const step = parseInt(req.query.step || '1', 10);
2283
- const requirements = await getAuthorizationRequirements.execute(
2284
- credential.type,
2285
- step
2286
- );
2287
-
2288
- res.json(requirements);
2289
- })
2290
- );
2291
-
2292
- // POST /api/v2/credentials/:id/reauthorize - v2: Submit reauth data
2293
- router.route('/api/v2/credentials/:id/reauthorize').post(
2294
- catchAsyncError(async (req, res) => {
2295
- const user = await authenticateUser.execute(req);
2296
- const userId = user.getId();
2297
-
2298
- const params = checkRequiredParams(req.body, ['data']);
2299
- const step = parseInt(req.body.step || '1', 10);
2300
- const sessionId = req.body.sessionId || null;
2301
-
2302
- const result = await reauthorizeCredential.execute(
2303
- req.params.id,
2304
- userId,
2305
- params.data,
2306
- step,
2307
- sessionId
2308
- );
2309
-
2310
- res.json(result);
2311
- })
2312
- );
2313
- }
2314
-
2315
- /**
2316
- * Helper function to map HTTP status codes to error codes
2317
- * @private
2318
- * @param {number} statusCode - HTTP status code
2319
- * @returns {string} Error code
2320
- */
2321
- function _getErrorCodeFromStatus(statusCode) {
2322
- const statusMap = {
2323
- 400: 'INVALID_REQUEST',
2324
- 401: 'INVALID_AUTH',
2325
- 403: 'PERMISSION_DENIED',
2326
- 404: 'NOT_FOUND',
2327
- 429: 'RATE_LIMITED',
2328
- 500: 'INTERNAL_ERROR',
2329
- 502: 'NETWORK_ERROR',
2330
- 503: 'SERVICE_UNAVAILABLE',
2331
- 504: 'TIMEOUT',
2332
- };
2333
-
2334
- return statusMap[statusCode] || 'UNKNOWN_ERROR';
2335
664
  }
2336
665
 
2337
666
  module.exports = { createIntegrationRouter, checkRequiredParams };