@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
@@ -1,80 +0,0 @@
1
- /**
2
- * An integration's schedule configuration (`static Definition.schedule`),
3
- * read by hosts that tick ONGOING_SYNC from a cron (provider-netlify's
4
- * scheduled-sync function).
5
- *
6
- * schedule: {
7
- * // 'class' (default): one ONGOING_SYNC per integration class per tick,
8
- * // with no integration id: the worker runs it on an unhydrated
9
- * // instance, so the handler finds its own installations.
10
- * // 'installation': one ONGOING_SYNC per ENABLED integration record of
11
- * // the class, carrying `data.integrationId`, so the worker hydrates
12
- * // that installation before the handler runs.
13
- * scope: 'installation',
14
- * // Optional, 'installation' only: (integration, { now }) => boolean |
15
- * // Promise<boolean>. Return false to skip that installation this tick.
16
- * // Without it every installation is dispatched on every tick.
17
- * isDue: (integration, { now }) => true,
18
- * }
19
- *
20
- * Validated at load (loadAppDefinition) so a mistyped scope stops the app
21
- * rather than silently falling back to 'class'.
22
- */
23
-
24
- const SCHEDULE_SCOPES = Object.freeze({
25
- CLASS: 'class',
26
- INSTALLATION: 'installation',
27
- });
28
-
29
- const DEFAULT_SCHEDULE_SCOPE = SCHEDULE_SCOPES.CLASS;
30
-
31
- class ScheduleConfigError extends Error {
32
- constructor(message) {
33
- super(message);
34
- this.name = 'ScheduleConfigError';
35
- this.code = 'SCHEDULE_CONFIG_INVALID';
36
- }
37
- }
38
-
39
- /**
40
- * @param {Object} [definition] - an integration's static Definition
41
- * @returns {{ scope: 'class'|'installation', isDue: Function|null }}
42
- * @throws {ScheduleConfigError} when `schedule` is malformed
43
- */
44
- function resolveScheduleConfig(definition) {
45
- const name = definition?.name || 'integration';
46
- const schedule = definition?.schedule;
47
- if (schedule === undefined || schedule === null) {
48
- return { scope: DEFAULT_SCHEDULE_SCOPE, isDue: null };
49
- }
50
- if (typeof schedule !== 'object' || Array.isArray(schedule)) {
51
- throw new ScheduleConfigError(`${name}: Definition.schedule must be an object`);
52
- }
53
-
54
- const scopes = Object.values(SCHEDULE_SCOPES);
55
- const scope = schedule.scope === undefined ? DEFAULT_SCHEDULE_SCOPE : schedule.scope;
56
- if (!scopes.includes(scope)) {
57
- throw new ScheduleConfigError(
58
- `${name}: Definition.schedule.scope must be one of ${scopes.map((s) => `'${s}'`).join(', ')} (got ${JSON.stringify(schedule.scope)})`
59
- );
60
- }
61
-
62
- const isDue = schedule.isDue === undefined ? null : schedule.isDue;
63
- if (isDue !== null && typeof isDue !== 'function') {
64
- throw new ScheduleConfigError(`${name}: Definition.schedule.isDue must be a function`);
65
- }
66
- if (isDue && scope !== SCHEDULE_SCOPES.INSTALLATION) {
67
- throw new ScheduleConfigError(
68
- `${name}: Definition.schedule.isDue applies to scope 'installation' only`
69
- );
70
- }
71
-
72
- return { scope, isDue };
73
- }
74
-
75
- module.exports = {
76
- SCHEDULE_SCOPES,
77
- DEFAULT_SCHEDULE_SCOPE,
78
- ScheduleConfigError,
79
- resolveScheduleConfig,
80
- };
@@ -1,100 +0,0 @@
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 };
@@ -1,256 +0,0 @@
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
- };
@@ -1,177 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { SCOPES, resolveScope } = require('../../modules/scope-resolution');
3
- const {
4
- integrationOwnerKind,
5
- resolveIntegrationOwner,
6
- } = require('../../modules/scope-resolution');
7
- const {
8
- describeRecord,
9
- unscopedRowsBelongHere,
10
- } = require('../../modules/scope-visibility');
11
- const {
12
- bindingScopeSetting,
13
- readIntegrationOwnership,
14
- } = require('../../modules/scope-config');
15
- const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
16
- const { entityTypeNameOf } = require('../../modules/entity-type-registry');
17
-
18
- const notFound = (entityId) =>
19
- Boom.notFound(`Entity ${entityId} not found`, { code: 'ENTITY_NOT_FOUND' });
20
-
21
- /**
22
- * Who owns an integration about to be created, and in which organization
23
- * (ADR-056 §3, §3.3, §3.6). Shared by the v1 and v2 create paths. Nothing is
24
- * persisted before this has answered, so a refusal leaves no row behind.
25
- *
26
- * Every entity must be one the caller may use in the active organization. A
27
- * missing one, another organization's and a `global` one are the same 404, so
28
- * ids cannot be probed. When the integration declares a scope (a binding's
29
- * `scope` or `Definition.entities[key].scope`, or `Definition.ownership.scope`)
30
- * the entity's scope must be in it. Levels below the integration (entity type,
31
- * app, default) decide where a connection is CREATED and do not restrict what
32
- * an integration may bind, so an app that adds a registry scope later does not
33
- * break integrations that already use older connections.
34
- *
35
- * The owner follows the entities (all organization: the organization;
36
- * otherwise the person) unless `Definition.ownership.owner` says. Every entity
37
- * must then be of the owner's own scope: an integration is loaded by its owner,
38
- * who must own what it uses, so an integration that mixes an organization
39
- * entity with a person's own, or that names an owner that does not match, is
40
- * refused with a 400 here, before anything is written.
41
- */
42
- class ResolveIntegrationOwnership {
43
- /**
44
- * @param {Object} params
45
- * @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
46
- * @param {Array<Function>} params.integrationClasses
47
- * @param {Object} [params.scopePolicy] - from createScopePolicy
48
- */
49
- constructor({ moduleRepository, integrationClasses, scopePolicy }) {
50
- this.moduleRepository = moduleRepository;
51
- this.integrationClasses = integrationClasses || [];
52
- this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
53
- }
54
-
55
- /**
56
- * @param {Object} params
57
- * @param {Array<string|number>} params.entityIds
58
- * @param {Object} params.config - `{ type }`
59
- * @param {Object} params.user - authenticated User
60
- * @returns {Promise<{ownerId: string, organizationId: (string|null), ownerKind: string, adoptUnscoped: boolean}>}
61
- */
62
- async execute({ entityIds, config, user }) {
63
- const ctx = this.scopePolicy.accessContext(user);
64
- const integrationClass = this.integrationClasses.find(
65
- (IntegrationClass) => IntegrationClass.Definition?.name === config?.type
66
- );
67
- const ownership = readIntegrationOwnership(integrationClass);
68
-
69
- const entities = await this.loadEntities(entityIds);
70
- const scoped = entities.map((entity) => ({
71
- entity,
72
- scope: this.acceptedScope({ entity, ctx, integrationClass, ownership }),
73
- }));
74
-
75
- const ownerKind = integrationOwnerKind(
76
- scoped.map(({ scope }) => scope),
77
- ownership.owner
78
- );
79
- this.assertOneScope(scoped, ownerKind, config?.type);
80
- return {
81
- ...resolveIntegrationOwner(ownerKind, ctx),
82
- ownerKind,
83
- // Whether an existing integration with no organization (written
84
- // before scoping) is this caller's to reuse. An organization-owned
85
- // one is unambiguous: its owner is the organization, whoever asks
86
- // and whatever their own legacy pointer says. A person's is theirs
87
- // only where scope-visibility says (their legacy organization).
88
- adoptUnscoped:
89
- ownerKind === SCOPES.ORGANIZATION ||
90
- unscopedRowsBelongHere(ctx, 'integration'),
91
- };
92
- }
93
-
94
- /** Every entity must be of the integration owner's own scope (see the class comment). */
95
- assertOneScope(scoped, ownerKind, type) {
96
- for (const { entity, scope } of scoped) {
97
- if (scope === ownerKind) continue;
98
- // The load-time check (resolveOwnershipConfig) only sees declared
99
- // scopes; this one sees the entities actually named.
100
- if (ownerKind === SCOPES.ORGANIZATION) {
101
- throw Boom.badRequest(
102
- "An organization-owned integration cannot use an 'individual' entity: a shared integration must not run on one person's credential",
103
- {
104
- code: 'ORG_INTEGRATION_INDIVIDUAL_ENTITY',
105
- details: { entityId: String(entity.id) },
106
- }
107
- );
108
- }
109
- throw Boom.badRequest(
110
- `Integration '${type}' would be owned by the person but uses the organization entity ${entity.id}. An integration must use entities of one scope: all of the person's, or all of the organization's.`,
111
- {
112
- code: 'MIXED_ENTITY_SCOPES',
113
- details: { entityId: String(entity.id), scope },
114
- }
115
- );
116
- }
117
- }
118
-
119
- async loadEntities(entityIds) {
120
- const ids = (entityIds || []).map(String);
121
- if (ids.length === 0) return [];
122
- let found;
123
- try {
124
- found = await this.moduleRepository.findEntitiesByIds(ids);
125
- } catch (error) {
126
- // A malformed id (e.g. not an integer on PostgreSQL) is "not found".
127
- if (/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
128
- throw notFound(ids[0]);
129
- }
130
- throw error;
131
- }
132
- const byId = new Map((found || []).filter(Boolean).map((e) => [String(e.id), e]));
133
- return ids.map((id) => {
134
- if (!byId.has(id)) throw notFound(id);
135
- return byId.get(id);
136
- });
137
- }
138
-
139
- /** The entity's scope for this caller, or a refusal. */
140
- acceptedScope({ entity, ctx, integrationClass, ownership }) {
141
- const described = describeRecord(entity, ctx, 'entity');
142
- if (!described) throw notFound(entity.id);
143
- const { scope } = described;
144
-
145
- const declared = this.declaredScope({ entity, integrationClass, ownership });
146
- if (declared && !declared.allow.includes(scope)) {
147
- throw Boom.badRequest(
148
- `Entity ${entity.id} is ${scope}-scoped, but this integration binds ${entity.moduleName} at ${declared.allow.join(' or ')} scope`,
149
- {
150
- code: 'ENTITY_SCOPE_MISMATCH',
151
- details: { entityId: String(entity.id), scope, allowed: declared.allow },
152
- }
153
- );
154
- }
155
- return scope;
156
- }
157
-
158
- /**
159
- * What the integration itself declares for this entity's binding (levels 2
160
- * and 3 only), or null when it declares nothing.
161
- */
162
- declaredScope({ entity, integrationClass, ownership }) {
163
- const Definition = integrationClass?.Definition;
164
- const key = Object.keys(Definition?.modules || {}).find(
165
- (candidate) =>
166
- entityTypeNameOf(Definition.modules[candidate]?.definition) ===
167
- entity.moduleName
168
- );
169
- const resolved = resolveScope({
170
- binding: key === undefined ? undefined : bindingScopeSetting(Definition, key),
171
- integration: ownership.scope,
172
- });
173
- return resolved.decidedBy === 'default' ? null : resolved;
174
- }
175
- }
176
-
177
- module.exports = { ResolveIntegrationOwnership };
@@ -1,49 +0,0 @@
1
- const express = require('express');
2
- const { resolveManagementApiConfig } = require('./management-api-config');
3
- const {
4
- createV1DeprecationRouter,
5
- createV1DisabledRouter,
6
- } = require('./v1-deprecation');
7
-
8
- /**
9
- * The Management API served by the auth Lambda (ADR-053): v2 under /api/v2
10
- * and the frozen, deprecated v1 at the unprefixed paths. With
11
- * `managementApi.v1: false` the v1 handlers are never built and the v1 paths
12
- * answer 410.
13
- *
14
- * @param {Object} [params]
15
- * @param {Object} [params.appDefinition] - defaults to loadAppDefinition().
16
- * @param {Function} [params.createV1Router] - defaults to createIntegrationRouter.
17
- * @param {Function} [params.createV2Router] - defaults to createManagementApiV2Router.
18
- */
19
- function createManagementApiRouter({
20
- appDefinition,
21
- createV1Router,
22
- createV2Router,
23
- } = {}) {
24
- const definition =
25
- appDefinition ||
26
- require('../handlers/app-definition-loader').loadAppDefinition();
27
- const config = resolveManagementApiConfig(definition.managementApi);
28
-
29
- const buildV1 =
30
- createV1Router ||
31
- require('../integrations/integration-router').createIntegrationRouter;
32
- const buildV2 =
33
- createV2Router ||
34
- require('./v2/create-v2-router').createManagementApiV2Router;
35
-
36
- const router = express.Router();
37
- router.use(buildV2({ appDefinition: definition, config }));
38
-
39
- if (config.v1) {
40
- router.use(createV1DeprecationRouter());
41
- router.use(buildV1());
42
- } else {
43
- router.use(createV1DisabledRouter());
44
- }
45
-
46
- return router;
47
- }
48
-
49
- module.exports = { createManagementApiRouter };