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