@friggframework/core 2.0.0--canary.686.df38e8e.0 → 2.0.0--canary.710.9045bbc.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 (217) hide show
  1. package/CHANGELOG.md +0 -16
  2. package/CLAUDE.md +41 -29
  3. package/README.md +0 -105
  4. package/application/commands/README.md +17 -11
  5. package/application/commands/entity-commands.js +18 -60
  6. package/application/commands/integration-commands.js +73 -20
  7. package/application/commands/scheduler-commands.js +7 -11
  8. package/application/commands/user-commands.js +5 -17
  9. package/application/index.js +2 -6
  10. package/core/Worker.js +7 -31
  11. package/core/create-handler.js +0 -9
  12. package/credential/repositories/credential-repository-documentdb.js +0 -34
  13. package/credential/repositories/credential-repository-interface.js +0 -15
  14. package/credential/repositories/credential-repository-mongo.js +0 -23
  15. package/credential/repositories/credential-repository-postgres.js +0 -28
  16. package/database/config.js +24 -48
  17. package/database/documentdb-encryption-service.js +22 -9
  18. package/database/documentdb-utils.js +0 -2
  19. package/database/encryption/README.md +17 -81
  20. package/database/encryption/documentdb-encryption-service.md +1 -78
  21. package/database/encryption/encryption-schema-registry.js +46 -123
  22. package/database/encryption/field-encryption-service.js +5 -69
  23. package/database/encryption/integration-mapping-encryption.js +1 -1
  24. package/database/prisma.js +38 -15
  25. package/database/use-cases/check-encryption-health-use-case.js +52 -43
  26. package/errors/index.js +4 -0
  27. package/errors/mapping-already-exists-error.js +14 -0
  28. package/generated/prisma-mongodb/edge.js +4 -43
  29. package/generated/prisma-mongodb/index-browser.js +1 -40
  30. package/generated/prisma-mongodb/index.d.ts +111 -4739
  31. package/generated/prisma-mongodb/index.js +4 -43
  32. package/generated/prisma-mongodb/package.json +1 -1
  33. package/generated/prisma-mongodb/schema.prisma +0 -92
  34. package/generated/prisma-mongodb/wasm.js +4 -43
  35. package/generated/prisma-postgresql/edge.js +4 -43
  36. package/generated/prisma-postgresql/index-browser.js +1 -40
  37. package/generated/prisma-postgresql/index.d.ts +33 -4966
  38. package/generated/prisma-postgresql/index.js +4 -43
  39. package/generated/prisma-postgresql/package.json +1 -1
  40. package/generated/prisma-postgresql/schema.prisma +0 -87
  41. package/generated/prisma-postgresql/wasm.js +4 -43
  42. package/handlers/WEBHOOKS.md +29 -0
  43. package/handlers/app-definition-loader.js +19 -121
  44. package/handlers/app-handler-helpers.js +3 -25
  45. package/handlers/backend-utils.js +25 -31
  46. package/handlers/middleware/admin-auth.js +1 -14
  47. package/handlers/routers/auth.js +10 -46
  48. package/handlers/routers/health.js +4 -13
  49. package/handlers/routers/integration-defined-routers.js +92 -144
  50. package/handlers/routers/integration-webhook-routers.js +64 -102
  51. package/handlers/workers/integration-defined-workers.js +2 -5
  52. package/index.js +3 -65
  53. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +67 -13
  54. package/infrastructure/scheduler/index.js +6 -19
  55. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  56. package/integrations/index.js +0 -1
  57. package/integrations/integration-base.js +29 -0
  58. package/integrations/integration-router.js +96 -131
  59. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  60. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  61. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  62. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  63. package/integrations/repositories/integration-mapping-repository.js +29 -0
  64. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  65. package/integrations/repositories/integration-repository-interface.js +31 -17
  66. package/integrations/repositories/integration-repository-mongo.js +129 -60
  67. package/integrations/repositories/integration-repository-postgres.js +134 -62
  68. package/integrations/repositories/process-repository-mongo.js +1 -1
  69. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  70. package/integrations/use-cases/add-entities-to-integration.js +92 -0
  71. package/integrations/use-cases/create-integration.js +75 -72
  72. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  73. package/integrations/use-cases/index.js +1 -7
  74. package/integrations/use-cases/remove-entities-from-integration.js +70 -0
  75. package/integrations/utils/assert-entity-ids-input.js +42 -0
  76. package/integrations/utils/map-integration-dto.js +14 -43
  77. package/jest-setup.js +13 -2
  78. package/jest-teardown.js +5 -2
  79. package/jest.config.js +4 -0
  80. package/jest.db.config.js +23 -0
  81. package/modules/index.js +0 -14
  82. package/modules/module-factory.js +0 -12
  83. package/modules/module.js +2 -53
  84. package/modules/repositories/module-repository-documentdb.js +6 -51
  85. package/modules/repositories/module-repository-interface.js +2 -19
  86. package/modules/repositories/module-repository-mongo.js +74 -67
  87. package/modules/repositories/module-repository-postgres.js +74 -66
  88. package/modules/repositories/module-repository.js +1 -2
  89. package/modules/requester/requester.js +2 -27
  90. package/modules/use-cases/get-entities-for-user.js +13 -26
  91. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  92. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  93. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  94. package/modules/use-cases/get-module.js +4 -10
  95. package/modules/use-cases/process-authorization-callback.js +19 -53
  96. package/modules/use-cases/refresh-entity-options.js +4 -10
  97. package/modules/use-cases/test-module-auth.js +4 -10
  98. package/modules/utils/map-module-dto.js +5 -3
  99. package/modules/utils/to-credential-id.js +14 -0
  100. package/package.json +7 -6
  101. package/prisma-mongodb/schema.prisma +0 -92
  102. package/prisma-postgresql/schema.prisma +0 -87
  103. package/queues/index.js +0 -25
  104. package/queues/queuer-util.js +6 -5
  105. package/types/index.d.ts +0 -2
  106. package/user/repositories/user-repository-documentdb.js +2 -27
  107. package/user/repositories/user-repository-interface.js +0 -18
  108. package/user/repositories/user-repository-mongo.js +0 -24
  109. package/user/repositories/user-repository-postgres.js +0 -18
  110. package/user/tests/doubles/test-user-repository.js +1 -1
  111. package/user/use-cases/authenticate-user.js +1 -7
  112. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  113. package/user/use-cases/get-user-from-bearer-token.js +7 -2
  114. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  115. package/user/user.js +1 -64
  116. package/credential/use-cases/credential-operations-for-user.js +0 -202
  117. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  118. package/database/encryption/encryption-config.js +0 -219
  119. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  120. package/database/organization-where.js +0 -18
  121. package/database/unique-violation.js +0 -25
  122. package/extensions/bootstrap-runner.js +0 -43
  123. package/extensions/extension-loader.js +0 -187
  124. package/extensions/index.js +0 -41
  125. package/extensions/initialize-app.js +0 -123
  126. package/extensions/route-mounter.js +0 -103
  127. package/extensions/schema-composer.js +0 -188
  128. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -220
  129. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  130. package/integrations/repositories/integration-not-found.js +0 -14
  131. package/integrations/schedule-config.js +0 -80
  132. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  133. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  134. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  135. package/management-api/create-management-api-router.js +0 -49
  136. package/management-api/error-response.js +0 -116
  137. package/management-api/management-api-config.js +0 -151
  138. package/management-api/meta-router.js +0 -76
  139. package/management-api/openapi/build-openapi.js +0 -144
  140. package/management-api/openapi/components.js +0 -408
  141. package/management-api/openapi/v1-operations.js +0 -83
  142. package/management-api/openapi/v2-operations.js +0 -264
  143. package/management-api/route-registry.js +0 -411
  144. package/management-api/test-support/http-client.js +0 -39
  145. package/management-api/use-cases/get-management-api-meta.js +0 -57
  146. package/management-api/v1-deprecation.js +0 -120
  147. package/management-api/v2/create-v2-router.js +0 -85
  148. package/management-api/v2/dependencies.js +0 -281
  149. package/management-api/v2/dto.js +0 -143
  150. package/management-api/v2/handlers/authorization.js +0 -94
  151. package/management-api/v2/handlers/credentials.js +0 -33
  152. package/management-api/v2/handlers/entities.js +0 -77
  153. package/management-api/v2/handlers/integrations.js +0 -128
  154. package/management-api/v2/handlers/proxy.js +0 -34
  155. package/management-api/v2/request.js +0 -85
  156. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  157. package/modules/domain/authorization-session.js +0 -144
  158. package/modules/entity-scope.js +0 -15
  159. package/modules/entity-type-registry.js +0 -311
  160. package/modules/proxy/proxy-errors.js +0 -20
  161. package/modules/proxy/proxy-path.js +0 -179
  162. package/modules/proxy/proxy-policy.js +0 -248
  163. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  164. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  165. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  166. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  167. package/modules/repositories/entity-not-found.js +0 -18
  168. package/modules/requester/proxy-request.js +0 -244
  169. package/modules/scope-config.js +0 -192
  170. package/modules/scope-policy.js +0 -112
  171. package/modules/scope-resolution.js +0 -288
  172. package/modules/scope-visibility.js +0 -247
  173. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  174. package/modules/tests/doubles/ownership-matching.js +0 -18
  175. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  176. package/modules/use-cases/create-entity-from-credential.js +0 -113
  177. package/modules/use-cases/entity-operations-for-user.js +0 -162
  178. package/modules/use-cases/entity-types.js +0 -139
  179. package/modules/use-cases/execute-entity-proxy-request.js +0 -286
  180. package/modules/use-cases/get-authorization-requirements.js +0 -100
  181. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  182. package/modules/use-cases/manage-access.js +0 -21
  183. package/modules/use-cases/multi-step-authorization.js +0 -329
  184. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  185. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  186. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  187. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  188. package/providers/index.js +0 -23
  189. package/providers/resolve-provider.js +0 -93
  190. package/providers/runtime-detection.js +0 -124
  191. package/queues/providers/index.js +0 -11
  192. package/queues/providers/netlify-background-provider.js +0 -187
  193. package/queues/providers/qstash-queue-provider.js +0 -219
  194. package/queues/providers/sqs-queue-provider.js +0 -70
  195. package/queues/queue-client-interface.js +0 -55
  196. package/queues/queue-message-auth.js +0 -193
  197. package/queues/queue-provider-factory.js +0 -101
  198. package/queues/queue-provider.js +0 -65
  199. package/queues/sqs-queue-client.js +0 -81
  200. package/types/modules/index.d.ts +0 -197
  201. package/types/user/index.d.ts +0 -134
  202. package/user/adopter-jwt-config.js +0 -177
  203. package/user/adopter-jwt-errors.js +0 -59
  204. package/user/adopter-token-verifier.js +0 -122
  205. package/user/app-user-helpers.js +0 -97
  206. package/user/create-user-authentication.js +0 -64
  207. package/user/membership.js +0 -51
  208. package/user/repositories/organization-membership-record.js +0 -19
  209. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  210. package/user/repositories/organization-membership-repository-factory.js +0 -42
  211. package/user/repositories/organization-membership-repository-interface.js +0 -57
  212. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  213. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  214. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  215. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  216. package/user/use-cases/ensure-organization-membership.js +0 -92
  217. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -31,6 +31,9 @@ const {
31
31
  GetIntegrationInstance,
32
32
  } = require('./use-cases/get-integration-instance');
