@friggframework/core 2.0.0--canary.704.79d9a63.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 (207) hide show
  1. package/CLAUDE.md +28 -40
  2. package/README.md +105 -0
  3. package/application/commands/entity-commands.js +60 -18
  4. package/application/commands/integration-commands.js +20 -73
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/application/index.js +6 -2
  8. package/core/Worker.js +31 -7
  9. package/core/create-handler.js +9 -0
  10. package/credential/repositories/credential-repository-documentdb.js +34 -0
  11. package/credential/repositories/credential-repository-interface.js +15 -0
  12. package/credential/repositories/credential-repository-mongo.js +23 -0
  13. package/credential/repositories/credential-repository-postgres.js +28 -0
  14. package/credential/use-cases/credential-operations-for-user.js +202 -0
  15. package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
  16. package/database/config.js +48 -24
  17. package/database/documentdb-encryption-service.js +9 -22
  18. package/database/documentdb-utils.js +2 -0
  19. package/database/encryption/README.md +76 -14
  20. package/database/encryption/documentdb-encryption-service.md +78 -1
  21. package/database/encryption/encryption-config.js +219 -0
  22. package/database/encryption/encryption-schema-registry.js +93 -12
  23. package/database/encryption/integration-mapping-encryption.js +1 -1
  24. package/database/organization-where.js +18 -0
  25. package/database/prisma.js +15 -38
  26. package/database/unique-violation.js +25 -0
  27. package/database/use-cases/check-encryption-health-use-case.js +43 -52
  28. package/errors/index.js +0 -4
  29. package/extensions/bootstrap-runner.js +43 -0
  30. package/extensions/extension-loader.js +187 -0
  31. package/extensions/index.js +41 -0
  32. package/extensions/initialize-app.js +123 -0
  33. package/extensions/route-mounter.js +103 -0
  34. package/extensions/schema-composer.js +188 -0
  35. package/generated/prisma-mongodb/edge.js +43 -4
  36. package/generated/prisma-mongodb/index-browser.js +40 -1
  37. package/generated/prisma-mongodb/index.d.ts +4761 -133
  38. package/generated/prisma-mongodb/index.js +43 -4
  39. package/generated/prisma-mongodb/package.json +1 -1
  40. package/generated/prisma-mongodb/schema.prisma +92 -0
  41. package/generated/prisma-mongodb/wasm.js +43 -4
  42. package/generated/prisma-postgresql/edge.js +43 -4
  43. package/generated/prisma-postgresql/index-browser.js +40 -1
  44. package/generated/prisma-postgresql/index.d.ts +4961 -28
  45. package/generated/prisma-postgresql/index.js +43 -4
  46. package/generated/prisma-postgresql/package.json +1 -1
  47. package/generated/prisma-postgresql/schema.prisma +87 -0
  48. package/generated/prisma-postgresql/wasm.js +43 -4
  49. package/handlers/WEBHOOKS.md +0 -29
  50. package/handlers/app-definition-loader.js +121 -19
  51. package/handlers/app-handler-helpers.js +25 -3
  52. package/handlers/backend-utils.js +31 -25
  53. package/handlers/middleware/admin-auth.js +14 -1
  54. package/handlers/routers/auth.js +46 -10
  55. package/handlers/routers/health.js +13 -4
  56. package/handlers/routers/integration-defined-routers.js +144 -92
  57. package/handlers/routers/integration-webhook-routers.js +102 -64
  58. package/handlers/workers/integration-defined-workers.js +5 -2
  59. package/index.js +65 -3
  60. package/infrastructure/scheduler/index.js +19 -6
  61. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  62. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  63. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  64. package/integrations/index.js +1 -0
  65. package/integrations/integration-base.js +0 -29
  66. package/integrations/integration-router.js +131 -96
  67. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  68. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  71. package/integrations/repositories/integration-mapping-repository.js +0 -29
  72. package/integrations/repositories/integration-not-found.js +14 -0
  73. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  74. package/integrations/repositories/integration-repository-interface.js +17 -31
  75. package/integrations/repositories/integration-repository-mongo.js +60 -129
  76. package/integrations/repositories/integration-repository-postgres.js +62 -134
  77. package/integrations/repositories/process-repository-mongo.js +1 -1
  78. package/integrations/schedule-config.js +80 -0
  79. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  80. package/integrations/use-cases/create-integration.js +72 -75
  81. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  82. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  83. package/integrations/use-cases/index.js +7 -1
  84. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  85. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  86. package/integrations/utils/map-integration-dto.js +43 -14
  87. package/management-api/create-management-api-router.js +49 -0
  88. package/management-api/error-response.js +116 -0
  89. package/management-api/management-api-config.js +151 -0
  90. package/management-api/meta-router.js +76 -0
  91. package/management-api/openapi/build-openapi.js +144 -0
  92. package/management-api/openapi/components.js +408 -0
  93. package/management-api/openapi/v1-operations.js +83 -0
  94. package/management-api/openapi/v2-operations.js +264 -0
  95. package/management-api/route-registry.js +411 -0
  96. package/management-api/test-support/http-client.js +39 -0
  97. package/management-api/use-cases/get-management-api-meta.js +57 -0
  98. package/management-api/v1-deprecation.js +120 -0
  99. package/management-api/v2/create-v2-router.js +85 -0
  100. package/management-api/v2/dependencies.js +281 -0
  101. package/management-api/v2/dto.js +143 -0
  102. package/management-api/v2/handlers/authorization.js +94 -0
  103. package/management-api/v2/handlers/credentials.js +33 -0
  104. package/management-api/v2/handlers/entities.js +77 -0
  105. package/management-api/v2/handlers/integrations.js +128 -0
  106. package/management-api/v2/handlers/proxy.js +34 -0
  107. package/management-api/v2/request.js +85 -0
  108. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  109. package/modules/domain/authorization-session.js +144 -0
  110. package/modules/entity-scope.js +15 -0
  111. package/modules/entity-type-registry.js +311 -0
  112. package/modules/index.js +14 -0
  113. package/modules/module-factory.js +12 -0
  114. package/modules/module.js +53 -2
  115. package/modules/proxy/proxy-errors.js +20 -0
  116. package/modules/proxy/proxy-path.js +179 -0
  117. package/modules/proxy/proxy-policy.js +248 -0
  118. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  119. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  120. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  121. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  122. package/modules/repositories/entity-not-found.js +18 -0
  123. package/modules/repositories/module-repository-documentdb.js +51 -6
  124. package/modules/repositories/module-repository-interface.js +19 -2
  125. package/modules/repositories/module-repository-mongo.js +67 -74
  126. package/modules/repositories/module-repository-postgres.js +66 -74
  127. package/modules/repositories/module-repository.js +2 -1
  128. package/modules/requester/proxy-request.js +244 -0
  129. package/modules/requester/requester.js +27 -2
  130. package/modules/scope-config.js +192 -0
  131. package/modules/scope-policy.js +112 -0
  132. package/modules/scope-resolution.js +288 -0
  133. package/modules/scope-visibility.js +247 -0
  134. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  135. package/modules/tests/doubles/ownership-matching.js +18 -0
  136. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  137. package/modules/use-cases/create-entity-from-credential.js +113 -0
  138. package/modules/use-cases/entity-operations-for-user.js +162 -0
  139. package/modules/use-cases/entity-types.js +139 -0
  140. package/modules/use-cases/execute-entity-proxy-request.js +286 -0
  141. package/modules/use-cases/get-authorization-requirements.js +100 -0
  142. package/modules/use-cases/get-entities-for-user.js +26 -13
  143. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  144. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  145. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  146. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  147. package/modules/use-cases/get-module.js +10 -4
  148. package/modules/use-cases/manage-access.js +21 -0
  149. package/modules/use-cases/multi-step-authorization.js +329 -0
  150. package/modules/use-cases/process-authorization-callback.js +53 -19
  151. package/modules/use-cases/refresh-entity-options.js +10 -4
  152. package/modules/use-cases/test-module-auth.js +10 -4
  153. package/modules/utils/map-module-dto.js +3 -5
  154. package/package.json +6 -5
  155. package/prisma-mongodb/schema.prisma +92 -0
  156. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  157. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  158. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  159. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  160. package/prisma-postgresql/schema.prisma +87 -0
  161. package/providers/index.js +23 -0
  162. package/providers/resolve-provider.js +93 -0
  163. package/providers/runtime-detection.js +124 -0
  164. package/queues/index.js +25 -0
  165. package/queues/providers/index.js +11 -0
  166. package/queues/providers/netlify-background-provider.js +150 -0
  167. package/queues/providers/qstash-queue-provider.js +219 -0
  168. package/queues/providers/sqs-queue-provider.js +70 -0
  169. package/queues/queue-client-interface.js +55 -0
  170. package/queues/queue-message-auth.js +193 -0
  171. package/queues/queue-provider-factory.js +101 -0
  172. package/queues/queue-provider.js +65 -0
  173. package/queues/sqs-queue-client.js +81 -0
  174. package/types/index.d.ts +2 -0
  175. package/types/modules/index.d.ts +197 -0
  176. package/types/user/index.d.ts +134 -0
  177. package/user/adopter-jwt-config.js +177 -0
  178. package/user/adopter-jwt-errors.js +59 -0
  179. package/user/adopter-token-verifier.js +122 -0
  180. package/user/app-user-helpers.js +97 -0
  181. package/user/create-user-authentication.js +64 -0
  182. package/user/membership.js +51 -0
  183. package/user/repositories/organization-membership-record.js +19 -0
  184. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  185. package/user/repositories/organization-membership-repository-factory.js +42 -0
  186. package/user/repositories/organization-membership-repository-interface.js +57 -0
  187. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  188. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  189. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  190. package/user/repositories/user-repository-documentdb.js +27 -2
  191. package/user/repositories/user-repository-interface.js +18 -0
  192. package/user/repositories/user-repository-mongo.js +24 -0
  193. package/user/repositories/user-repository-postgres.js +18 -0
  194. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  195. package/user/tests/doubles/test-user-repository.js +1 -1
  196. package/user/use-cases/authenticate-user.js +7 -1
  197. package/user/use-cases/ensure-organization-membership.js +92 -0
  198. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  199. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  200. package/user/use-cases/get-user-from-bearer-token.js +2 -7
  201. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  202. package/user/user.js +64 -1
  203. package/errors/mapping-already-exists-error.js +0 -14
  204. package/integrations/use-cases/add-entities-to-integration.js +0 -92
  205. package/integrations/use-cases/remove-entities-from-integration.js +0 -70
  206. package/integrations/utils/assert-entity-ids-input.js +0 -42
  207. package/modules/utils/to-credential-id.js +0 -14
