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