@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
package/core/Worker.js CHANGED
@@ -1,19 +1,41 @@
1
- const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/client-sqs');
2
1
  const _ = require('lodash');
3
2
  const { RequiredPropertyError } = require('../errors');
4
3
  const { get } = require('../assertions');
5
4
 
6
- const sqs = new SQSClient({ region: process.env.AWS_REGION });
7
-
5
+ /**
6
+ * Worker - Queue message producer/consumer base class.
7
+ *
8
+ * Subclass and override _run() to implement your worker logic.
9
+ * The queue transport (SQS, Netlify, etc.) is abstracted behind
10
+ * QueueClientInterface, injected via constructor options.
11
+ *
12
+ * BREAKING CHANGE (v3): A queueClient must be explicitly provided.
13
+ * For AWS/SQS, pass `new SqsQueueClient()` from @friggframework/provider-aws.
14
+ * See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.
15
+ */
8
16
  class Worker {
17
+ constructor(options = {}) {
18
+ this._queueClient = options.queueClient || null;
19
+ }
20
+
21
+ /**
22
+ * Get the queue client. Throws if none was injected.
23
+ * @returns {QueueClientInterface}
24
+ */
25
+ _getQueueClient() {
26
+ if (!this._queueClient) {
27
+ throw new Error(
28
+ 'Worker requires a queueClient. Pass one via constructor options, e.g.:\n' +
29
+ ' const { SqsQueueClient } = require("@friggframework/provider-aws");\n' +
30
+ ' new MyWorker({ queueClient: new SqsQueueClient() })\n' +
31
+ 'See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.'
32
+ );
33
+ }
34
+ return this._queueClient;
35
+ }
36
+
9
37
  async getQueueURL(params) {
10
- // Passing params in because there will be multiple QueueNames
11
- // let params = {
12
- // QueueName: process.env.QueueName
13
- // };
14
- const command = new GetQueueUrlCommand(params);
15
- const data = await sqs.send(command);
16
- return data.QueueUrl;
38
+ return this._getQueueClient().getQueueUrl(params);
17
39
  }
18
40
 
19
41
  async run(params, context = {}) {
@@ -47,8 +69,7 @@ class Worker {
47
69
  }
48
70
 
49
71
  async sendAsyncSQSMessage(params) {
50
- const command = new SendMessageCommand(params);
51
- const data = await sqs.send(command);
72
+ const data = await this._getQueueClient().sendMessage(params);
52
73
  return data.MessageId;
53
74
  }
54
75
 
@@ -0,0 +1,262 @@
1
+ const express = require('express');
2
+ const Boom = require('@hapi/boom');
3
+ const catchAsyncError = require('express-async-handler');
4
+ const {
5
+ createCredentialRepository,
6
+ } = require('./repositories/credential-repository-factory');
7
+ const {
8
+ createModuleRepository,
9
+ } = require('../modules/repositories/module-repository-factory');
10
+ const {
11
+ ListCredentialsForUser,
12
+ } = require('./use-cases/list-credentials-for-user');
13
+ const { GetCredentialForUser } = require('./use-cases/get-credential-for-user');
14
+ const {
15
+ DeleteCredentialForUser,
16
+ } = require('./use-cases/delete-credential-for-user');
17
+ const { ReauthorizeCredential } = require('./use-cases/reauthorize-credential');
18
+
19
+ /**
20
+ * Boom Error Handler Middleware
21
+ * Handles Boom errors and converts them to appropriate HTTP responses
22
+ * @param {Error} err - Error object
23
+ * @param {Request} req - Express request
24
+ * @param {Response} res - Express response
25
+ * @param {Function} next - Express next function
26
+ */
27
+ function boomErrorHandler(err, req, res, next) {
28
+ // Handle Boom errors
29
+ if (err.isBoom) {
30
+ return res.status(err.output.statusCode).json({
31
+ error: err.output.payload.message,
32
+ statusCode: err.output.statusCode,
33
+ });
34
+ }
35
+
36
+ // Handle generic errors (500)
37
+ console.error('Unexpected error:', err);
38
+ return res.status(500).json({
39
+ error: 'Internal server error',
40
+ statusCode: 500,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Create Credential Router
46
+ * Factory function that creates an Express router with credential management endpoints
47
+ *
48
+ * Endpoints:
49
+ * - GET /api/credentials - List all credentials for authenticated user
50
+ * - GET /api/credentials/:id - Get a single credential
51
+ * - DELETE /api/credentials/:id - Delete a credential
52
+ * - POST /api/credentials/:id/reauthorize - Reauthorize a credential
53
+ *
54
+ * Security:
55
+ * - All endpoints require authentication (via authenticateUser middleware)
56
+ * - Credentials are filtered to only belong to the authenticated user
57
+ * - Sensitive data (tokens) are filtered from responses
58
+ *
59
+ * @returns {express.Router} Configured Express router
60
+ */
61
+ function createCredentialRouter() {
62
+ const router = express.Router();
63
+
64
+ // Load configuration and create repositories
65
+ const credentialRepository = createCredentialRepository();
66
+ const moduleRepository = createModuleRepository();
67
+
68
+ // Create credential use cases
69
+ const listCredentialsForUser = new ListCredentialsForUser({
70
+ credentialRepository,
71
+ });
72
+
73
+ const getCredentialForUser = new GetCredentialForUser({
74
+ credentialRepository,
75
+ });
76
+
77
+ const deleteCredentialForUser = new DeleteCredentialForUser({
78
+ credentialRepository,
79
+ });
80
+
81
+ const reauthorizeCredential = new ReauthorizeCredential({
82
+ credentialRepository,
83
+ moduleRepository,
84
+ });
85
+
86
+ /**
87
+ * Filter sensitive data from credential objects
88
+ * Removes tokens and other sensitive fields from credentials before sending to client
89
+ *
90
+ * @param {Object|Array} credentials - Credential(s) to filter
91
+ * @returns {Object|Array} Filtered credential(s)
92
+ */
93
+ function filterSensitiveData(credentials) {
94
+ const filter = (cred) => {
95
+ if (!cred) return cred;
96
+
97
+ // Create a copy without sensitive fields
98
+ const {
99
+ data,
100
+ access_token,
101
+ refresh_token,
102
+ id_token,
103
+ domain,
104
+ ...safeCredential
105
+ } = cred;
106
+
107
+ // Ensure we have timestamps in ISO format
108
+ if (
109
+ safeCredential.createdAt &&
110
+ !(safeCredential.createdAt instanceof Date)
111
+ ) {
112
+ safeCredential.createdAt = new Date(
113
+ safeCredential.createdAt
114
+ ).toISOString();
115
+ }
116
+ if (
117
+ safeCredential.updatedAt &&
118
+ !(safeCredential.updatedAt instanceof Date)
119
+ ) {
120
+ safeCredential.updatedAt = new Date(
121
+ safeCredential.updatedAt
122
+ ).toISOString();
123
+ }
124
+
125
+ return safeCredential;
126
+ };
127
+
128
+ return Array.isArray(credentials)
129
+ ? credentials.map(filter)
130
+ : filter(credentials);
131
+ }
132
+
133
+ // GET /api/credentials - List all credentials for authenticated user
134
+ router.get(
135
+ '/',
136
+ catchAsyncError(async (req, res) => {
137
+ // Expect authentication middleware to have set req.user
138
+ if (!req.user) {
139
+ throw Boom.unauthorized('Authentication required');
140
+ }
141
+ const userId =
142
+ typeof req.user.getId === 'function'
143
+ ? req.user.getId()
144
+ : req.user.id;
145
+
146
+ const credentials = await listCredentialsForUser.execute(userId);
147
+
148
+ // Filter out sensitive data before responding
149
+ const safeCredentials = filterSensitiveData(credentials);
150
+
151
+ res.json({ credentials: safeCredentials });
152
+ })
153
+ );
154
+
155
+ // GET /api/credentials/:id - Get a single credential
156
+ router.get(
157
+ '/:id',
158
+ catchAsyncError(async (req, res) => {
159
+ if (!req.user) {
160
+ throw Boom.unauthorized('Authentication required');
161
+ }
162
+ const userId =
163
+ typeof req.user.getId === 'function'
164
+ ? req.user.getId()
165
+ : req.user.id;
166
+ const credentialId = req.params.id;
167
+
168
+ const credential = await getCredentialForUser.execute(
169
+ credentialId,
170
+ userId
171
+ );
172
+
173
+ // Filter out sensitive data before responding
174
+ const safeCredential = filterSensitiveData(credential);
175
+
176
+ res.json(safeCredential);
177
+ })
178
+ );
179
+
180
+ // DELETE /api/credentials/:id - Delete a credential
181
+ router.delete(
182
+ '/:id',
183
+ catchAsyncError(async (req, res) => {
184
+ if (!req.user) {
185
+ throw Boom.unauthorized('Authentication required');
186
+ }
187
+ const userId =
188
+ typeof req.user.getId === 'function'
189
+ ? req.user.getId()
190
+ : req.user.id;
191
+ const credentialId = req.params.id;
192
+
193
+ const result = await deleteCredentialForUser.execute(
194
+ credentialId,
195
+ userId
196
+ );
197
+
198
+ // Check if deletion was successful
199
+ if (result.deletedCount === 0) {
200
+ throw Boom.internal('Failed to delete credential');
201
+ }
202
+
203
+ res.json({
204
+ success: true,
205
+ message: `Credential ${credentialId} deleted successfully`,
206
+ });
207
+ })
208
+ );
209
+
210
+ // POST /api/credentials/:id/reauthorize - Reauthorize a credential
211
+ router.post(
212
+ '/:id/reauthorize',
213
+ catchAsyncError(async (req, res) => {
214
+ if (!req.user) {
215
+ throw Boom.unauthorized('Authentication required');
216
+ }
217
+ const userId =
218
+ typeof req.user.getId === 'function'
219
+ ? req.user.getId()
220
+ : req.user.id;
221
+ const credentialId = req.params.id;
222
+
223
+ // Validate request body
224
+ if (!req.body.data) {
225
+ throw Boom.badRequest('data is required in request body');
226
+ }
227
+
228
+ // Get step and sessionId from request
229
+ const step = parseInt(req.body.step || '1', 10);
230
+ const sessionId = req.body.sessionId;
231
+
232
+ // Validate step is a positive integer
233
+ if (step < 1 || !Number.isInteger(step)) {
234
+ throw Boom.badRequest('step must be a positive integer');
235
+ }
236
+
237
+ // Validate sessionId is present for steps > 1
238
+ if (step > 1 && !sessionId) {
239
+ throw Boom.badRequest('sessionId is required for step > 1');
240
+ }
241
+
242
+ // Execute the reauthorization
243
+ const result = await reauthorizeCredential.execute(
244
+ credentialId,
245
+ userId,
246
+ req.body.data,
247
+ step,
248
+ sessionId
249
+ );
250
+
251
+ res.json(result);
252
+ })
253
+ );
254
+
255
+ // Boom error handler middleware
256
+ // Must be added after all routes to catch errors from handlers
257
+ router.use(boomErrorHandler);
258
+
259
+ return router;
260
+ }
261
+
262
+ module.exports = { createCredentialRouter, boomErrorHandler };
@@ -3,6 +3,7 @@ const {
3
3
  toObjectId,
4
4
  fromObjectId,
5
5
  findOne,
6
+ findMany,
6
7
  insertOne,
7
8
  updateOne,
8
9
  deleteOne,
@@ -106,7 +107,10 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
106
107
  const updateDocument = {
107
108
  userId: existing.userId,
108
109
  externalId: existing.externalId,
109
- authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
110
+ authIsValid:
111
+ authIsValid !== undefined
112
+ ? authIsValid
113
+ : existing.authIsValid,
110
114
  data: mergedData,
111
115
  updatedAt: now,
112
116
  };
@@ -172,8 +176,50 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
172
176
  return this._mapCredential(decryptedCredential);
173
177
  }
174
178
 
179
+ /**
180
+ * Find credential(s) by filter criteria
181
+ *
182
+ * When filter includes only userId, returns an array of all credentials for that user
183
+ * When filter includes credentialId or externalId, returns a single credential or null
184
+ *
185
+ * @param {Object} filter
186
+ * @param {string} [filter.userId] - User ID
187
+ * @param {string} [filter.externalId] - External ID
188
+ * @param {string} [filter.credentialId] - Credential ID
189
+ * @returns {Promise<Array|Object|null>} Credential array, single credential, or null
190
+ */
175
191
  async findCredential(filter) {
176
192
  const query = this._buildFilter(filter);
193
+
194
+ // If filtering by userId only, return all credentials for that user
195
+ const hasOnlyUserId =
196
+ filter.userId &&
197
+ !filter.credentialId &&
198
+ !filter.externalId &&
199
+ !filter.id;
200
+
201
+ if (hasOnlyUserId) {
202
+ const credentials = await findMany(
203
+ this.prisma,
204
+ 'Credential',
205
+ query
206
+ );
207
+
208
+ const decryptedCredentials = await Promise.all(
209
+ credentials.map(async (credential) => {
210
+ const decrypted =
211
+ await this.encryptionService.decryptFields(
212
+ 'Credential',
213
+ credential
214
+ );
215
+ return this._mapCredentialWithMetadata(decrypted);
216
+ })
217
+ );
218
+
219
+ return decryptedCredentials;
220
+ }
221
+
222
+ // Otherwise, find single credential
177
223
  const credential = await findOne(this.prisma, 'Credential', query);
178
224
  if (!credential) return null;
179
225
 
@@ -299,6 +345,30 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
299
345
  ...data,
300
346
  };
301
347
  }
348
+
349
+ /**
350
+ * Map credential document with metadata (for list views)
351
+ * Includes timestamps and additional fields needed by API
352
+ * @private
353
+ */
354
+ _mapCredentialWithMetadata(doc) {
355
+ const data = doc?.data || {};
356
+ const id = fromObjectId(doc?._id);
357
+ const userId = doc?.userId;
358
+ return {
359
+ id,
360
+ type: doc?.type,
361
+ userId,
362
+ externalId: doc?.externalId ?? null,
363
+ authIsValid: doc?.authIsValid ?? null,
364
+ entityCount: doc?.entityCount,
365
+ createdAt: doc?.createdAt,
366
+ updatedAt: doc?.updatedAt,
367
+ access_token: data.access_token,
368
+ refresh_token: data.refresh_token,
369
+ ...data,
370
+ };
371
+ }
302
372
  }
303
373
 
304
374
  module.exports = { CredentialRepositoryDocumentDB };
@@ -1,10 +1,6 @@
1
- const { CredentialRepositoryMongo } = require('./credential-repository-mongo');
2
1
  const {
3
2
  CredentialRepositoryPostgres,
4
3
  } = require('./credential-repository-postgres');
5
- const {
6
- CredentialRepositoryDocumentDB,
7
- } = require('./credential-repository-documentdb');
8
4
  const config = require('../../database/config');
9
5
 
10
6
  /**
@@ -30,12 +26,14 @@ function createCredentialRepository() {
30
26
 
31
27
  switch (dbType) {
32
28
  case 'mongodb':
29
+ const { CredentialRepositoryMongo } = require('./credential-repository-mongo');
33
30
  return new CredentialRepositoryMongo();
34
31
 
35
32
  case 'postgresql':
36
33
  return new CredentialRepositoryPostgres();
37
34
 
38
35
  case 'documentdb':
36
+ const { CredentialRepositoryDocumentDB } = require('./credential-repository-documentdb');
39
37
  return new CredentialRepositoryDocumentDB();
40
38
 
41
39
  default:
@@ -48,7 +46,7 @@ function createCredentialRepository() {
48
46
  module.exports = {
49
47
  createCredentialRepository,
50
48
  // Export adapters for direct testing
51
- CredentialRepositoryMongo,
49
+ get CredentialRepositoryMongo() { return require('./credential-repository-mongo').CredentialRepositoryMongo; },
52
50
  CredentialRepositoryPostgres,
53
- CredentialRepositoryDocumentDB,
51
+ get CredentialRepositoryDocumentDB() { return require('./credential-repository-documentdb').CredentialRepositoryDocumentDB; },
54
52
  };
@@ -68,10 +68,16 @@ class CredentialRepositoryInterface {
68
68
  }
69
69
 
70
70
  /**
71
- * Find a credential by filter criteria
71
+ * Find credential(s) by filter criteria
72
+ *
73
+ * When filter includes only userId, returns an array of all credentials for that user
74
+ * When filter includes credentialId or externalId, returns a single credential or null
72
75
  *
73
76
  * @param {Object} filter - Filter criteria
74
- * @returns {Promise<Object|null>} Credential object or null if not found
77
+ * @param {string} [filter.userId] - User ID
78
+ * @param {string} [filter.externalId] - External ID
79
+ * @param {string} [filter.credentialId] - Credential ID
80
+ * @returns {Promise<Array|Object|null>} Credential array, single credential, or null
75
81
  * @abstract
76
82
  */
77
83
  async findCredential(filter) {
@@ -121,7 +121,10 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
121
121
  data: {
122
122
  userId: existing.userId,
123
123
  externalId: existing.externalId,
124
- authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
124
+ authIsValid:
125
+ authIsValid !== undefined
126
+ ? authIsValid
127
+ : existing.authIsValid,
125
128
  data: mergedData,
126
129
  },
127
130
  });
@@ -154,18 +157,52 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
154
157
  }
155
158
 
156
159
  /**
157
- * Find a credential by filter criteria
158
- * Replaces: Credential.findOne(query)
160
+ * Find credential(s) by filter criteria
161
+ * Replaces: Credential.find(query) or Credential.findOne(query)
162
+ *
163
+ * When filter includes only userId, returns an array of all credentials for that user
164
+ * When filter includes credentialId or externalId, returns a single credential or null
159
165
  *
160
166
  * @param {Object} filter
161
167
  * @param {string} [filter.userId] - User ID
162
168
  * @param {string} [filter.externalId] - External ID
163
169
  * @param {string} [filter.credentialId] - Credential ID
164
- * @returns {Promise<Object|null>} Credential object or null if not found
170
+ * @returns {Promise<Array|Object|null>} Credential array, single credential, or null
165
171
  */
166
172
  async findCredential(filter) {
167
173
  const where = this._convertFilterToWhere(filter);
168
174
 
175
+ // If filtering by userId only, return all credentials for that user
176
+ const hasOnlyUserId =
177
+ filter.userId &&
178
+ !filter.credentialId &&
179
+ !filter.externalId &&
180
+ !filter.id;
181
+
182
+ if (hasOnlyUserId) {
183
+ const credentials = await this.prisma.credential.findMany({
184
+ where,
185
+ });
186
+
187
+ return credentials.map((credential) => {
188
+ const data = credential.data || {};
189
+ return {
190
+ id: credential.id,
191
+ type: credential.type,
192
+ userId: credential.userId,
193
+ externalId: credential.externalId,
194
+ authIsValid: credential.authIsValid,
195
+ entityCount: credential.entityCount,
196
+ createdAt: credential.createdAt,
197
+ updatedAt: credential.updatedAt,
198
+ access_token: data.access_token,
199
+ refresh_token: data.refresh_token,
200
+ ...data,
201
+ };
202
+ });
203
+ }
204
+
205
+ // Otherwise, find single credential
169
206
  const credential = await this.prisma.credential.findFirst({
170
207
  where,
171
208
  });
@@ -178,9 +215,13 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
178
215
 
179
216
  return {
180
217
  id: credential.id,
218
+ type: credential.type,
181
219
  userId: credential.userId,
182
220
  externalId: credential.externalId,
183
221
  authIsValid: credential.authIsValid,
222
+ entityCount: credential.entityCount,
223
+ createdAt: credential.createdAt,
224
+ updatedAt: credential.updatedAt,
184
225
  access_token: data.access_token,
185
226
  refresh_token: data.refresh_token,
186
227
  ...data,
@@ -138,7 +138,10 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
138
138
  data: {
139
139
  userId: this._convertId(existing.userId),
140
140
  externalId: existing.externalId,
141
- authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
141
+ authIsValid:
142
+ authIsValid !== undefined
143
+ ? authIsValid
144
+ : existing.authIsValid,
142
145
  data: mergedData,
143
146
  },
144
147
  });
@@ -172,17 +175,51 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
172
175
  }