@@ -0,0 +1,100 @@
1
+ const {
2
+ SCHEDULE_SCOPES,
3
+ resolveScheduleConfig,
4
+ } = require('../schedule-config');
5
+
6
+ const ONGOING_SYNC_EVENT = 'ONGOING_SYNC';
7
+ const ACTIVE_STATUS = 'ENABLED';
8
+
9
+ /**
10
+ * Dispatches one cron tick of ONGOING_SYNC to the queue worker, per each
11
+ * integration class's `Definition.schedule.scope` (schedule-config.js):
12
+ *
13
+ * - 'class' (default): one message per class, `data: {}`.
14
+ * - 'installation': one message per ENABLED integration record of the class,
15
+ * `data: { integrationId }`, so the worker hydrates that installation;
16
+ * skipped when the definition's `isDue(integration, { now })` says no.
17
+ *
18
+ * Only classes that implement `startOngoingSync` take part. A failure for one
19
+ * class or installation is counted and logged; the rest still go out.
20
+ */
21
+ class DispatchOngoingSyncs {
22
+ /**
23
+ * @param {Object} params
24
+ * @param {Object} params.integrationRepository - findIntegrations({ type, status })
25
+ * @param {Object} params.queueProvider - send(message, queueId)
26
+ * @param {string} params.queueId - the worker the messages go to
27
+ * @param {() => Date} [params.now]
28
+ * @param {Object} [params.logger=console]
29
+ */
30
+ constructor({ integrationRepository, queueProvider, queueId, now = () => new Date(), logger = console }) {
31
+ this.integrationRepository = integrationRepository;
32
+ this.queueProvider = queueProvider;
33
+ this.queueId = queueId;
34
+ this.now = now;
35
+ this.logger = logger;
36
+ }
37
+
38
+ /**
39
+ * @param {Array<Function>} integrationClasses
40
+ * @returns {Promise<{ dispatched: number, notDue: number, errors: number }>}
41
+ */
42
+ async execute(integrationClasses = []) {
43
+ const result = { dispatched: 0, notDue: 0, errors: 0 };
44
+ const now = this.now();
45
+
46
+ for (const IntegrationClass of integrationClasses) {
47
+ const name = IntegrationClass?.Definition?.name;
48
+ if (!name) continue;
49
+ if (typeof IntegrationClass.prototype?.startOngoingSync !== 'function') continue;
50
+
51
+ try {
52
+ const { scope, isDue } = resolveScheduleConfig(IntegrationClass.Definition);
53
+ if (scope === SCHEDULE_SCOPES.INSTALLATION) {
54
+ await this._dispatchPerInstallation(name, isDue, now, result);
55
+ } else {
56
+ await this._send(name, {});
57
+ result.dispatched++;
58
+ }
59
+ } catch (error) {
60
+ result.errors++;
61
+ this.logger.error(`[ScheduledSync] Failed to dispatch sync for ${name}:`, error.message);
62
+ }
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ async _dispatchPerInstallation(name, isDue, now, result) {
69
+ const integrations = await this.integrationRepository.findIntegrations({
70
+ type: name,
71
+ status: ACTIVE_STATUS,
72
+ });
73
+
74
+ for (const integration of integrations || []) {
75
+ try {
76
+ if (isDue && (await isDue(integration, { now })) !== true) {
77
+ result.notDue++;
78
+ continue;
79
+ }
80
+ await this._send(name, { integrationId: integration.id });
81
+ result.dispatched++;
82
+ } catch (error) {
83
+ result.errors++;
84
+ this.logger.error(
85
+ `[ScheduledSync] Failed to dispatch sync for ${name} integration ${integration?.id}:`,
86
+ error.message
87
+ );
88
+ }
89
+ }
90
+ }
91
+
92
+ _send(integrationName, data) {
93
+ return this.queueProvider.send(
94
+ { integrationName, event: ONGOING_SYNC_EVENT, data },
95
+ this.queueId
96
+ );
97
+ }
98
+ }
99
+
100
+ module.exports = { DispatchOngoingSyncs, ONGOING_SYNC_EVENT };
@@ -2,9 +2,17 @@
2
2
  const {
3
3
  mapIntegrationClassToIntegrationDTO,
4
4
  } = require('../utils/map-integration-dto');
5
+ const {
6
+ describeRecord,
7
+ visibilityClauses,
8
+ matchesListFilters,
9
+ } = require('../../modules/scope-visibility');
10
+ const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
5
11
 
6
12
  /**
7
- * Use case for retrieving all integrations for a specific user.
13
+ * Use case for retrieving the integrations a caller may see: the ones they own
14
+ * or their active organization owns, created in that organization (ADR-056
15
+ * §3.6), each with `tenancy` ({scope, owner, organizationId, access}).
8
16
  * @class GetIntegrationsForUser
9
17
  */
10
18
  class GetIntegrationsForUser {
@@ -15,13 +23,16 @@ class GetIntegrationsForUser {
15
23
  * @param {Array<import('../integration').Integration>} params.integrationClasses - Array of available integration classes.
16
24
  * @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management.
17
25
  * @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for module and entity data operations.
26
+ * @param {Object} [params.scopePolicy] - from createScopePolicy
18
27
  */
19
28
  constructor({
20
29
  integrationRepository,
21
30
  integrationClasses,
22
31
  moduleFactory,
23
32
  moduleRepository,
33
+ scopePolicy,
24
34
  }) {
35
+ this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
25
36
  /**
26
37
  * @type {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface}
27
38
  */
@@ -32,57 +43,86 @@ class GetIntegrationsForUser {
32
43
  }
33
44
 
34
45
  /**
35
- * Executes the retrieval of all integrations for a user.
36
46
  * @async
37
- * @param {string} userId - ID of the user whose integrations to retrieve.
38
- * @returns {Promise<Object[]>} Array of integration DTOs for the specified user.
47
+ * @param {Object} user - authenticated User
48
+ * @param {{scope?: string, owner?: string}} [filters] - `?scope=` and `?owner=`
49
+ * @returns {Promise<Object[]>} Array of integration DTOs.
39
50
  */
40
- async execute(userId) {
41
- const integrationRecords =
42
- await this.integrationRepository.findIntegrationsByUserId(userId);
51
+ async execute(user, filters = {}) {
52
+ const ctx = this.scopePolicy.accessContext(user);
53
+ const records = await this.integrationRepository.findIntegrationsByOwnership(
54
+ visibilityClauses(ctx, 'integration')
55
+ );
43
56
 
44
57
  const integrations = [];
58
+ for (const record of records) {
59
+ // The query is the filter; the same rule again means a repository
60
+ // that returns too much cannot widen the list.
61
+ const tenancy = describeRecord(record, ctx, 'integration');
62
+ if (!tenancy || !matchesListFilters(tenancy, filters)) continue;
63
+ integrations.push({ ...(await this.toDto(record)), tenancy });
64
+ }
65
+ return integrations;
66
+ }
45
67
 
46
- for (const integrationRecord of integrationRecords) {
47
- const entities = await this.moduleRepository.findEntitiesByIds(
48
- integrationRecord.entitiesIds
49
- );
68
+ /** Hydrates one stored integration record into its DTO. */
69
+ async toDto(integrationRecord) {
70
+ const entities = await this.moduleRepository.findEntitiesByIds(
71
+ integrationRecord.entitiesIds
72
+ );
73
+
74
+ const integrationClass = this.integrationClasses.find(
75
+ (integrationClass) =>
76
+ integrationClass.Definition.name ===
77
+ integrationRecord.config.type
78
+ );
50
79
 
51
- const integrationClass = this.integrationClasses.find(
52
- (integrationClass) =>
53
- integrationClass.Definition.name ===
54
- integrationRecord.config.type
80
+ const modules = [];
81
+ for (const entity of entities) {
82
+ const moduleInstance = await this.moduleFactory.getModuleInstance(
83
+ entity.id,
84
+ integrationRecord.userId
55
85
  );
86
+ modules.push(moduleInstance);
87
+ }
56
88
 
57
- const modules = [];
58
- for (const entity of entities) {
59
- const moduleInstance =
60
- await this.moduleFactory.getModuleInstance(
61
- entity.id,
62
- integrationRecord.userId
63
- );
64
- modules.push(moduleInstance);
65
- }
89
+ return mapIntegrationClassToIntegrationDTO({
90
+ id: integrationRecord.id,
91
+ userId: integrationRecord.userId,
92
+ entities: entities,
93
+ config: integrationRecord.config,
94
+ status: integrationRecord.status,
95
+ version: integrationRecord.version,
96
+ messages: integrationRecord.messages || { errors: [], warnings: [] },
97
+ modules,
98
+ options: integrationClass.getOptionDetails(),
99
+ });
100
+ }
101
+ }
66
102
 
67
- const integrationData = {
68
- id: integrationRecord.id,
69
- userId: integrationRecord.userId,
70
- entities: entities,
71
- config: integrationRecord.config,
72
- status: integrationRecord.status,
73
- version: integrationRecord.version,
74
- messages: integrationRecord.messages || { errors: [], warnings: [] },
75
- modules,
76
- options: integrationClass.getOptionDetails(),
77
- };
103
+ /**
104
+ * Every integration one user id owns, with no organization rules: the lookup
105
+ * behind the app-level `friggCommands.findIntegrationsByUserId`, which has a
106
+ * user id and no request.
107
+ */
108
+ class GetIntegrationsByUserId {
109
+ /** @param {ConstructorParameters<typeof GetIntegrationsForUser>[0]} params */
110
+ constructor(params) {
111
+ this.integrationRepository = params.integrationRepository;
112
+ this.hydrator = new GetIntegrationsForUser(params);
113
+ }
78
114
 
79
- integrations.push(
80
- mapIntegrationClassToIntegrationDTO(integrationData)
81
- );
115
+ /** @param {string} userId */
116
+ async execute(userId) {
117
+ const records = await this.integrationRepository.findIntegrationsByUserId(
118
+ userId
119
+ );
120
+ const integrations = [];
121
+ for (const record of records) {
122
+ integrations.push(await this.hydrator.toDto(record));
82
123
  }
83
-
84
124
  return integrations;
85
125
  }
86
126
  }
87
127
 
88
- module.exports = { GetIntegrationsForUser };
128
+ module.exports = { GetIntegrationsForUser, GetIntegrationsByUserId };
@@ -1,4 +1,7 @@
1
- const { GetIntegrationsForUser } = require('./get-integrations-for-user');
1
+ const {
2
+ GetIntegrationsForUser,
3
+ GetIntegrationsByUserId,
4
+ } = require('./get-integrations-for-user');
2
5
  const { DeleteIntegrationForUser } = require('./delete-integration-for-user');
3
6
  const { CreateIntegration } = require('./create-integration');
4
7
  const { GetIntegration } = require('./get-integration');
@@ -6,9 +9,11 @@ const { CreateProcess } = require('./create-process');
6
9
  const { UpdateProcessState } = require('./update-process-state');
7
10
  const { UpdateProcessMetrics } = require('./update-process-metrics');
8
11
  const { GetProcess } = require('./get-process');
12
+ const { DispatchOngoingSyncs } = require('./dispatch-ongoing-syncs');
9
13
 
10
14
  module.exports = {
11
15
  GetIntegrationsForUser,
16
+ GetIntegrationsByUserId,
12
17
  DeleteIntegrationForUser,
13
18
  CreateIntegration,
14
19
  GetIntegration,
@@ -16,4 +21,5 @@ module.exports = {
16
21
  UpdateProcessState,
17
22
  UpdateProcessMetrics,
18
23
  GetProcess,
24
+ DispatchOngoingSyncs,
19
25
  };
@@ -0,0 +1,256 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { describeRecord } = require('../../modules/scope-visibility');
3
+ const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
4
+ const { assertCanManage } = require('../../modules/use-cases/manage-access');
5
+ const { isIntegrationNotFound } = require('../repositories/integration-not-found');
6
+
7
+ /**
8
+ * Management API v2 integration operations. They put an access check
9
+ * (missing or unavailable integration = 404) in front of the existing
10
+ * integration use cases, and pass the record's owner id on. Access follows
11
+ * the owner and the organization the integration was created in (ADR-056
12
+ * §3.6), not the owner alone.
13
+ */
14
+
15
+ const notFound = (integrationId) =>
16
+ Boom.notFound(`Integration ${integrationId} not found`, {
17
+ code: 'INTEGRATION_NOT_FOUND',
18
+ });
19
+
20
+ /**
21
+ * The integration record, or 404 when it is missing or the caller may not
22
+ * use it: not theirs or their active organization's, or created in another
23
+ * organization.
24
+ */
25
+ class GetOwnedIntegrationRecord {
26
+ constructor({ integrationRepository, scopePolicy }) {
27
+ this.integrationRepository = integrationRepository;
28
+ this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
29
+ }
30
+
31
+ /** `{ scope, owner, organizationId, access }` of a record to this caller, or null. */
32
+ describe(record, user) {
33
+ return describeRecord(record, this.scopePolicy.accessContext(user), 'integration');
34
+ }
35
+
36
+ /**
37
+ * @param {string} integrationId
38
+ * @param {Object} user - authenticated User
39
+ * @param {{manage?: boolean}} [options] - `manage: true` for changing or
40
+ * deleting: a person may use an organization's integration but only an
41
+ * admin may change it (403 ORG_ADMIN_REQUIRED)
42
+ */
43
+ async execute(integrationId, user, { manage = false } = {}) {
44
+ let record = null;
45
+ try {
46
+ record = await this.integrationRepository.findIntegrationById(
47
+ integrationId
48
+ );
49
+ } catch (error) {
50
+ if (
51
+ /Invalid ID|ObjectId|Malformed/i.test(error?.message || '') ||
52
+ isIntegrationNotFound(error)
53
+ ) {
54
+ throw notFound(integrationId);
55
+ }
56
+ throw error;
57
+ }
58
+ const tenancy = record && this.describe(record, user);
59
+ if (!tenancy) throw notFound(integrationId);
60
+ if (manage) assertCanManage(tenancy.access, `integration ${integrationId}`);
61
+ return record;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Creates (or reuses) an integration between entities the caller may use.
67
+ * The entities' access, the binding scopes and the integration type are
68
+ * checked before anything is written, and the integration is owned and
69
+ * paired with an organization by `resolveIntegrationOwnership` (§3, §3.6).
70
+ */
71
+ class CreateIntegrationForUser {
72
+ constructor({ createIntegration, resolveIntegrationOwnership, integrationClasses }) {
73
+ this.createIntegration = createIntegration;
74
+ this.resolveIntegrationOwnership = resolveIntegrationOwnership;
75
+ this.integrationClasses = integrationClasses || [];
76
+ }
77
+
78
+ async execute({ entities, config }, user) {
79
+ if (!config || typeof config !== 'object' || typeof config.type !== 'string') {
80
+ throw Boom.badRequest('config.type is required', {
81
+ code: 'VALIDATION_ERROR',
82
+ details: { field: 'config.type' },
83
+ });
84
+ }
85
+ const known = this.integrationClasses.some(
86
+ (IntegrationClass) => IntegrationClass.Definition?.name === config.type
87
+ );
88
+ if (!known) {
89
+ throw Boom.badRequest(`Unknown integration type '${config.type}'`, {
90
+ code: 'UNKNOWN_INTEGRATION_TYPE',
91
+ });
92
+ }
93
+ if (
94
+ !Array.isArray(entities) ||
95
+ entities.some((id) => typeof id !== 'string' && typeof id !== 'number')
96
+ ) {
97
+ throw Boom.badRequest('entities must be an array of entity ids', {
98
+ code: 'VALIDATION_ERROR',
99
+ details: { field: 'entities' },
100
+ });
101
+ }
102
+ const { ownerId, organizationId, adoptUnscoped } =
103
+ await this.resolveIntegrationOwnership.execute({
104
+ entityIds: entities,
105
+ config,
106
+ user,
107
+ });
108
+ return this.createIntegration.execute(
109
+ entities.map(String),
110
+ ownerId,
111
+ config,
112
+ { organizationId, adoptUnscoped }
113
+ );
114
+ }
115
+ }
116
+
117
+ /** The hydrated integration instance (modules loaded) for an owned integration. */
118
+ class GetIntegrationInstanceForUser {
119
+ constructor({ getOwnedIntegrationRecord, getIntegrationInstance }) {
120
+ this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
121
+ this.getIntegrationInstance = getIntegrationInstance;
122
+ }
123
+
124
+ async execute(integrationId, user) {
125
+ const record = await this.getOwnedIntegrationRecord.execute(
126
+ integrationId,
127
+ user
128
+ );
129
+ return this.getIntegrationInstance.execute(record.id, record.userId);
130
+ }
131
+ }
132
+
133
+ /** Replaces an owned integration's config. */
134
+ class UpdateIntegrationForUser {
135
+ constructor({ getOwnedIntegrationRecord, updateIntegration }) {
136
+ this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
137
+ this.updateIntegration = updateIntegration;
138
+ }
139
+
140
+ async execute(integrationId, user, config) {
141
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
142
+ throw Boom.badRequest('config must be an object', {
143
+ code: 'VALIDATION_ERROR',
144
+ details: { field: 'config' },
145
+ });
146
+ }
147
+ const record = await this.getOwnedIntegrationRecord.execute(
148
+ integrationId,
149
+ user,
150
+ { manage: true }
151
+ );
152
+ return this.updateIntegration.execute(record.id, record.userId, config);
153
+ }
154
+ }
155
+
156
+ /** Deletes an owned integration (runs its onDelete hooks). */
157
+ class RemoveIntegrationForUser {
158
+ constructor({ getOwnedIntegrationRecord, deleteIntegrationForUser }) {
159
+ this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
160
+ this.deleteIntegrationForUser = deleteIntegrationForUser;
161
+ }
162
+
163
+ async execute(integrationId, user) {
164
+ const record = await this.getOwnedIntegrationRecord.execute(
165
+ integrationId,
166
+ user,
167
+ { manage: true }
168
+ );
169
+ await this.deleteIntegrationForUser.execute(record.id, record.userId);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Sends one of the framework's user-facing events (GET_CONFIG_OPTIONS,
175
+ * GET_USER_ACTIONS, ...) to an owned integration. Handlers pass fixed event
176
+ * names only; caller-chosen action ids go through RunIntegrationActionForUser.
177
+ */
178
+ class SendIntegrationEventForUser {
179
+ constructor({ getIntegrationInstanceForUser }) {
180
+ this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
181
+ }
182
+
183
+ async execute(integrationId, user, event, payload) {
184
+ const instance = await this.getIntegrationInstanceForUser.execute(
185
+ integrationId,
186
+ user
187
+ );
188
+ return instance.send(event, payload);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Runs a user action on an owned integration. Unlike v1, the action id must
194
+ * be one the integration lists as a user action (static USER_ACTION events
195
+ * or its dynamic user actions), so a caller cannot fire internal events such
196
+ * as lifecycle hooks.
197
+ */
198
+ class RunIntegrationActionForUser {
199
+ constructor({ getIntegrationInstanceForUser }) {
200
+ this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
201
+ }
202
+
203
+ async execute(integrationId, user, actionId, payload) {
204
+ const instance = await this.getIntegrationInstanceForUser.execute(
205
+ integrationId,
206
+ user
207
+ );
208
+ const actions =
209
+ typeof instance.loadUserActions === 'function'
210
+ ? await instance.loadUserActions()
211
+ : {};
212
+ if (!actions || !Object.prototype.hasOwnProperty.call(actions, actionId)) {
213
+ throw Boom.notFound(
214
+ `Integration ${integrationId} has no user action '${actionId}'`,
215
+ { code: 'ACTION_NOT_FOUND' }
216
+ );
217
+ }
218
+ return instance.send(actionId, payload);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Runs the integration's auth test and reconciles its status. Returns the
224
+ * errors recorded during the test.
225
+ */
226
+ class TestIntegrationAuthForUser {
227
+ constructor({ getIntegrationInstanceForUser }) {
228
+ this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
229
+ }
230
+
231
+ /** @returns {Promise<{ ok: boolean, errors: Array<Object> }>} */
232
+ async execute(integrationId, user) {
233
+ const instance = await this.getIntegrationInstanceForUser.execute(
234
+ integrationId,
235
+ user
236
+ );
237
+ const start = Date.now();
238
+ const authPassed = await instance.testAuth();
239
+ await instance.reconcileAuthStatus(authPassed);
240
+ const errors = (instance.record?.messages?.errors || []).filter(
241
+ ({ timestamp }) => timestamp >= start
242
+ );
243
+ return { ok: Boolean(authPassed) && errors.length === 0, errors };
244
+ }
245
+ }
246
+
247
+ module.exports = {
248
+ GetOwnedIntegrationRecord,
249
+ CreateIntegrationForUser,
250
+ GetIntegrationInstanceForUser,
251
+ UpdateIntegrationForUser,
252
+ RemoveIntegrationForUser,
253
+ SendIntegrationEventForUser,
254
+ RunIntegrationActionForUser,
255
+ TestIntegrationAuthForUser,
256
+ };