33
33
  const { UpdateIntegration } = require('./use-cases/update-integration');
34
+ const {
35
+ getModulesDefinitionFromIntegrationClasses,
36
+ } = require('./utils/map-integration-dto');
34
37
  const {
35
38
  GetModuleInstanceFromType,
36
39
  } = require('../modules/use-cases/get-module-instance-from-type');
@@ -51,51 +54,47 @@ const {
51
54
  const {
52
55
  createUserRepository,
53
56
  } = require('../user/repositories/user-repository-factory');
57
+ const {
58
+ GetUserFromBearerToken,
59
+ } = require('../user/use-cases/get-user-from-bearer-token');
60
+ const {
61
+ GetUserFromXFriggHeaders,
62
+ } = require('../user/use-cases/get-user-from-x-frigg-headers');
63
+ const {
64
+ GetUserFromAdopterJwt,
65
+ } = require('../user/use-cases/get-user-from-adopter-jwt');
54
66
  const {
55
67
  AuthenticateWithSharedSecret,
56
68
  } = require('../user/use-cases/authenticate-with-shared-secret');
57
69
  const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
58
- const {
59
- buildUserAuthentication,
60
- } = require('../user/create-user-authentication');
61
70
  const {
62
71
  ProcessAuthorizationCallback,
63
72
  } = require('../modules/use-cases/process-authorization-callback');
64
- const { createScopePolicy } = require('../modules/scope-policy');
65
- const {
66
- AuthorizeEntityForUser,
67
- } = require('../modules/use-cases/authorize-entity-for-user');
68
- const {
69
- CreateEntityFromCredential,
70
- } = require('../modules/use-cases/create-entity-from-credential');
71
- const {
72
- ResolveIntegrationOwnership,
73
- } = require('./use-cases/resolve-integration-ownership');
74
- const { GetOwnedIntegrationRecord } = require('./use-cases/integration-operations-for-user');
75
73
 
76
74
  const log = getLogger('frigg.integrations');
77
75
 
78
76
  function createIntegrationRouter() {
79
- // moduleDefinitions is the app registry's definitions (ADR-059), so
80
- // /api/authorize and friends accept a type no integration binds.
81
- const {
82
- integrations: integrationClasses,
83
- userConfig,
84
- moduleDefinitions,
85
- ownership,
86
- entityTypeRegistry,
87
- } = loadAppDefinition();
88
- // Ownership by scope (ADR-056 §3): one object every use case asks.
89
- const scopePolicy = createScopePolicy({ ownership, entityTypeRegistry });
77
+ const { integrations: integrationClasses, userConfig } =
78
+ loadAppDefinition();
90
79
  const moduleRepository = createModuleRepository();
91
80
  const integrationRepository = createIntegrationRepository();
92
81
  const credentialRepository = createCredentialRepository();
93
82
  const userRepository = createUserRepository();
94
- const {
95
- getUserFromBearerToken,
96
- getUserFromXFriggHeaders,
97
- getUserFromAdopterJwt,
98
- } = buildUserAuthentication({ userConfig, userRepository });
83
+
84
+ const getUserFromBearerToken = new GetUserFromBearerToken({
85
+ userRepository,
86
+ userConfig,
87
+ });
88
+
89
+ const getUserFromXFriggHeaders = new GetUserFromXFriggHeaders({
90
+ userRepository,
91
+ userConfig,
92
+ });
93
+
94
+ const getUserFromAdopterJwt = new GetUserFromAdopterJwt({
95
+ userRepository,
96
+ userConfig,
97
+ });
99
98
 
100
99
  const authenticateWithSharedSecret = new AuthenticateWithSharedSecret();
101
100
 
@@ -109,7 +108,8 @@ function createIntegrationRouter() {
109
108
 
110
109
  const moduleFactory = new ModuleFactory({
111
110
  moduleRepository,
112
- moduleDefinitions,
111
+ moduleDefinitions:
112
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
113
113
  });
114
114
  const deleteIntegrationForUser = new DeleteIntegrationForUser({
115
115
  integrationRepository,
@@ -122,19 +122,6 @@ function createIntegrationRouter() {
122
122
  integrationClasses,
123
123
  moduleFactory,
124
124
  moduleRepository,
125
- scopePolicy,
126
- });
127
-
128
- // Decides who owns a new integration and which entities the caller may bind
129
- // (the v1 create used to check neither), and who may reach an existing one.
130
- const resolveIntegrationOwnership = new ResolveIntegrationOwnership({
131
- moduleRepository,
132
- integrationClasses,
133
- scopePolicy,
134
- });
135
- const getOwnedIntegrationRecord = new GetOwnedIntegrationRecord({
136
- integrationRepository,
137
- scopePolicy,
138
125
  });
139
126
 
140
127
  const getCredentialForUser = new GetCredentialForUser({
@@ -149,8 +136,8 @@ function createIntegrationRouter() {
149
136
 
150
137
  const getEntitiesForUser = new GetEntitiesForUser({
151
138
  moduleRepository,
152
- moduleDefinitions,
153
- scopePolicy,
139
+ moduleDefinitions:
140
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
154
141
  });
155
142
 
156
143
  const getIntegrationInstance = new GetIntegrationInstance({
@@ -166,65 +153,54 @@ function createIntegrationRouter() {
166
153
  });
167
154
 
168
155
  const getModuleInstanceFromType = new GetModuleInstanceFromType({
169
- moduleDefinitions,
156
+ moduleDefinitions:
157
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
170
158
  });
171
159
 
172
160
  const getEntityOptionsByType = new GetEntityOptionsByType({
173
- moduleDefinitions,
161
+ moduleDefinitions:
162
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
174
163
  });
175
164
 
176
165
  const testModuleAuth = new TestModuleAuth({
177
166
  moduleRepository,
178
- moduleDefinitions,
179
- scopePolicy,
167
+ moduleDefinitions:
168
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
180
169
  });
181
170
 
182
171
  const getModule = new GetModule({
183
172
  moduleRepository,
184
- moduleDefinitions,
185
- scopePolicy,
173
+ moduleDefinitions:
174
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
186
175
  });
187
176
 
188
177
  const getEntityOptionsById = new GetEntityOptionsById({
189
178
  moduleRepository,
190
- moduleDefinitions,
191
- scopePolicy,
179
+ moduleDefinitions:
180
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
192
181
  });
193
182
 
194
183
  const refreshEntityOptions = new RefreshEntityOptions({
195
184
  moduleRepository,
196
- moduleDefinitions,
197
- scopePolicy,
185
+ moduleDefinitions:
186
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
198
187
  });
199
188
 
200
189
  const getPossibleIntegrations = new GetPossibleIntegrations({
201
190
  integrationClasses,
202
191
  });
203
192
 
204
- // The owner of what an authorization creates follows its scope (ADR-056 §3.2).
205
193
  const processAuthorizationCallback = new ProcessAuthorizationCallback({
206
194
  moduleRepository,
207
195
  credentialRepository,
208
196
  integrationRepository,
209
- moduleDefinitions,
210
- });
211
- const authorizeEntityForUser = new AuthorizeEntityForUser({
212
- scopePolicy,
213
- processAuthorizationCallback,
214
- });
215
- // v1 POST /api/entity: same owner resolution as /api/authorize.
216
- const createEntityFromCredential = new CreateEntityFromCredential({
217
- authorizeEntityForUser,
218
- processAuthorizationCallback,
219
- credentialRepository,
220
- moduleDefinitions,
197
+ moduleDefinitions:
198
+ getModulesDefinitionFromIntegrationClasses(integrationClasses),
221
199
  });
222
200
 
223
201
  const router = express();
224
202
 
225
203
  setIntegrationRoutes(router, authenticateUser, {
226
- resolveIntegrationOwnership,
227
- getOwnedIntegrationRecord,
228
204
  createIntegration,
229
205
  deleteIntegrationForUser,
230
206
  getIntegrationsForUser,
@@ -241,8 +217,7 @@ function createIntegrationRouter() {
241
217
  getModule,
242
218
  getEntityOptionsById,
243
219
  refreshEntityOptions,
244
- authorizeEntityForUser,
245
- createEntityFromCredential,
220
+ processAuthorizationCallback,
246
221
  });
247
222
  return router;
248
223
  }
@@ -279,8 +254,6 @@ function checkRequiredParams(params, requiredKeys) {
279
254
  */
280
255
  function setIntegrationRoutes(router, authenticateUser, useCases) {
281
256
  const {
282
- resolveIntegrationOwnership,
283
- getOwnedIntegrationRecord,
284
257
  createIntegration,
285
258
  deleteIntegrationForUser,
286
259
  getIntegrationsForUser,
@@ -289,22 +262,15 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
289
262
  updateIntegration,
290
263
  getPossibleIntegrations,
291
264
  } = useCases;
292
-
293
- // The owner of an integration the caller may reach (404 otherwise), which is
294
- // what the id-and-owner use cases below expect. `manage: true` is for
295
- // changing or deleting: an organization's integration needs an admin (403).
296
- const ownerOf = async (integrationId, user, options) =>
297
- (await getOwnedIntegrationRecord.execute(integrationId, user, options))
298
- .userId;
299
-
300
265
  router.route('/api/integrations').get(
301
266
  catchAsyncError(async (req, res) => {
302
267
  const user = await authenticateUser.execute(req);
303
- const integrations = await getIntegrationsForUser.execute(user);
268
+ const userId = user.getId();
269
+ const integrations = await getIntegrationsForUser.execute(userId);
304
270
  const results = {
305
271
  entities: {
306
272
  options: await getPossibleIntegrations.execute(),
307
- authorized: await getEntitiesForUser.execute(user),
273
+ authorized: await getEntitiesForUser.execute(userId),
308
274
  },
309
275
  integrations: integrations,
310
276
  };
@@ -316,6 +282,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
316
282
  router.route('/api/integrations').post(
317
283
  catchAsyncError(async (req, res) => {
318
284
  const user = await authenticateUser.execute(req);
285
+ const userId = user.getId();
319
286
  const params = checkRequiredParams(req.body, [
320
287
  'entities',
321
288
  'config',
@@ -323,17 +290,10 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
323
290
 
324
291
  get(params.config, 'type');
325
292
 
326
- const { ownerId, organizationId, adoptUnscoped } =
327
- await resolveIntegrationOwnership.execute({
328
- entityIds: params.entities,
329
- config: params.config,
330
- user,
331
- });
332
293
  const integration = await createIntegration.execute(
333
294
  params.entities,
334
- ownerId,
335
- params.config,
336
- { organizationId, adoptUnscoped }
295
+ userId,
296
+ params.config
337
297
  );
338
298
 
339
299
  res.status(201).json(integration);
@@ -343,11 +303,12 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
343
303
  router.route('/api/integrations/:integrationId').patch(
344
304
  catchAsyncError(async (req, res) => {
345
305
  const user = await authenticateUser.execute(req);
306
+ const userId = user.getId();
346
307
  const params = checkRequiredParams(req.body, ['config']);
347
308
 
348
309
  const integration = await updateIntegration.execute(
349
310
  req.params.integrationId,
350
- await ownerOf(req.params.integrationId, user, { manage: true }),
311
+ userId,
351
312
  params.config
352
313
  );
353
314
  res.json(integration);
@@ -360,7 +321,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
360
321
  const params = checkRequiredParams(req.params, ['integrationId']);
361
322
  await deleteIntegrationForUser.execute(
362
323
  params.integrationId,
363
- await ownerOf(params.integrationId, user, { manage: true })
324
+ user.getId()
364
325
  );
365
326
  res.status(204).json({});
366
327
  })
@@ -372,7 +333,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
372
333
  const params = checkRequiredParams(req.params, ['integrationId']);
373
334
  const integration = await getIntegrationInstance.execute(
374
335
  params.integrationId,
375
- await ownerOf(params.integrationId, user)
336
+ user.getId()
376
337
  );
377
338
  res.json(await integration.send('GET_CONFIG_OPTIONS'));
378
339
  })
@@ -388,7 +349,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
388
349
  ]);
389
350
  const integration = await getIntegrationInstance.execute(
390
351
  params.integrationId,
391
- await ownerOf(params.integrationId, user)
352
+ user.getId()
392
353
  );
393
354
 
394
355
  res.json(
@@ -402,7 +363,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
402
363
  const params = checkRequiredParams(req.params, ['integrationId']);
403
364
  const integration = await getIntegrationInstance.execute(
404
365
  params.integrationId,
405
- await ownerOf(params.integrationId, user)
366
+ user.getId()
406
367
  );
407
368
  res.json(await integration.send('GET_USER_ACTIONS', req.body));
408
369
  })
@@ -419,7 +380,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
419
380
  ]);
420
381
  const integration = await getIntegrationInstance.execute(
421
382
  params.integrationId,
422
- await ownerOf(params.integrationId, user)
383
+ user.getId()
423
384
  );
424
385
 
425
386
  res.json(
@@ -444,7 +405,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
444
405
  ]);
445
406
  const integration = await getIntegrationInstance.execute(
446
407
  params.integrationId,
447
- await ownerOf(params.integrationId, user)
408
+ user.getId()
448
409
  );
449
410
 
450
411
  res.json(
@@ -465,7 +426,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
465
426
  ]);
466
427
  const integration = await getIntegrationInstance.execute(
467
428
  params.integrationId,
468
- await ownerOf(params.integrationId, user)
429
+ user.getId()
469
430
  );
470
431
  res.json(await integration.send(params.actionId, req.body));
471
432
  })
@@ -482,7 +443,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
482
443
  const params = checkRequiredParams(req.params, ['integrationId']);
483
444
  const integration = await getIntegrationInstance.execute(
484
445
  params.integrationId,
485
- await ownerOf(params.integrationId, user)
446
+ user.getId()
486
447
  );
487
448
 
488
449
  // We could perhaps augment router with dynamic options? Haven't decided yet, but here may be the place
@@ -502,7 +463,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
502
463
  const params = checkRequiredParams(req.params, ['integrationId']);
503
464
  const instance = await getIntegrationInstance.execute(
504
465
  params.integrationId,
505
- await ownerOf(params.integrationId, user)
466
+ user.getId()
506
467
  );
507
468
 
508
469
  if (!instance) {
@@ -540,21 +501,16 @@ function setEntityRoutes(router, authenticateUser, useCases) {
540
501
  getModule,
541
502
  getEntityOptionsById,
542
503
  refreshEntityOptions,
543
- authorizeEntityForUser,
544
- createEntityFromCredential,
504
+ processAuthorizationCallback,
545
505
  } = useCases;
546
506
 
547
507
  router.route('/api/authorize').get(
548
508
  catchAsyncError(async (req, res) => {
549
509
  const user = await authenticateUser.execute(req);
510
+ const userId = user.getId();
550
511
  const params = checkRequiredParams(req.query, ['entityType']);
551
- // The owner POST stores under, so GET and POST describe one connection.
552
- const { ownerId } = authorizeEntityForUser.resolveOwnership({
553
- user,
554
- entityType: params.entityType,
555
- });
556
512
  const module = await getModuleInstanceFromType.execute(
557
- ownerId,
513
+ userId,
558
514
  params.entityType,
559
515
  { state: req.query.state }
560
516
  );
@@ -573,15 +529,11 @@ function setEntityRoutes(router, authenticateUser, useCases) {
573
529
  router.route('/api/authorize').post(
574
530
  catchAsyncError(async (req, res) => {
575
531
  const user = await authenticateUser.execute(req);
532
+ const userId = user.getId();
576
533
  const params = checkRequiredParams(req.body, [
577
534
  'entityType',
578
535
  'data',
579
536
  ]);
580
- const ownership = authorizeEntityForUser.resolveOwnership({
581
- user,
582
- entityType: params.entityType,
583
- });
584
- const userId = ownership.ownerId;
585
537
 
586
538
  const dataKeys =
587
539
  params.data && typeof params.data === 'object'
@@ -593,13 +545,11 @@ function setEntityRoutes(router, authenticateUser, useCases) {
593
545
  dataKeys,
594
546
  });
595
547
 
596
- const { result: entityDetails } =
597
- await authorizeEntityForUser.execute({
598
- user,
599
- entityType: params.entityType,
600
- data: params.data,
601
- ownership,
602
- });
548
+ const entityDetails = await processAuthorizationCallback.execute(
549
+ userId,
550
+ params.entityType,
551
+ params.data
552
+ );
603
553
 
604
554
  log.info('Entity authorized', {
605
555
  eventName: 'frigg.integrations.authorized',
@@ -616,20 +566,35 @@ function setEntityRoutes(router, authenticateUser, useCases) {
616
566
  router.route('/api/entity').post(
617
567
  catchAsyncError(async (req, res) => {
618
568
  const user = await authenticateUser.execute(req);
569
+ const userId = user.getId();
619
570
  const params = checkRequiredParams(req.body, [
620
571
  'entityType',
621
572
  'data',
622
573
  ]);
623
574
  checkRequiredParams(req.body.data, ['credential_id']);
624
575
 
625
- // The owner follows the entity type's scope, as /api/authorize.
626
- res.json(
627
- await createEntityFromCredential.execute({
628
- user,
629
- entityType: params.entityType,
630
- credentialId: params.data.credential_id,
631
- })
576
+ // May want to pass along the user ID as well so credential ID's can't be fished???
577
+ const credential = await getCredentialForUser.execute(
578
+ params.data.credential_id,
579
+ userId
632
580
  );
581
+
582
+ if (!credential) {
583
+ throw Boom.badRequest('Invalid credential ID');
584
+ }
585
+
586
+ const module = await getModuleInstanceFromType.execute(
587
+ userId,
588
+ params.entityType
589
+ );
590
+ const entityDetails = await module.getEntityDetails(
591
+ module.api,
592
+ null,
593
+ null,
594
+ userId
595
+ );
596
+
597
+ res.json(await module.findOrCreateEntity(entityDetails));
633
598
  })
634
599
  );
635
600
 
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { MappingAlreadyExistsError } = require('../../errors');
2
3
  const {
3
4
  toObjectId,
4
5
  fromObjectId,
@@ -180,6 +181,55 @@ class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositor
180
181
  return this._mapMapping(decryptedMapping);
181
182
  }
182
183
 
184
+ async createMapping(integrationId, sourceId, mapping) {
185
+ const now = new Date();
186
+ const encryptedDocument = await this.encryptionService.encryptFields(
187
+ 'IntegrationMapping',
188
+ {
189
+ integrationId,
190
+ sourceId:
191
+ sourceId === null || sourceId === undefined
192
+ ? null
193
+ : String(sourceId),
194
+ mapping,
195
+ createdAt: now,
196
+ updatedAt: now,
197
+ }
198
+ );
199
+
200
+ let insertedId;
201
+ try {
202
+ insertedId = await insertOne(
203
+ this.prisma,
204
+ 'IntegrationMapping',
205
+ encryptedDocument
206
+ );
207
+ } catch (error) {
208
+ if (error.code === 11000) {
209
+ throw new MappingAlreadyExistsError(integrationId, sourceId, {
210
+ cause: error,
211
+ });
212
+ }
213
+ throw error;
214
+ }
215
+
216
+ const created = await findOne(this.prisma, 'IntegrationMapping', {
217
+ _id: insertedId,
218
+ });
219
+ if (!created) {
220
+ throw new Error(
221
+ 'Failed to create mapping: Document not found after insert. ' +
222
+ 'This indicates a database consistency issue.'
223
+ );
224
+ }
225
+ return this._mapMapping(
226
+ await this.encryptionService.decryptFields(
227
+ 'IntegrationMapping',
228
+ created
229
+ )
230
+ );
231
+ }
232
+
183
233
  async findMappingsByIntegration(integrationId) {
184
234
  const filter = {};
185
235
  if (integrationId) filter.integrationId = integrationId;
@@ -39,6 +39,21 @@ class IntegrationMappingRepositoryInterface {
39
39
  throw new Error('Method upsertMapping must be implemented by subclass');
40
40
  }
41
41
 
42
+ /**
43
+ * Insert a mapping, failing if one already exists for the
44
+ * (integrationId, sourceId) pair. Atomic: usable as a claim/lock row.
45
+ *
46
+ * @param {string|number} integrationId - The integration ID
47
+ * @param {string} sourceId - The source ID for lookup
48
+ * @param {Object} mapping - The mapping data
49
+ * @returns {Promise<Object>} The created mapping document
50
+ * @throws {MappingAlreadyExistsError} If the pair already exists
51
+ * @abstract
52
+ */
53
+ async createMapping(integrationId, sourceId, mapping) {
54
+ throw new Error('createMapping must be implemented by subclass');
55
+ }
56
+
42
57
  /**
43
58
  * Find all mappings for an integration
44
59
  *
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { MappingAlreadyExistsError } = require('../../errors');
2
3
  const {
3
4
  assertMappingWrittenUnencrypted,
4
5
  decryptQueriedMappings,
@@ -93,6 +94,34 @@ class IntegrationMappingRepositoryMongo extends IntegrationMappingRepositoryInte
93
94
  });
94
95
  }
95
96
 
97
+ /**
98
+ * Insert a mapping; fails if (integrationId, sourceId) already exists
99
+ *
100
+ * @param {string} integrationId - The integration ID
101
+ * @param {string} sourceId - The source ID
102
+ * @param {Object} mapping - The mapping data
103
+ * @returns {Promise<Object>} The created mapping document
104
+ * @throws {MappingAlreadyExistsError} If the pair already exists
105
+ */
106
+ async createMapping(integrationId, sourceId, mapping) {
107
+ try {
108
+ return await this.prisma.integrationMapping.create({
109
+ data: {
110
+ integrationId,
111
+ sourceId: this._toString(sourceId),
112
+ mapping,
113
+ },
114
+ });
115
+ } catch (error) {
116
+ if (error.code === 'P2002') {
117
+ throw new MappingAlreadyExistsError(integrationId, sourceId, {
118
+ cause: error,
119
+ });
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+
96
125
  /**
97
126
  * Find all mappings for an integration
98
127
  * Replaces: IntegrationMapping.find({ integration: integrationId })
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { MappingAlreadyExistsError } = require('../../errors');
2
3
  const {
3
4
  assertMappingWrittenUnencrypted,
4
5
  decryptQueriedMappings,
@@ -150,6 +151,35 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
150
151
  return this._convertMappingIds(result);
151
152
  }
152
153
 
154
+ /**
155
+ * Insert a mapping; fails if (integrationId, sourceId) already exists
156
+ *
157
+ * @param {string} integrationId - The integration ID
158
+ * @param {string} sourceId - The source ID
159
+ * @param {Object} mapping - The mapping data
160
+ * @returns {Promise<Object>} The created mapping document
161
+ * @throws {MappingAlreadyExistsError} If the pair already exists
162
+ */
163
+ async createMapping(integrationId, sourceId, mapping) {
164
+ try {
165
+ const created = await this.prisma.integrationMapping.create({
166
+ data: {
167
+ integrationId: this._stringToInt(integrationId),
168
+ sourceId: this._toString(sourceId),
169
+ mapping,
170
+ },
171
+ });
172
+ return this._convertMappingIds(created);
173
+ } catch (error) {
174
+ if (error.code === 'P2002') {
175
+ throw new MappingAlreadyExistsError(integrationId, sourceId, {
176
+ cause: error,
177
+ });
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+
153
183
  /**
154
184
  * Find all mappings for an integration
155
185
  * Replaces: IntegrationMapping.find({ integration: integrationId })
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { MappingAlreadyExistsError } = require('../../errors');
2
3
  const {
3
4
  IntegrationMappingRepositoryInterface,
4
5
  } = require('./integration-mapping-repository-interface');
@@ -69,6 +70,34 @@ class IntegrationMappingRepository extends IntegrationMappingRepositoryInterface
69
70
  });
70
71
  }
71
72
 
73
+ /**
74
+ * Insert a mapping; fails if (integrationId, sourceId) already exists
75
+ *
76
+ * @param {string} integrationId - The integration ID
77
+ * @param {string} sourceId - The source ID
78
+ * @param {Object} mapping - The mapping data
79
+ * @returns {Promise<Object>} The created mapping document
80
+ * @throws {MappingAlreadyExistsError} If the pair already exists
81
+ */
82
+ async createMapping(integrationId, sourceId, mapping) {
83
+ try {
84
+ return await this.prisma.integrationMapping.create({
85
+ data: {
86
+ integrationId,
87
+ sourceId,
88
+ mapping,
89
+ },
90
+ });
91
+ } catch (error) {
92
+ if (error.code === 'P2002') {
93
+ throw new MappingAlreadyExistsError(integrationId, sourceId, {
94
+ cause: error,
95
+ });
96
+ }
97
+ throw error;
98
+ }
99
+ }
100
+
72
101
  /**
73
102
  * Find all mappings for an integration
74
103
  * Replaces: IntegrationMapping.find({ integration: integrationId })