@friggframework/core 2.0.0--canary.680.daae24e.0 → 2.0.0--canary.686.e94de4d.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 (156) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/CLAUDE.md +28 -0
  3. package/README.md +105 -0
  4. package/application/commands/integration-commands.js +2 -2
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/core/Worker.js +31 -7
  8. package/core/create-handler.js +9 -0
  9. package/credential/use-cases/credential-operations-for-user.js +110 -25
  10. package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
  11. package/database/config.js +48 -24
  12. package/database/documentdb-utils.js +6 -1
  13. package/database/encryption/README.md +0 -2
  14. package/database/encryption/encryption-config.js +10 -31
  15. package/database/encryption/encryption-schema-registry.js +99 -47
  16. package/database/encryption/field-encryption-service.js +5 -69
  17. package/database/organization-where.js +18 -0
  18. package/database/prisma.js +4 -2
  19. package/database/unique-violation.js +25 -0
  20. package/extensions/bootstrap-runner.js +43 -0
  21. package/extensions/extension-loader.js +187 -0
  22. package/extensions/index.js +41 -0
  23. package/extensions/initialize-app.js +123 -0
  24. package/extensions/route-mounter.js +103 -0
  25. package/extensions/schema-composer.js +188 -0
  26. package/generated/prisma-mongodb/edge.js +27 -4
  27. package/generated/prisma-mongodb/index-browser.js +24 -1
  28. package/generated/prisma-mongodb/index.d.ts +3227 -47
  29. package/generated/prisma-mongodb/index.js +27 -4
  30. package/generated/prisma-mongodb/package.json +1 -1
  31. package/generated/prisma-mongodb/schema.prisma +65 -0
  32. package/generated/prisma-mongodb/wasm.js +27 -4
  33. package/generated/prisma-postgresql/edge.js +27 -4
  34. package/generated/prisma-postgresql/index-browser.js +24 -1
  35. package/generated/prisma-postgresql/index.d.ts +3519 -67
  36. package/generated/prisma-postgresql/index.js +27 -4
  37. package/generated/prisma-postgresql/package.json +1 -1
  38. package/generated/prisma-postgresql/schema.prisma +61 -0
  39. package/generated/prisma-postgresql/wasm.js +27 -4
  40. package/handlers/app-definition-loader.js +103 -12
  41. package/handlers/routers/auth.js +40 -12
  42. package/handlers/routers/health.js +4 -2
  43. package/handlers/routers/integration-defined-routers.js +144 -92
  44. package/handlers/routers/integration-webhook-routers.js +102 -64
  45. package/index.js +63 -1
  46. package/infrastructure/scheduler/index.js +19 -6
  47. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  48. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  49. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  50. package/integrations/index.js +5 -0
  51. package/integrations/integration-router.js +115 -73
  52. package/integrations/repositories/integration-not-found.js +14 -0
  53. package/integrations/repositories/integration-repository-documentdb.js +35 -3
  54. package/integrations/repositories/integration-repository-interface.js +17 -1
  55. package/integrations/repositories/integration-repository-mongo.js +59 -76
  56. package/integrations/repositories/integration-repository-postgres.js +62 -76
  57. package/integrations/schedule-config.js +80 -0
  58. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  59. package/integrations/use-cases/create-integration.js +55 -20
  60. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  61. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  62. package/integrations/use-cases/index.js +7 -1
  63. package/integrations/use-cases/integration-operations-for-user.js +55 -24
  64. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  65. package/integrations/utils/map-integration-dto.js +44 -1
  66. package/management-api/management-api-config.js +71 -2
  67. package/management-api/meta-router.js +21 -2
  68. package/management-api/openapi/components.js +40 -0
  69. package/management-api/openapi/v2-operations.js +16 -4
  70. package/management-api/route-registry.js +123 -6
  71. package/management-api/v2/dependencies.js +40 -15
  72. package/management-api/v2/dto.js +20 -0
  73. package/management-api/v2/handlers/authorization.js +1 -0
  74. package/management-api/v2/handlers/credentials.js +6 -2
  75. package/management-api/v2/handlers/entities.js +10 -7
  76. package/management-api/v2/handlers/integrations.js +6 -3
  77. package/management-api/v2/request.js +32 -1
  78. package/modules/entity-scope.js +15 -0
  79. package/modules/index.js +8 -0
  80. package/modules/module-factory.js +12 -0
  81. package/modules/module.js +53 -2
  82. package/modules/repositories/entity-not-found.js +18 -0
  83. package/modules/repositories/module-repository-documentdb.js +51 -6
  84. package/modules/repositories/module-repository-interface.js +19 -2
  85. package/modules/repositories/module-repository-mongo.js +67 -74
  86. package/modules/repositories/module-repository-postgres.js +66 -74
  87. package/modules/repositories/module-repository.js +2 -1
  88. package/modules/scope-config.js +192 -0
  89. package/modules/scope-policy.js +112 -0
  90. package/modules/scope-resolution.js +288 -0
  91. package/modules/scope-visibility.js +247 -0
  92. package/modules/tests/doubles/ownership-matching.js +18 -0
  93. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  94. package/modules/use-cases/create-entity-from-credential.js +113 -0
  95. package/modules/use-cases/entity-operations-for-user.js +50 -13
  96. package/modules/use-cases/entity-types.js +31 -7
  97. package/modules/use-cases/execute-entity-proxy-request.js +50 -3
  98. package/modules/use-cases/get-authorization-requirements.js +2 -2
  99. package/modules/use-cases/get-entities-for-user.js +26 -13
  100. package/modules/use-cases/get-entity-module-for-user.js +34 -7
  101. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  102. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  103. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  104. package/modules/use-cases/get-module.js +10 -4
  105. package/modules/use-cases/manage-access.js +21 -0
  106. package/modules/use-cases/multi-step-authorization.js +40 -13
  107. package/modules/use-cases/process-authorization-callback.js +53 -19
  108. package/modules/use-cases/refresh-entity-options.js +10 -4
  109. package/modules/use-cases/test-module-auth.js +10 -4
  110. package/package.json +6 -5
  111. package/prisma-mongodb/schema.prisma +65 -0
  112. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  113. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  114. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  115. package/prisma-postgresql/schema.prisma +61 -0
  116. package/providers/index.js +23 -0
  117. package/providers/resolve-provider.js +93 -0
  118. package/providers/runtime-detection.js +124 -0
  119. package/queues/index.js +25 -0
  120. package/queues/providers/index.js +11 -0
  121. package/queues/providers/netlify-background-provider.js +150 -0
  122. package/queues/providers/qstash-queue-provider.js +219 -0
  123. package/queues/providers/sqs-queue-provider.js +70 -0
  124. package/queues/queue-client-interface.js +55 -0
  125. package/queues/queue-message-auth.js +193 -0
  126. package/queues/queue-provider-factory.js +101 -0
  127. package/queues/queue-provider.js +65 -0
  128. package/queues/sqs-queue-client.js +81 -0
  129. package/types/index.d.ts +1 -0
  130. package/types/modules/index.d.ts +139 -0
  131. package/types/user/index.d.ts +134 -0
  132. package/user/adopter-jwt-config.js +177 -0
  133. package/user/adopter-jwt-errors.js +59 -0
  134. package/user/adopter-token-verifier.js +122 -0
  135. package/user/app-user-helpers.js +97 -0
  136. package/user/create-user-authentication.js +64 -0
  137. package/user/membership.js +51 -0
  138. package/user/repositories/organization-membership-record.js +19 -0
  139. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  140. package/user/repositories/organization-membership-repository-factory.js +42 -0
  141. package/user/repositories/organization-membership-repository-interface.js +57 -0
  142. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  143. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  144. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  145. package/user/repositories/user-repository-documentdb.js +27 -2
  146. package/user/repositories/user-repository-interface.js +18 -0
  147. package/user/repositories/user-repository-mongo.js +24 -0
  148. package/user/repositories/user-repository-postgres.js +18 -0
  149. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  150. package/user/use-cases/authenticate-user.js +7 -1
  151. package/user/use-cases/ensure-organization-membership.js +92 -0
  152. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  153. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  154. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  155. package/user/user.js +64 -1
  156. package/database/encryption/test-support/fake-ciphertext.js +0 -15