173
176
 
174
177
  /**
175
- * Find a credential by filter criteria
178
+ * Find credential(s) by filter criteria
179
+ *
180
+ * When filter includes only userId, returns an array of all credentials for that user
181
+ * When filter includes credentialId or externalId, returns a single credential or null
176
182
  *
177
183
  * @param {Object} filter
178
184
  * @param {string} [filter.userId] - User ID (string from application layer)
179
185
  * @param {string} [filter.externalId] - External ID
180
186
  * @param {string} [filter.credentialId] - Credential ID (string from application layer)
181
- * @returns {Promise<Object|null>} Credential object with string IDs or null if not found
187
+ * @returns {Promise<Array|Object|null>} Credential array, single credential with string IDs, or null
182
188
  */
183
189
  async findCredential(filter) {
184
190
  const where = this._convertFilterToWhere(filter);
185
191
 
192
+ // If filtering by userId only, return all credentials for that user
193
+ const hasOnlyUserId =
194
+ filter.userId &&
195
+ !filter.credentialId &&
196
+ !filter.externalId &&
197
+ !filter.id;
198
+
199
+ if (hasOnlyUserId) {
200
+ const credentials = await this.prisma.credential.findMany({
201
+ where,
202
+ });
203
+
204
+ return credentials.map((credential) => {
205
+ const data = credential.data || {};
206
+ return {
207
+ id: credential.id.toString(),
208
+ type: credential.type,
209
+ userId: credential.userId?.toString(),
210
+ externalId: credential.externalId,
211
+ authIsValid: credential.authIsValid,
212
+ entityCount: credential.entityCount,
213
+ createdAt: credential.createdAt,
214
+ updatedAt: credential.updatedAt,
215
+ access_token: data.access_token,
216
+ refresh_token: data.refresh_token,
217
+ ...data,
218
+ };
219
+ });
220
+ }
221
+
222
+ // Otherwise, find single credential
186
223
  const credential = await this.prisma.credential.findFirst({
187
224
  where,
188
225
  });
@@ -195,9 +232,13 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
195
232
 
196
233
  return {
197
234
  id: credential.id.toString(),
235
+ type: credential.type,
198
236
  userId: credential.userId?.toString(),
199
237
  externalId: credential.externalId,
200
238
  authIsValid: credential.authIsValid,
239
+ entityCount: credential.entityCount,
240
+ createdAt: credential.createdAt,
241
+ updatedAt: credential.updatedAt,
201
242
  access_token: data.access_token,
202
243
  refresh_token: data.refresh_token,
203
244
  ...data,
@@ -243,7 +243,9 @@ class CredentialRepository extends CredentialRepositoryInterface {
243
243
  externalId:
244
244
  externalId !== undefined ? externalId : existing.externalId,
245
245
  authIsValid:
246
- authIsValid !== undefined ? authIsValid : existing.authIsValid,
246
+ authIsValid !== undefined
247
+ ? authIsValid
248
+ : existing.authIsValid,
247
249
  data: mergedData,
248
250
  },
249
251
  });