@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
@@ -17,6 +17,15 @@ 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');
20
29
  const { CreateIntegration } = require('./use-cases/create-integration');
21
30
  const { ModuleFactory } = require('../modules/module-factory');
22
31
  const {
@@ -69,6 +78,19 @@ const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
69
78
  const {
70
79
  ProcessAuthorizationCallback,
71
80
  } = 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');
72
94
 
73
95
  function createIntegrationRouter() {
74
96
  const { integrations: integrationClasses, userConfig } =
@@ -77,6 +99,7 @@ function createIntegrationRouter() {
77
99
  const integrationRepository = createIntegrationRepository();
78
100
  const credentialRepository = createCredentialRepository();
79
101
  const userRepository = createUserRepository();
102
+ const authSessionRepository = createAuthorizationSessionRepository();
80
103
 
81
104
  const getUserFromBearerToken = new GetUserFromBearerToken({
82
105
  userRepository,
@@ -103,10 +126,18 @@ function createIntegrationRouter() {
103
126
  userConfig,
104
127
  });
105
128
 
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
+
106
136
  const moduleFactory = new ModuleFactory({
107
137
  moduleRepository,
108
- moduleDefinitions:
109
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
138
+ moduleDefinitions: isModuleDefinitionFormat
139
+ ? integrationClasses
140
+ : getModulesDefinitionFromIntegrationClasses(integrationClasses),
110
141
  });
111
142
  const deleteIntegrationForUser = new DeleteIntegrationForUser({
112
143
  integrationRepository,
@@ -125,16 +156,32 @@ function createIntegrationRouter() {
125
156
  credentialRepository,
126
157
  });
127
158
 
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
+
128
172
  const createIntegration = new CreateIntegration({
129
173
  integrationRepository,
130
174
  integrationClasses,
131
175
  moduleFactory,
132
176
  });
133
177
 
178
+ const moduleDefinitions = isModuleDefinitionFormat
179
+ ? integrationClasses
180
+ : getModulesDefinitionFromIntegrationClasses(integrationClasses);
181
+
134
182
  const getEntitiesForUser = new GetEntitiesForUser({
135
183
  moduleRepository,
136
- moduleDefinitions:
137
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
184
+ moduleDefinitions,
138
185
  });
139
186
 
140
187
  const getIntegrationInstance = new GetIntegrationInstance({
@@ -150,37 +197,31 @@ function createIntegrationRouter() {
150
197
  });
151
198
 
152
199
  const getModuleInstanceFromType = new GetModuleInstanceFromType({
153
- moduleDefinitions:
154
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
200
+ moduleDefinitions,
155
201
  });
156
202
 
157
203
  const getEntityOptionsByType = new GetEntityOptionsByType({
158
- moduleDefinitions:
159
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
204
+ moduleDefinitions,
160
205
  });
161
206
 
162
207
  const testModuleAuth = new TestModuleAuth({
163
208
  moduleRepository,
164
- moduleDefinitions:
165
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
209
+ moduleDefinitions,
166
210
  });
167
211
 
168
212
  const getModule = new GetModule({
169
213
  moduleRepository,
170
- moduleDefinitions:
171
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
214
+ moduleDefinitions,
172
215
  });
173
216
 
174
217
  const getEntityOptionsById = new GetEntityOptionsById({
175
218
  moduleRepository,
176
- moduleDefinitions:
177
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
219
+ moduleDefinitions,
178
220
  });
179
221
 
180
222
  const refreshEntityOptions = new RefreshEntityOptions({
181
223
  moduleRepository,
182
- moduleDefinitions:
183
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
224
+ moduleDefinitions,
184
225
  });
185
226
 
186
227
  const getPossibleIntegrations = new GetPossibleIntegrations({
@@ -190,8 +231,28 @@ function createIntegrationRouter() {
190
231
  const processAuthorizationCallback = new ProcessAuthorizationCallback({
191
232
  moduleRepository,
192
233
  credentialRepository,
193
- moduleDefinitions:
194
- getModulesDefinitionFromIntegrationClasses(integrationClasses),
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,
195
256
  });
196
257
 
197
258
  const router = express();
@@ -214,6 +275,22 @@ function createIntegrationRouter() {
214
275
  getEntityOptionsById,
215
276
  refreshEntityOptions,
216
277
  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,
217
294
  });
218
295
  return router;
219
296
  }
@@ -258,20 +335,84 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
258
335
  updateIntegration,
259
336
  getPossibleIntegrations,
260
337
  } = 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
261
344
  router.route('/api/integrations').get(
262
345
  catchAsyncError(async (req, res) => {
263
346
  const user = await authenticateUser.execute(req);
264
347
  const userId = user.getId();
265
- const integrations = await getIntegrationsForUser.execute(userId);
266
- const results = {
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({
267
357
  entities: {
268
- options: await getPossibleIntegrations.execute(),
269
- authorized: await getEntitiesForUser.execute(userId),
358
+ options,
359
+ authorized,
270
360
  },
271
- integrations: integrations,
272
- };
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
+ );
273
399
 
274
- res.json(results);
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);
414
+
415
+ res.json({ entities });
275
416
  })
276
417
  );
277
418
 
@@ -480,187 +621,1717 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
480
621
  }
481
622
  })
482
623
  );
483
- }
484
-
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;
501
-
502
- router.route('/api/authorize').get(
503
- catchAsyncError(async (req, res) => {
504
- const user = await authenticateUser.execute(req);
505
- const userId = user.getId();
506
- const params = checkRequiredParams(req.query, ['entityType']);
507
- const module = await getModuleInstanceFromType.execute(
508
- userId,
509
- params.entityType
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
- }
518
624
 
519
- res.json(module.getAuthorizationRequirements());
520
- })
521
- );
625
+ // =========================================================================
626
+ // v2 API Routes - Integration endpoints (can evolve independently from v1)
627
+ // =========================================================================
522
628
 
523
- router.route('/api/authorize').post(
629
+ // POST /api/v2/integrations - Create integration (v2)
630
+ router.route('/api/v2/integrations').post(
524
631
  catchAsyncError(async (req, res) => {
525
632
  const user = await authenticateUser.execute(req);
526
633
  const userId = user.getId();
527
634
  const params = checkRequiredParams(req.body, [
528
- 'entityType',
529
- 'data',
635
+ 'entities',
636
+ 'config',
530
637
  ]);
531
638
 
532
- const entityDetails = await processAuthorizationCallback.execute(
639
+ get(params.config, 'type');
640
+
641
+ const integration = await createIntegration.execute(
642
+ params.entities,
533
643
  userId,
534
- params.entityType,
535
- params.data
644
+ params.config
536
645
  );
537
646
 
538
- res.json(entityDetails);
647
+ res.status(201).json(integration);
539
648
  })
540
649
  );
541
650
 
542
- router.route('/api/entity').post(
651
+ // PATCH /api/v2/integrations/:integrationId - Update integration (v2)
652
+ router.route('/api/v2/integrations/:integrationId').patch(
543
653
  catchAsyncError(async (req, res) => {
544
654
  const user = await authenticateUser.execute(req);
545
655
  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
556
- );
557
-
558
- if (!credential) {
559
- throw Boom.badRequest('Invalid credential ID');
560
- }
656
+ const params = checkRequiredParams(req.body, ['config']);
561
657
 
562
- const module = await getModuleInstanceFromType.execute(
658
+ const integration = await updateIntegration.execute(
659
+ req.params.integrationId,
563
660
  userId,
564
- params.entityType
565
- );
566
- const entityDetails = await module.getEntityDetails(
567
- module.api,
568
- null,
569
- null,
570
- userId
661
+ params.config
571
662
  );
572
-
573
- res.json(await module.findOrCreateEntity(entityDetails));
663
+ res.json(integration);
574
664
  })
575
665
  );
576
666
 
577
- router.route('/api/entity/options/:credentialId').get(
667
+ // DELETE /api/v2/integrations/:integrationId - Delete integration (v2)
668
+ router.route('/api/v2/integrations/:integrationId').delete(
578
669
  catchAsyncError(async (req, res) => {
579
670
  const user = await authenticateUser.execute(req);
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');
589
- }
590
-
591
- const params = checkRequiredParams(req.query, ['entityType']);
592
- const entityOptions = await getEntityOptionsByType.execute(
593
- userId,
594
- params.entityType
671
+ const params = checkRequiredParams(req.params, ['integrationId']);
672
+ await deleteIntegrationForUser.execute(
673
+ params.integrationId,
674
+ user.getId()
595
675
  );
596
-
597
- res.json(entityOptions);
676
+ res.status(204).json({});
598
677
  })
599
678
  );
600
679
 
601
- router.route('/api/entities/:entityId/test-auth').get(
680
+ // GET /api/v2/integrations/:integrationId - Get single integration (v2)
681
+ router.route('/api/v2/integrations/:integrationId').get(
602
682
  catchAsyncError(async (req, res) => {
603
683
  const user = await authenticateUser.execute(req);
604
- const params = checkRequiredParams(req.params, ['entityId']);
605
- const testAuthResponse = await testModuleAuth.execute(
606
- params.entityId,
607
- user // Pass User object for proper validation
608
- );
609
684
 
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' });
685
+ if (!user) {
686
+ throw Boom.forbidden('User not found');
623
687
  }
624
- })
625
- );
626
688
 
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
689
+ const params = checkRequiredParams(req.params, ['integrationId']);
690
+ const integration = await getIntegrationInstance.execute(
691
+ params.integrationId,
692
+ user.getId()
693
+ );
632
694
 
633
- res.json(module);
695
+ res.json({
696
+ id: integration.id,
697
+ entities: integration.entities,
698
+ status: integration.status,
699
+ config: integration.config,
700
+ });
634
701
  })
635
702
  );
636
703
 
637
- router.route('/api/entities/:entityId/options').post(
704
+ // GET /api/v2/integrations/:integrationId/config/options - Get config options (v2)
705
+ router.route('/api/v2/integrations/:integrationId/config/options').get(
638
706
  catchAsyncError(async (req, res) => {
639
707
  const user = await authenticateUser.execute(req);
640
- const params = checkRequiredParams(req.params, ['entityId']);
641
-
642
- const entityOptions = await getEntityOptionsById.execute(
643
- params.entityId,
644
- user // Pass User object
708
+ const params = checkRequiredParams(req.params, ['integrationId']);
709
+ const integration = await getIntegrationInstance.execute(
710
+ params.integrationId,
711
+ user.getId()
645
712
  );
646
-
647
- res.json(entityOptions);
713
+ res.json(await integration.send('GET_CONFIG_OPTIONS'));
648
714
  })
649
715
  );
650
716
 
651
- router.route('/api/entities/:entityId/options/refresh').post(
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
+ );
730
+
731
+ res.json(
732
+ await integration.send('REFRESH_CONFIG_OPTIONS', req.body)
733
+ );
734
+ })
735
+ );
736
+
737
+ // ALL /api/v2/integrations/:integrationId/actions - Get user actions (v2)
738
+ router.route('/api/v2/integrations/:integrationId/actions').all(
652
739
  catchAsyncError(async (req, res) => {
653
740
  const user = await authenticateUser.execute(req);
654
- const params = checkRequiredParams(req.params, ['entityId']);
655
- const updatedOptions = await refreshEntityOptions.execute(
656
- params.entityId,
657
- user, // Pass User object
658
- req.body
741
+ const params = checkRequiredParams(req.params, ['integrationId']);
742
+ const integration = await getIntegrationInstance.execute(
743
+ params.integrationId,
744
+ user.getId()
659
745
  );
660
-
661
- res.json(updatedOptions);
746
+ res.json(await integration.send('GET_USER_ACTIONS', req.body));
662
747
  })
663
748
  );
749
+
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(
1977
+ catchAsyncError(async (req, res) => {
1978
+ const user = await authenticateUser.execute(req);
1979
+ const params = checkRequiredParams(req.params, ['entityId']);
1980
+ const updatedOptions = await refreshEntityOptions.execute(
1981
+ params.entityId,
1982
+ user,
1983
+ req.body
1984
+ );
1985
+
1986
+ res.json(updatedOptions);
1987
+ })
1988
+ );
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';
664
2335
  }
665
2336
 
666
2337
  module.exports = { createIntegrationRouter, checkRequiredParams };