@@ -51,22 +51,27 @@ const {
51
51
  const {
52
52
  createUserRepository,
53
53
  } = require('../user/repositories/user-repository-factory');
54
- const {
55
- GetUserFromBearerToken,
56
- } = require('../user/use-cases/get-user-from-bearer-token');
57
- const {
58
- GetUserFromXFriggHeaders,
59
- } = require('../user/use-cases/get-user-from-x-frigg-headers');
60
- const {
61
- GetUserFromAdopterJwt,
62
- } = require('../user/use-cases/get-user-from-adopter-jwt');
63
54
  const {
64
55
  AuthenticateWithSharedSecret,
65
56
  } = require('../user/use-cases/authenticate-with-shared-secret');
66
57
  const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
58
+ const {
59
+ buildUserAuthentication,
60
+ } = require('../user/create-user-authentication');
67
61
  const {
68
62
  ProcessAuthorizationCallback,
69
63
  } = 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');
70
75
 
71
76
  const log = getLogger('frigg.integrations');
72
77
 
@@ -77,26 +82,20 @@ function createIntegrationRouter() {
77
82
  integrations: integrationClasses,
78
83
  userConfig,
79
84
  moduleDefinitions,
85
+ ownership,
86
+ entityTypeRegistry,
80
87
  } = loadAppDefinition();
88
+ // Ownership by scope (ADR-056 §3): one object every use case asks.
89
+ const scopePolicy = createScopePolicy({ ownership, entityTypeRegistry });
81
90
  const moduleRepository = createModuleRepository();
82
91
  const integrationRepository = createIntegrationRepository();
83
92
  const credentialRepository = createCredentialRepository();
84
93
  const userRepository = createUserRepository();
85
-
86
- const getUserFromBearerToken = new GetUserFromBearerToken({
87
- userRepository,
88
- userConfig,
89
- });
90
-
91
- const getUserFromXFriggHeaders = new GetUserFromXFriggHeaders({
92
- userRepository,
93
- userConfig,
94
- });
95
-
96
- const getUserFromAdopterJwt = new GetUserFromAdopterJwt({
97
- userRepository,
98
- userConfig,
99
- });
94
+ const {
95
+ getUserFromBearerToken,
96
+ getUserFromXFriggHeaders,
97
+ getUserFromAdopterJwt,
98
+ } = buildUserAuthentication({ userConfig, userRepository });
100
99
 
101
100
  const authenticateWithSharedSecret = new AuthenticateWithSharedSecret();
102
101
 
@@ -123,6 +122,19 @@ function createIntegrationRouter() {
123
122
  integrationClasses,
124
123
  moduleFactory,
125
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,
126
138
  });
127
139
 
128
140
  const getCredentialForUser = new GetCredentialForUser({
@@ -138,6 +150,7 @@ function createIntegrationRouter() {
138
150
  const getEntitiesForUser = new GetEntitiesForUser({
139
151
  moduleRepository,
140
152
  moduleDefinitions,
153
+ scopePolicy,
141
154
  });
142
155
 
143
156
  const getIntegrationInstance = new GetIntegrationInstance({
@@ -163,37 +176,55 @@ function createIntegrationRouter() {
163
176
  const testModuleAuth = new TestModuleAuth({
164
177
  moduleRepository,
165
178
  moduleDefinitions,
179
+ scopePolicy,
166
180
  });
167
181
 
168
182
  const getModule = new GetModule({
169
183
  moduleRepository,
170
184
  moduleDefinitions,
185
+ scopePolicy,
171
186
  });
172
187
 
173
188
  const getEntityOptionsById = new GetEntityOptionsById({
174
189
  moduleRepository,
175
190
  moduleDefinitions,
191
+ scopePolicy,
176
192
  });
177
193
 
178
194
  const refreshEntityOptions = new RefreshEntityOptions({
179
195
  moduleRepository,
180
196
  moduleDefinitions,
197
+ scopePolicy,
181
198
  });
182
199
 
183
200
  const getPossibleIntegrations = new GetPossibleIntegrations({
184
201
  integrationClasses,
185
202
  });
186
203
 
204
+ // The owner of what an authorization creates follows its scope (ADR-056 §3.2).
187
205
  const processAuthorizationCallback = new ProcessAuthorizationCallback({
188
206
  moduleRepository,
189
207
  credentialRepository,
190
208
  integrationRepository,
191
209
  moduleDefinitions,
192
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,
221
+ });
193
222
 
194
223
  const router = express();
195
224
 
196
225
  setIntegrationRoutes(router, authenticateUser, {
226
+ resolveIntegrationOwnership,
227
+ getOwnedIntegrationRecord,
197
228
  createIntegration,
198
229
  deleteIntegrationForUser,
199
230
  getIntegrationsForUser,
@@ -210,7 +241,8 @@ function createIntegrationRouter() {
210
241
  getModule,
211
242
  getEntityOptionsById,
212
243
  refreshEntityOptions,
213
- processAuthorizationCallback,
244
+ authorizeEntityForUser,
245
+ createEntityFromCredential,
214
246
  });
215
247
  return router;
216
248
  }
@@ -247,6 +279,8 @@ function checkRequiredParams(params, requiredKeys) {
247
279
  */
248
280
  function setIntegrationRoutes(router, authenticateUser, useCases) {
249
281
  const {
282
+ resolveIntegrationOwnership,
283
+ getOwnedIntegrationRecord,
250
284
  createIntegration,
251
285
  deleteIntegrationForUser,
252
286
  getIntegrationsForUser,
@@ -255,15 +289,22 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
255
289
  updateIntegration,
256
290
  getPossibleIntegrations,
257
291
  } = 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
+
258
300
  router.route('/api/integrations').get(
259
301
  catchAsyncError(async (req, res) => {
260
302
  const user = await authenticateUser.execute(req);
261
- const userId = user.getId();
262
- const integrations = await getIntegrationsForUser.execute(userId);
303
+ const integrations = await getIntegrationsForUser.execute(user);
263
304
  const results = {
264
305
  entities: {
265
306
  options: await getPossibleIntegrations.execute(),
266
- authorized: await getEntitiesForUser.execute(userId),
307
+ authorized: await getEntitiesForUser.execute(user),
267
308
  },
268
309
  integrations: integrations,
269
310
  };
@@ -275,7 +316,6 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
275
316
  router.route('/api/integrations').post(
276
317
  catchAsyncError(async (req, res) => {
277
318
  const user = await authenticateUser.execute(req);
278
- const userId = user.getId();
279
319
  const params = checkRequiredParams(req.body, [
280
320
  'entities',
281
321
  'config',
@@ -283,10 +323,17 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
283
323
 
284
324
  get(params.config, 'type');
285
325
 
326
+ const { ownerId, organizationId, adoptUnscoped } =
327
+ await resolveIntegrationOwnership.execute({
328
+ entityIds: params.entities,
329
+ config: params.config,
330
+ user,
331
+ });
286
332
  const integration = await createIntegration.execute(
287
333
  params.entities,
288
- userId,
289
- params.config
334
+ ownerId,
335
+ params.config,
336
+ { organizationId, adoptUnscoped }
290
337
  );
291
338
 
292
339
  res.status(201).json(integration);
@@ -296,12 +343,11 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
296
343
  router.route('/api/integrations/:integrationId').patch(
297
344
  catchAsyncError(async (req, res) => {
298
345
  const user = await authenticateUser.execute(req);
299
- const userId = user.getId();
300
346
  const params = checkRequiredParams(req.body, ['config']);
301
347
 
302
348
  const integration = await updateIntegration.execute(
303
349
  req.params.integrationId,
304
- userId,
350
+ await ownerOf(req.params.integrationId, user, { manage: true }),
305
351
  params.config
306
352
  );
307
353
  res.json(integration);
@@ -314,7 +360,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
314
360
  const params = checkRequiredParams(req.params, ['integrationId']);
315
361
  await deleteIntegrationForUser.execute(
316
362
  params.integrationId,
317
- user.getId()
363
+ await ownerOf(params.integrationId, user, { manage: true })
318
364
  );
319
365
  res.status(204).json({});
320
366
  })
@@ -326,7 +372,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
326
372
  const params = checkRequiredParams(req.params, ['integrationId']);
327
373
  const integration = await getIntegrationInstance.execute(
328
374
  params.integrationId,
329
- user.getId()
375
+ await ownerOf(params.integrationId, user)
330
376
  );
331
377
  res.json(await integration.send('GET_CONFIG_OPTIONS'));
332
378
  })
@@ -342,7 +388,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
342
388
  ]);
343
389
  const integration = await getIntegrationInstance.execute(
344
390
  params.integrationId,
345
- user.getId()
391
+ await ownerOf(params.integrationId, user)
346
392
  );
347
393
 
348
394
  res.json(
@@ -356,7 +402,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
356
402
  const params = checkRequiredParams(req.params, ['integrationId']);
357
403
  const integration = await getIntegrationInstance.execute(
358
404
  params.integrationId,
359
- user.getId()
405
+ await ownerOf(params.integrationId, user)
360
406
  );
361
407
  res.json(await integration.send('GET_USER_ACTIONS', req.body));
362
408
  })
@@ -373,7 +419,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
373
419
  ]);
374
420
  const integration = await getIntegrationInstance.execute(
375
421
  params.integrationId,
376
- user.getId()
422
+ await ownerOf(params.integrationId, user)
377
423
  );
378
424
 
379
425
  res.json(
@@ -398,7 +444,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
398
444
  ]);
399
445
  const integration = await getIntegrationInstance.execute(
400
446
  params.integrationId,
401
- user.getId()
447
+ await ownerOf(params.integrationId, user)
402
448
  );
403
449
 
404
450
  res.json(
@@ -419,7 +465,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
419
465
  ]);
420
466
  const integration = await getIntegrationInstance.execute(
421
467
  params.integrationId,
422
- user.getId()
468
+ await ownerOf(params.integrationId, user)
423
469
  );
424
470
  res.json(await integration.send(params.actionId, req.body));
425
471
  })
@@ -436,7 +482,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
436
482
  const params = checkRequiredParams(req.params, ['integrationId']);
437
483
  const integration = await getIntegrationInstance.execute(
438
484
  params.integrationId,
439
- user.getId()
485
+ await ownerOf(params.integrationId, user)
440
486
  );
441
487
 
442
488
  // We could perhaps augment router with dynamic options? Haven't decided yet, but here may be the place
@@ -456,7 +502,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
456
502
  const params = checkRequiredParams(req.params, ['integrationId']);
457
503
  const instance = await getIntegrationInstance.execute(
458
504
  params.integrationId,
459
- user.getId()
505
+ await ownerOf(params.integrationId, user)
460
506
  );
461
507
 
462
508
  if (!instance) {
@@ -494,16 +540,21 @@ function setEntityRoutes(router, authenticateUser, useCases) {
494
540
  getModule,
495
541
  getEntityOptionsById,
496
542
  refreshEntityOptions,
497
- processAuthorizationCallback,
543
+ authorizeEntityForUser,
544
+ createEntityFromCredential,
498
545
  } = useCases;
499
546
 
500
547
  router.route('/api/authorize').get(
501
548
  catchAsyncError(async (req, res) => {
502
549
  const user = await authenticateUser.execute(req);
503
- const userId = user.getId();
504
550
  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
+ });
505
556
  const module = await getModuleInstanceFromType.execute(
506
- userId,
557
+ ownerId,
507
558
  params.entityType,
508
559
  { state: req.query.state }
509
560
  );
@@ -522,11 +573,15 @@ function setEntityRoutes(router, authenticateUser, useCases) {
522
573
  router.route('/api/authorize').post(
523
574
  catchAsyncError(async (req, res) => {
524
575
  const user = await authenticateUser.execute(req);
525
- const userId = user.getId();
526
576
  const params = checkRequiredParams(req.body, [
527
577
  'entityType',
528
578
  'data',
529
579
  ]);
580
+ const ownership = authorizeEntityForUser.resolveOwnership({
581
+ user,
582
+ entityType: params.entityType,
583
+ });
584
+ const userId = ownership.ownerId;
530
585
 
531
586
  const dataKeys =
532
587
  params.data && typeof params.data === 'object'
@@ -538,11 +593,13 @@ function setEntityRoutes(router, authenticateUser, useCases) {
538
593
  dataKeys,
539
594
  });
540
595
 
541
- const entityDetails = await processAuthorizationCallback.execute(
542
- userId,
543
- params.entityType,
544
- params.data
545
- );
596
+ const { result: entityDetails } =
597
+ await authorizeEntityForUser.execute({
598
+ user,
599
+ entityType: params.entityType,
600
+ data: params.data,
601
+ ownership,
602
+ });
546
603
 
547
604
  log.info('Entity authorized', {
548
605
  eventName: 'frigg.integrations.authorized',
@@ -559,35 +616,20 @@ function setEntityRoutes(router, authenticateUser, useCases) {
559
616
  router.route('/api/entity').post(
560
617
  catchAsyncError(async (req, res) => {
561
618
  const user = await authenticateUser.execute(req);
562
- const userId = user.getId();
563
619
  const params = checkRequiredParams(req.body, [
564
620
  'entityType',
565
621
  'data',
566
622
  ]);
567
623
  checkRequiredParams(req.body.data, ['credential_id']);
568
624
 
569
- // May want to pass along the user ID as well so credential ID's can't be fished???
570
- const credential = await getCredentialForUser.execute(
571
- params.data.credential_id,
572
- userId
573
- );
574
-
575
- if (!credential) {
576
- throw Boom.badRequest('Invalid credential ID');
577
- }
578
-
579
- const module = await getModuleInstanceFromType.execute(
580
- userId,
581
- params.entityType
582
- );
583
- const entityDetails = await module.getEntityDetails(
584
- module.api,
585
- null,
586
- null,
587
- userId
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
+ })
588
632
  );
589
-
590
- res.json(await module.findOrCreateEntity(entityDetails));
591
633
  })
592
634
  );
593
635
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The error `findIntegrationById` throws for an id with no row, and the one
3
+ * predicate that recognises it. The adapters build the error here and the
4
+ * use cases match it here, so the message is written once and a missing
5
+ * integration can be told from any other failure without widening a regex.
6
+ */
7
+ const MESSAGE = /^Integration with id .+ not found$/;
8
+
9
+ const integrationNotFoundError = (id) =>
10
+ new Error(`Integration with id ${id} not found`);
11
+
12
+ const isIntegrationNotFound = (error) => MESSAGE.test(error?.message || '');
13
+
14
+ module.exports = { integrationNotFoundError, isIntegrationNotFound };
@@ -14,6 +14,7 @@ const {
14
14
  IntegrationRepositoryInterface,
15
15
  } = require('./integration-repository-interface');
16
16
  const { validateConfigPatch } = require('./config-patch-shared');
17
+ const { integrationNotFoundError } = require('./integration-not-found');
17
18
 
18
19
  class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
19
20
  constructor() {
@@ -28,6 +29,28 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
28
29
  return records.map((doc) => this._mapIntegration(doc));
29
30
  }
30
31
 
32
+ /**
33
+ * Find integrations matching any of the owner/organization pairs, in one
34
+ * query. `organizationId: null` matches rows with no organization (a raw
35
+ * MongoDB `null` also matches a missing field). The caller (a use case)
36
+ * decides which pairs; this only runs them.
37
+ *
38
+ * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
39
+ * @returns {Promise<Array>}
40
+ */
41
+ async findIntegrationsByOwnership(clauses) {
42
+ if (!Array.isArray(clauses) || clauses.length === 0) return [];
43
+ // Drained: a list can hold more than the first cursor batch (~101).
44
+ const records = await findManyDrained(this.prisma, 'Integration', {
45
+ $or: clauses.map(({ userId, organizationId }) => ({
46
+ userId: toObjectId(userId),
47
+ organizationId:
48
+ organizationId === null ? null : toObjectId(organizationId),
49
+ })),
50
+ });
51
+ return records.map((doc) => this._mapIntegration(doc));
52
+ }
53
+
31
54
  async findIntegrations({ type, status } = {}) {
32
55
  const filter = {};
33
56
  if (type) {
@@ -68,11 +91,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
68
91
  async findIntegrationById(id) {
69
92
  const objectId = toObjectId(id);
70
93
  if (!objectId) {
71
- throw new Error(`Integration with id ${id} not found`);
94
+ throw integrationNotFoundError(id);
72
95
  }
73
96
  const doc = await findOne(this.prisma, 'Integration', { _id: objectId });
74
97
  if (!doc) {
75
- throw new Error(`Integration with id ${id} not found`);
98
+ throw integrationNotFoundError(id);
76
99
  }
77
100
  return this._mapIntegration(doc);
78
101
  }
@@ -132,10 +155,18 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
132
155
  return true;
133
156
  }
134
157
 
135
- async createIntegration(entities, userId, config) {
158
+ /**
159
+ * @param {Array<string>} entities - entity ids
160
+ * @param {string} userId - owner (the person, or the organization's user)
161
+ * @param {Object} config - Integration configuration
162
+ * @param {{organizationId?: (string|null)}} [tenancy] - the organization the
163
+ * integration is created in (ADR-056 §3.6)
164
+ */
165
+ async createIntegration(entities, userId, config, tenancy = {}) {
136
166
  const now = new Date();
137
167
  const document = {
138
168
  userId: toObjectId(userId) || null,
169
+ organizationId: toObjectId(tenancy.organizationId) || null,
139
170
  config,
140
171
  version: '0.0.0',
141
172
  status: 'IN_CREATION',
@@ -298,6 +329,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
298
329
  id: fromObjectId(doc?._id),
299
330
  entitiesIds: (doc?.entityIds || []).map((value) => fromObjectId(value)),
300
331
  userId: fromObjectId(doc?.userId),
332
+ organizationId: fromObjectId(doc?.organizationId) ?? null,
301
333
  config: doc?.config ?? null,
302
334
  version: doc?.version ?? null,
303
335
  status: doc?.status ?? null,
@@ -10,6 +10,20 @@
10
10
  * @abstract
11
11
  */
12
12
  class IntegrationRepositoryInterface {
13
+ /**
14
+ * Find integrations matching any of the owner/organization pairs, in one
15
+ * query (ADR-056 §3.6). `organizationId: null` matches rows with no
16
+ * organization. Which pairs is decided by the caller
17
+ * (modules/entity-scope.js), not here.
18
+ *
19
+ * @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
20
+ * @returns {Promise<Array>} Array of integration objects
21
+ * @abstract
22
+ */
23
+ async findIntegrationsByOwnership(clauses) {
24
+ throw new Error('Method findIntegrationsByOwnership must be implemented by subclass');
25
+ }
26
+
13
27
  /**
14
28
  * Find all integrations for a user
15
29
  *
@@ -123,10 +137,12 @@ class IntegrationRepositoryInterface {
123
137
  * @param {Array<string|number>} entities - Array of entity IDs
124
138
  * @param {string|number} userId - User ID
125
139
  * @param {Object} config - Integration configuration
140
+ * @param {{organizationId?: (string|number|null)}} [tenancy] - the organization the
141
+ * integration is created in (ADR-056 §3.6)
126
142
  * @returns {Promise<Object>} Created integration object
127
143
  * @abstract
128
144
  */
129
- async createIntegration(entities, userId, config) {
145
+ async createIntegration(entities, userId, config, tenancy) {
130
146
  throw new Error('Method createIntegration must be implemented by subclass');
131
147
  }
132
148