@friggframework/core 2.0.0--canary.703.12b980b.0 → 2.0.0--canary.707.df80bfd.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 (308) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +28 -0
  3. package/README.md +105 -0
  4. package/application/commands/integration-commands.js +20 -10
  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 -2
  19. package/database/encryption/README.md +78 -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 +121 -24
  23. package/database/encryption/field-encryption-service.js +69 -5
  24. package/database/encryption/integration-mapping-encryption.js +1 -1
  25. package/database/encryption/test-support/fake-ciphertext.js +15 -0
  26. package/database/organization-where.js +18 -0
  27. package/database/prisma.js +15 -38
  28. package/database/unique-violation.js +25 -0
  29. package/database/use-cases/check-encryption-health-use-case.js +43 -52
  30. package/errors/index.js +0 -4
  31. package/extensions/bootstrap-runner.js +43 -0
  32. package/extensions/extension-loader.js +230 -0
  33. package/extensions/index.js +44 -0
  34. package/extensions/initialize-app.js +123 -0
  35. package/extensions/prisma-composer.js +270 -0
  36. package/extensions/route-mounter.js +103 -0
  37. package/extensions/schema-composer.js +188 -0
  38. package/generated/prisma-mongodb/edge.js +33 -42
  39. package/generated/prisma-mongodb/index-browser.js +30 -39
  40. package/generated/prisma-mongodb/index.d.ts +3187 -2005
  41. package/generated/prisma-mongodb/index.js +33 -42
  42. package/generated/prisma-mongodb/package.json +1 -1
  43. package/generated/prisma-mongodb/schema.prisma +86 -59
  44. package/generated/prisma-mongodb/wasm.js +33 -42
  45. package/generated/prisma-postgresql/edge.js +33 -42
  46. package/generated/prisma-postgresql/index-browser.js +30 -39
  47. package/generated/prisma-postgresql/index.d.ts +3380 -2177
  48. package/generated/prisma-postgresql/index.js +33 -42
  49. package/generated/prisma-postgresql/package.json +1 -1
  50. package/generated/prisma-postgresql/schema.prisma +81 -57
  51. package/generated/prisma-postgresql/wasm.js +33 -42
  52. package/handlers/WEBHOOKS.md +0 -29
  53. package/handlers/app-definition-loader.js +128 -19
  54. package/handlers/app-handler-helpers.js +27 -8
  55. package/handlers/backend-utils.js +31 -25
  56. package/handlers/integration-event-dispatcher.js +3 -7
  57. package/handlers/middleware/admin-auth.js +14 -1
  58. package/handlers/routers/auth.js +46 -10
  59. package/handlers/routers/health.js +13 -4
  60. package/handlers/routers/integration-defined-routers.js +144 -92
  61. package/handlers/routers/integration-webhook-routers.js +100 -50
  62. package/handlers/workers/integration-defined-workers.js +5 -2
  63. package/index.js +66 -16
  64. package/infrastructure/scheduler/index.js +19 -6
  65. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  66. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  67. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  68. package/integrations/index.js +1 -14
  69. package/integrations/integration-base.js +2 -163
  70. package/integrations/integration-router.js +132 -102
  71. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  72. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  73. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  74. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  75. package/integrations/repositories/integration-mapping-repository.js +0 -29
  76. package/integrations/repositories/integration-not-found.js +14 -0
  77. package/integrations/repositories/integration-repository-documentdb.js +37 -9
  78. package/integrations/repositories/integration-repository-interface.js +17 -4
  79. package/integrations/repositories/integration-repository-mongo.js +64 -96
  80. package/integrations/repositories/integration-repository-postgres.js +63 -81
  81. package/integrations/repositories/process-repository-mongo.js +1 -1
  82. package/integrations/schedule-config.js +80 -0
  83. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  84. package/integrations/use-cases/create-integration.js +56 -31
  85. package/integrations/use-cases/delete-integration-for-user.js +4 -4
  86. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  87. package/integrations/use-cases/get-integration-for-user.js +2 -4
  88. package/integrations/use-cases/get-integration-instance.js +4 -4
  89. package/integrations/use-cases/get-integrations-for-user.js +79 -39
  90. package/integrations/use-cases/get-possible-integrations.js +2 -7
  91. package/integrations/use-cases/index.js +7 -1
  92. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  93. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  94. package/integrations/use-cases/update-integration.js +4 -4
  95. package/integrations/utils/map-integration-dto.js +43 -14
  96. package/management-api/create-management-api-router.js +49 -0
  97. package/management-api/error-response.js +116 -0
  98. package/management-api/management-api-config.js +151 -0
  99. package/management-api/meta-router.js +76 -0
  100. package/management-api/openapi/build-openapi.js +144 -0
  101. package/management-api/openapi/components.js +415 -0
  102. package/management-api/openapi/v1-operations.js +83 -0
  103. package/management-api/openapi/v2-operations.js +264 -0
  104. package/management-api/route-registry.js +411 -0
  105. package/management-api/test-support/http-client.js +39 -0
  106. package/management-api/use-cases/get-management-api-meta.js +57 -0
  107. package/management-api/v1-deprecation.js +120 -0
  108. package/management-api/v2/create-v2-router.js +85 -0
  109. package/management-api/v2/dependencies.js +281 -0
  110. package/management-api/v2/dto.js +143 -0
  111. package/management-api/v2/handlers/authorization.js +94 -0
  112. package/management-api/v2/handlers/credentials.js +33 -0
  113. package/management-api/v2/handlers/entities.js +77 -0
  114. package/management-api/v2/handlers/integrations.js +128 -0
  115. package/management-api/v2/handlers/proxy.js +34 -0
  116. package/management-api/v2/request.js +85 -0
  117. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  118. package/modules/domain/authorization-session.js +144 -0
  119. package/modules/entity-scope.js +15 -0
  120. package/modules/entity-type-registry.js +311 -0
  121. package/modules/index.js +14 -0
  122. package/modules/module-factory.js +12 -0
  123. package/modules/module.js +53 -2
  124. package/modules/proxy/proxy-bases.js +169 -0
  125. package/modules/proxy/proxy-errors.js +31 -0
  126. package/modules/proxy/proxy-path.js +183 -0
  127. package/modules/proxy/proxy-policy.js +277 -0
  128. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  129. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  130. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  131. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  132. package/modules/repositories/entity-not-found.js +18 -0
  133. package/modules/repositories/module-repository-documentdb.js +51 -6
  134. package/modules/repositories/module-repository-interface.js +19 -2
  135. package/modules/repositories/module-repository-mongo.js +67 -74
  136. package/modules/repositories/module-repository-postgres.js +66 -74
  137. package/modules/repositories/module-repository.js +2 -1
  138. package/modules/requester/proxy-request.js +245 -0
  139. package/modules/requester/requester.js +35 -5
  140. package/modules/scope-config.js +192 -0
  141. package/modules/scope-policy.js +112 -0
  142. package/modules/scope-resolution.js +288 -0
  143. package/modules/scope-visibility.js +247 -0
  144. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  145. package/modules/tests/doubles/ownership-matching.js +18 -0
  146. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  147. package/modules/use-cases/create-entity-from-credential.js +113 -0
  148. package/modules/use-cases/entity-operations-for-user.js +162 -0
  149. package/modules/use-cases/entity-types.js +139 -0
  150. package/modules/use-cases/execute-entity-proxy-request.js +331 -0
  151. package/modules/use-cases/get-authorization-requirements.js +100 -0
  152. package/modules/use-cases/get-entities-for-user.js +26 -13
  153. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  154. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  155. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  156. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  157. package/modules/use-cases/get-module.js +10 -4
  158. package/modules/use-cases/manage-access.js +21 -0
  159. package/modules/use-cases/multi-step-authorization.js +329 -0
  160. package/modules/use-cases/process-authorization-callback.js +53 -19
  161. package/modules/use-cases/refresh-entity-options.js +10 -4
  162. package/modules/use-cases/test-module-auth.js +10 -4
  163. package/package.json +6 -7
  164. package/prisma-mongodb/schema.prisma +86 -59
  165. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  166. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  167. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  168. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  169. package/prisma-postgresql/schema.prisma +81 -57
  170. package/providers/index.js +23 -0
  171. package/providers/resolve-provider.js +93 -0
  172. package/providers/runtime-detection.js +124 -0
  173. package/queues/index.js +25 -0
  174. package/queues/providers/index.js +11 -0
  175. package/queues/providers/netlify-background-provider.js +150 -0
  176. package/queues/providers/qstash-queue-provider.js +219 -0
  177. package/queues/providers/sqs-queue-provider.js +70 -0
  178. package/queues/queue-client-interface.js +55 -0
  179. package/queues/queue-message-auth.js +193 -0
  180. package/queues/queue-provider-factory.js +101 -0
  181. package/queues/queue-provider.js +65 -0
  182. package/queues/queuer-util.js +2 -9
  183. package/queues/sqs-queue-client.js +81 -0
  184. package/types/index.d.ts +2 -0
  185. package/types/modules/index.d.ts +214 -0
  186. package/types/user/index.d.ts +134 -0
  187. package/user/adopter-jwt-config.js +177 -0
  188. package/user/adopter-jwt-errors.js +59 -0
  189. package/user/adopter-token-verifier.js +122 -0
  190. package/user/app-user-helpers.js +97 -0
  191. package/user/create-user-authentication.js +64 -0
  192. package/user/membership.js +51 -0
  193. package/user/repositories/organization-membership-record.js +19 -0
  194. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  195. package/user/repositories/organization-membership-repository-factory.js +42 -0
  196. package/user/repositories/organization-membership-repository-interface.js +57 -0
  197. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  198. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  199. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  200. package/user/repositories/user-repository-documentdb.js +27 -2
  201. package/user/repositories/user-repository-interface.js +18 -0
  202. package/user/repositories/user-repository-mongo.js +24 -0
  203. package/user/repositories/user-repository-postgres.js +18 -0
  204. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  205. package/user/use-cases/authenticate-user.js +7 -1
  206. package/user/use-cases/ensure-organization-membership.js +92 -0
  207. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  208. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  209. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  210. package/user/user.js +64 -1
  211. package/errors/mapping-already-exists-error.js +0 -14
  212. package/handlers/workers/integration-triggers.js +0 -25
  213. package/infrastructure/scheduler/recurrence.js +0 -204
  214. package/integrations/define/DESIGN.md +0 -122
  215. package/integrations/define/config.js +0 -98
  216. package/integrations/define/define-app.js +0 -66
  217. package/integrations/define/define-extension.js +0 -154
  218. package/integrations/define/define-integration.js +0 -335
  219. package/integrations/define/examples/deal-alerts.js +0 -140
  220. package/integrations/define/extension-binding.js +0 -174
  221. package/integrations/define/index.js +0 -11
  222. package/integrations/define/json-schema-lite.js +0 -68
  223. package/integrations/define/subscriptions.js +0 -186
  224. package/integrations/define/tests/fixtures/hubspot.js +0 -109
  225. package/integrations/define/tests/fixtures/slack.js +0 -26
  226. package/integrations/define/use.js +0 -39
  227. package/integrations/definitions/README.md +0 -278
  228. package/integrations/definitions/canonical.js +0 -11
  229. package/integrations/definitions/capabilities/AUDIT.md +0 -480
  230. package/integrations/definitions/capabilities/acmeerp.capabilities.json +0 -633
  231. package/integrations/definitions/capabilities/asana.capabilities.json +0 -279
  232. package/integrations/definitions/capabilities/calendly.capabilities.json +0 -385
  233. package/integrations/definitions/capabilities/docusign.capabilities.json +0 -314
  234. package/integrations/definitions/capabilities/fathom.capabilities.json +0 -238
  235. package/integrations/definitions/capabilities/github.capabilities.json +0 -585
  236. package/integrations/definitions/capabilities/hubspot.capabilities.json +0 -822
  237. package/integrations/definitions/capabilities/jira.capabilities.json +0 -391
  238. package/integrations/definitions/capabilities/klaviyo.capabilities.json +0 -503
  239. package/integrations/definitions/capabilities/linear.capabilities.json +0 -378
  240. package/integrations/definitions/capabilities/netsuite.capabilities.json +0 -535
  241. package/integrations/definitions/capabilities/notion.capabilities.json +0 -208
  242. package/integrations/definitions/capabilities/phone.capabilities.json +0 -511
  243. package/integrations/definitions/capabilities/pipedrive.capabilities.json +0 -430
  244. package/integrations/definitions/capabilities/quickbooks.capabilities.json +0 -635
  245. package/integrations/definitions/capabilities/salesforce.capabilities.json +0 -568
  246. package/integrations/definitions/capabilities/shopify.capabilities.json +0 -1128
  247. package/integrations/definitions/capabilities/slack.capabilities.json +0 -373
  248. package/integrations/definitions/capabilities/stripe.capabilities.json +0 -630
  249. package/integrations/definitions/capabilities/webflow.capabilities.json +0 -196
  250. package/integrations/definitions/capabilities/zendesk.capabilities.json +0 -422
  251. package/integrations/definitions/compile.js +0 -80
  252. package/integrations/definitions/config-form.js +0 -137
  253. package/integrations/definitions/examples/acme-erp-quickbooks.definition.json +0 -467
  254. package/integrations/definitions/examples/asana-slack.definition.json +0 -219
  255. package/integrations/definitions/examples/calendly-salesforce.definition.json +0 -459
  256. package/integrations/definitions/examples/docusign-salesforce.definition.json +0 -305
  257. package/integrations/definitions/examples/fathom-hubspot-notion.definition.json +0 -575
  258. package/integrations/definitions/examples/github-linear.definition.json +0 -322
  259. package/integrations/definitions/examples/hubspot-slack.definition.json +0 -427
  260. package/integrations/definitions/examples/klaviyo-hubspot.definition.json +0 -336
  261. package/integrations/definitions/examples/phone-hubspot.definition.json +0 -633
  262. package/integrations/definitions/examples/pipedrive-hubspot.definition.json +0 -136
  263. package/integrations/definitions/examples/pipedrive-phone.definition.json +0 -340
  264. package/integrations/definitions/examples/records/phone-hubspot.definition-version.json +0 -652
  265. package/integrations/definitions/examples/records/phone-hubspot.integration.json +0 -86
  266. package/integrations/definitions/examples/records/phone-hubspot.mappings.json +0 -39
  267. package/integrations/definitions/examples/records/phone-hubspot.workflow-run.json +0 -62
  268. package/integrations/definitions/examples/shopify-klaviyo.definition.json +0 -488
  269. package/integrations/definitions/examples/shopify-netsuite.definition.json +0 -622
  270. package/integrations/definitions/examples/stripe-quickbooks.definition.json +0 -476
  271. package/integrations/definitions/examples/webflow-hubspot-slack.definition.json +0 -262
  272. package/integrations/definitions/examples/zendesk-jira.definition.json +0 -604
  273. package/integrations/definitions/expressions.js +0 -229
  274. package/integrations/definitions/index.js +0 -48
  275. package/integrations/definitions/integration-runtime.js +0 -779
  276. package/integrations/definitions/interpreter.js +0 -104
  277. package/integrations/definitions/manifests.js +0 -177
  278. package/integrations/definitions/renderers/common.js +0 -67
  279. package/integrations/definitions/renderers/html.js +0 -129
  280. package/integrations/definitions/renderers/hubspot.js +0 -59
  281. package/integrations/definitions/renderers/preview.js +0 -154
  282. package/integrations/definitions/renderers/slack.js +0 -146
  283. package/integrations/definitions/repositories/definition-version-repository.js +0 -67
  284. package/integrations/definitions/repositories/workflow-run-repository.js +0 -80
  285. package/integrations/definitions/router.js +0 -170
  286. package/integrations/definitions/runtime.js +0 -268
  287. package/integrations/definitions/schema/capability-manifest.v0.1.schema.json +0 -549
  288. package/integrations/definitions/schema/installation.v0.1.schema.json +0 -130
  289. package/integrations/definitions/schema/integration-definition.v0.1.schema.json +0 -909
  290. package/integrations/definitions/schema/records.md +0 -105
  291. package/integrations/definitions/schema-check.js +0 -58
  292. package/integrations/definitions/scripts/check-manifests.js +0 -220
  293. package/integrations/definitions/scripts/validate.js +0 -66
  294. package/integrations/definitions/steps.js +0 -123
  295. package/integrations/definitions/sync-engine.js +0 -297
  296. package/integrations/definitions/tests/helpers-fake-crm.js +0 -49
  297. package/integrations/definitions/transforms.js +0 -91
  298. package/integrations/definitions/use-cases/definition-use-cases.js +0 -245
  299. package/integrations/definitions/validate.js +0 -436
  300. package/integrations/definitions/versioning.js +0 -176
  301. package/integrations/definitions/webhooks.js +0 -119
  302. package/integrations/integration-types.js +0 -73
  303. package/integrations/triggers.js +0 -244
  304. package/integrations/use-cases/run-trigger-tick.js +0 -155
  305. package/integrations/use-cases/run-trigger.js +0 -77
  306. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +0 -66
  307. package/prisma-postgresql/migrations/20261019000000_workflow_run_waits/migration.sql +0 -7
  308. package/prisma-postgresql/migrations/20261019120000_definition_workspace/migration.sql +0 -5
@@ -3,6 +3,7 @@ const {
3
3
  IntegrationRepositoryInterface,
4
4
  } = require('./integration-repository-interface');
5
5
  const { validateConfigPatch } = require('./config-patch-shared');
6
+ const { integrationNotFoundError } = require('./integration-not-found');
6
7
  const { strictIntId } = require('./report-id');
7
8
 
8
9
  /**
@@ -51,6 +52,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
51
52
  ...integration,
52
53
  id: integration.id?.toString(),
53
54
  userId: integration.userId?.toString(),
55
+ organizationId: integration.organizationId?.toString() ?? null,
54
56
  entities: integration.entities?.map(e => ({
55
57
  ...e,
56
58
  id: e.id?.toString(),
@@ -60,6 +62,25 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
60
62
  };
61
63
  }
62
64
 
65
+ /**
66
+ * The one place a converted integration row becomes the record the
67
+ * application sees (string ids, ids of its entities, its organization).
68
+ * @private
69
+ */
70
+ _mapIntegration(converted) {
71
+ return {
72
+ id: converted.id,
73
+ entitiesIds: converted.entities.map((e) => e.id),
74
+ userId: converted.userId,
75
+ organizationId: converted.organizationId,
76
+ config: converted.config,
77
+ version: converted.version,
78
+ status: converted.status,
79
+ messages: converted.messages,
80
+ createdAt: converted.createdAt,
81
+ };
82
+ }
83
+
63
84
  /**
64
85
  * Find all integrations for a user
65
86
  *
@@ -78,16 +99,35 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
78
99
  // Map to domain objects with string IDs
79
100
  return integrations.map((integration) => {
80
101
  const converted = this._convertIntegrationIds(integration);
81
- return {
82
- id: converted.id,
83
- entitiesIds: converted.entities.map((e) => e.id),
84
- userId: converted.userId,
85
- config: converted.config,
86
- version: converted.version,
87
- status: converted.status,
88
- messages: converted.messages,
89
- createdAt: converted.createdAt,
90
- };
102
+ return this._mapIntegration(converted);
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Find integrations matching any of the owner/organization pairs, in one
108
+ * query. `organizationId: null` matches rows with no organization. The
109
+ * caller (a use case) decides which pairs; this only runs them.
110
+ *
111
+ * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
112
+ * @returns {Promise<Array>} Array of integration objects with string IDs
113
+ */
114
+ async findIntegrationsByOwnership(clauses) {
115
+ if (!Array.isArray(clauses) || clauses.length === 0) return [];
116
+ const integrations = await this.prisma.integration.findMany({
117
+ where: {
118
+ OR: clauses.map(({ userId, organizationId }) => ({
119
+ userId: this._convertId(userId),
120
+ organizationId: this._convertId(organizationId),
121
+ })),
122
+ },
123
+ include: {
124
+ entities: true,
125
+ },
126
+ });
127
+
128
+ return integrations.map((integration) => {
129
+ const converted = this._convertIntegrationIds(integration);
130
+ return this._mapIntegration(converted);
91
131
  });
92
132
  }
93
133
 
@@ -99,7 +139,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
99
139
  * @param {string} [filter.status] - Integration status
100
140
  * @returns {Promise<Array>} Array of integration objects (possibly empty)
101
141
  */
102
- async findIntegrations({ type, status, limit, after } = {}) {
142
+ async findIntegrations({ type, status } = {}) {
103
143
  const where = {};
104
144
  if (type) {
105
145
  where.config = { path: ['type'], equals: type };
@@ -107,30 +147,17 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
107
147
  if (status) {
108
148
  where.status = status;
109
149
  }
110
- if (limit && after !== undefined && after !== null) {
111
- where.id = { gt: this._convertId(after) };
112
- }
113
150
 
114
151
  const integrations = await this.prisma.integration.findMany({
115
152
  where,
116
153
  include: {
117
154
  entities: true,
118
155
  },
119
- ...(limit ? { orderBy: { id: 'asc' }, take: limit } : {}),
120
156
  });
121
157
 
122
158
  return integrations.map((integration) => {
123
159
  const converted = this._convertIntegrationIds(integration);
124
- return {
125
- id: converted.id,
126
- entitiesIds: converted.entities.map((e) => e.id),
127
- userId: converted.userId,
128
- config: converted.config,
129
- version: converted.version,
130
- status: converted.status,
131
- messages: converted.messages,
132
- createdAt: converted.createdAt,
133
- };
160
+ return this._mapIntegration(converted);
134
161
  });
135
162
  }
136
163
 
@@ -213,15 +240,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
213
240
  }
214
241
 
215
242
  const converted = this._convertIntegrationIds(integration);
216
- return {
217
- id: converted.id,
218
- entitiesIds: converted.entities.map((e) => e.id),
219
- userId: converted.userId,
220
- config: converted.config,
221
- version: converted.version,
222
- status: converted.status,
223
- messages: converted.messages,
224
- };
243
+ return this._mapIntegration(converted);
225
244
  }
226
245
 
227
246
  /**
@@ -240,19 +259,11 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
240
259
  });
241
260
 
242
261
  if (!integration) {
243
- throw new Error(`Integration with id ${id} not found`);
262
+ throw integrationNotFoundError(id);
244
263
  }
245
264
 
246
265
  const converted = this._convertIntegrationIds(integration);
247
- return {
248
- id: converted.id,
249
- entitiesIds: converted.entities.map((e) => e.id),
250
- userId: converted.userId,
251
- config: converted.config,
252
- version: converted.version,
253
- status: converted.status,
254
- messages: converted.messages,
255
- };
266
+ return this._mapIntegration(converted);
256
267
  }
257
268
 
258
269
  /**
@@ -332,12 +343,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
332
343
  * @param {Array<string>} entities - Array of entity IDs (strings from application layer)
333
344
  * @param {string} userId - User ID (string from application layer)
334
345
  * @param {Object} config - Integration configuration
346
+ * @param {{organizationId?: (string|null)}} [tenancy] - the organization the
347
+ * integration is created in (ADR-056 §3.6)
335
348
  * @returns {Promise<Object>} Created integration object with string IDs
336
349
  */
337
- async createIntegration(entities, userId, config) {
350
+ async createIntegration(entities, userId, config, tenancy = {}) {
338
351
  const data = {
339
352
  config,
340
353
  version: '0.0.0',
354
+ organizationId: this._convertId(tenancy.organizationId ?? null),
341
355
  };
342
356
 
343
357
  // PostgreSQL: use nested relations with ID conversion
@@ -358,15 +372,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
358
372
  });
359
373
 
360
374
  const converted = this._convertIntegrationIds(integration);
361
- return {
362
- id: converted.id,
363
- entitiesIds: converted.entities.map((e) => e.id),
364
- userId: converted.userId,
365
- config: converted.config,
366
- version: converted.version,
367
- status: converted.status,
368
- messages: converted.messages,
369
- };
375
+ return this._mapIntegration(converted);
370
376
  }
371
377
 
372
378
  /**
@@ -389,15 +395,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
389
395
  }
390
396
 
391
397
  const converted = this._convertIntegrationIds(integration);
392
- return {
393
- id: converted.id,
394
- entitiesIds: converted.entities.map((e) => e.id),
395
- userId: converted.userId,
396
- config: converted.config,
397
- version: converted.version,
398
- status: converted.status,
399
- messages: converted.messages,
400
- };
398
+ return this._mapIntegration(converted);
401
399
  }
402
400
 
403
401
  /**
@@ -422,15 +420,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
422
420
  });
423
421
 
424
422
  const converted = this._convertIntegrationIds(integration);
425
- return {
426
- id: converted.id,
427
- entitiesIds: converted.entities.map((e) => e.id),
428
- userId: converted.userId,
429
- config: converted.config,
430
- version: converted.version,
431
- status: converted.status,
432
- messages: converted.messages,
433
- };
423
+ return this._mapIntegration(converted);
434
424
  }
435
425
 
436
426
  /**
@@ -480,15 +470,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
480
470
 
481
471
  return integrations.map((integration) => {
482
472
  const converted = this._convertIntegrationIds(integration);
483
- return {
484
- id: converted.id,
485
- entitiesIds: converted.entities.map((e) => e.id),
486
- userId: converted.userId,
487
- config: converted.config,
488
- version: converted.version,
489
- status: converted.status,
490
- messages: converted.messages,
491
- };
473
+ return this._mapIntegration(converted);
492
474
  });
493
475
  }
494
476
  }
@@ -120,7 +120,7 @@ class ProcessRepositoryMongo extends ProcessRepositoryInterface {
120
120
  if (normalized.newState !== null) {
121
121
  $set.state = normalized.newState;
122
122
  }
123
- $set.updatedAt = { $date: new Date().toISOString() };
123
+ $set.updatedAt = new Date();
124
124
  update.$set = $set;
125
125
 
126
126
  if (Object.keys(normalized.pushSlice).length > 0) {
@@ -0,0 +1,80 @@
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,5 +1,6 @@
1
1
  const { v4: uuid } = require('uuid');
2
2
  const { validateConfigPatch } = require('../../repositories/config-patch-shared');
3
+ const { matchesAnyClause } = require('../../../modules/tests/doubles/ownership-matching');
3
4
 
4
5
  class TestIntegrationRepository {
5
6
  constructor() {
@@ -7,13 +8,14 @@ class TestIntegrationRepository {
7
8
  this.operationHistory = [];
8
9
  }
9
10
 
10
- async createIntegration(entities, userId, config) {
11
+ async createIntegration(entities, userId, config, tenancy = {}) {
11
12
  const id = uuid();
12
13
  const record = {
13
14
  id,
14
15
  _id: id,
15
16
  entitiesIds: entities,
16
17
  userId: userId,
18
+ organizationId: tenancy.organizationId ?? null,
17
19
  config,
18
20
  version: '0.0.0',
19
21
  status: 'IN_CREATION',
@@ -38,6 +40,15 @@ class TestIntegrationRepository {
38
40
  return results;
39
41
  }
40
42
 
43
+ // Answers the way a database does for the clauses a use case hands it.
44
+ async findIntegrationsByOwnership(clauses) {
45
+ const results = Array.from(this.store.values()).filter((r) =>
46
+ matchesAnyClause(r, clauses)
47
+ );
48
+ this.operationHistory.push({ operation: 'findByOwnership', clauses, count: results.length });
49
+ return results;
50
+ }
51
+
41
52
  async findIntegrations(filter = {}) {
42
53
  const { type, status } = filter;
43
54
  const results = Array.from(this.store.values()).filter((r) => {
@@ -1,4 +1,3 @@
1
- const { resolveIntegrationClass } = require('../integration-types');
2
1
  // Removed Integration wrapper - using IntegrationBase directly
3
2
  const {
4
3
  mapIntegrationClassToIntegrationDTO,
@@ -28,31 +27,28 @@ class CreateIntegration {
28
27
  * and entity set, instead of creating a duplicate.
29
28
  * @async
30
29
  * @param {string[]} entities - Array of entity IDs to associate with the integration.
31
- * @param {string} userId - ID of the user creating the integration.
30
+ * @param {string} userId - ID of the OWNER of the integration (ADR-056 §3):
31
+ * the person or the organization's user, as ResolveIntegrationOwnership decided.
32
32
  * @param {Object} config - Configuration object for the integration.
33
33
  * @param {string} config.type - Type of integration to create.
34
+ * @param {{organizationId?: (string|null), adoptUnscoped?: boolean}} [tenancy]
35
+ * the organization it is created in (§3.6). A duplicate is only reused in
36
+ * the same organization, so one person's integration in organization A is
37
+ * not handed back as the one they asked for in B. `adoptUnscoped` also
38
+ * accepts a duplicate written before scoping (no organization).
34
39
  * @returns {Promise<Object>} The created or reused integration DTO.
35
40
  * @throws {Error} When integration class is not found for the specified type.
36
41
  */
37
- async execute(entities, userId, config) {
42
+ async execute(entities, userId, config, tenancy = {}) {
38
43
  console.log(
39
44
  `[Frigg] Creating ${config?.type} integration for user ${userId} with entities [${entities}]`
40
45
  );
41
46
 
42
- // Only a type this user may install: registered code, the app's catalogue, or one their workspace built.
43
- const integrationClass = await resolveIntegrationClass(
44
- this.integrationClasses,
45
- config?.type,
46
- { workspaceId: String(userId) }
47
- );
48
- if (!integrationClass) {
49
- throw new Error(`No integration class found for type: ${config?.type}`);
50
- }
51
-
52
47
  const existing = await this._findDuplicate(
53
48
  userId,
54
49
  config?.type,
55
- entities
50
+ entities,
51
+ tenancy
56
52
  );
57
53
  if (existing) {
58
54
  console.log(
@@ -61,11 +57,18 @@ class CreateIntegration {
61
57
  return this._reuseExisting(existing, config);
62
58
  }
63
59
 
60
+ // Everything that can be refused is refused before a row is written: an
61
+ // unknown type, or an entity the owner cannot load, would otherwise
62
+ // leave an IN_CREATION integration behind that nothing moves on.
63
+ const integrationClass = this._requireClass(config?.type);
64
+ const modules = await this._loadModules(entities, userId);
65
+
64
66
  const integrationRecord =
65
67
  await this.integrationRepository.createIntegration(
66
68
  entities,
67
69
  userId,
68
- config
70
+ config,
71
+ { organizationId: tenancy.organizationId ?? null }
69
72
  );
70
73
  console.log(
71
74
  `[Frigg] Created integration ${integrationRecord.id} for user ${userId}`
@@ -76,7 +79,8 @@ class CreateIntegration {
76
79
  const survivor = await this._findDuplicate(
77
80
  userId,
78
81
  config?.type,
79
- entities
82
+ entities,
83
+ tenancy
80
84
  );
81
85
  if (survivor && String(survivor.id) !== String(integrationRecord.id)) {
82
86
  console.log(
@@ -89,7 +93,8 @@ class CreateIntegration {
89
93
  }
90
94
 
91
95
  const integrationInstance = await this._buildInstance(
92
- integrationRecord
96
+ integrationRecord,
97
+ { integrationClass, modules }
93
98
  );
94
99
  console.log(
95
100
  `[Frigg] Sending ON_CREATE for integration ${integrationRecord.id}`
@@ -146,7 +151,7 @@ class CreateIntegration {
146
151
  return mapIntegrationClassToIntegrationDTO(integrationInstance);
147
152
  }
148
153
 
149
- async _findDuplicate(userId, type, entityIds) {
154
+ async _findDuplicate(userId, type, entityIds, tenancy = {}) {
150
155
  const candidates =
151
156
  await this.integrationRepository.findIntegrationsByUserId(userId);
152
157
  const target = [...(entityIds ?? [])].map(String).sort();
@@ -161,6 +166,7 @@ class CreateIntegration {
161
166
  // matching how teardown failures are already handled here.
162
167
  if (integration.status === 'IN_DELETION') return false;
163
168
  if (integration.config?.type !== type) return false;
169
+ if (!this._sameOrganization(integration, tenancy)) return false;
164
170
  const current = [...(integration.entitiesIds ?? [])]
165
171
  .map(String)
166
172
  .sort();
@@ -173,6 +179,15 @@ class CreateIntegration {
173
179
  return matches.length ? this._pickOldest(matches) : null;
174
180
  }
175
181
 
182
+ /** Same organization, or a row from before scoping that belongs here. */
183
+ _sameOrganization(integration, tenancy) {
184
+ const wanted = tenancy.organizationId ?? null;
185
+ const has = integration.organizationId ?? null;
186
+ if (wanted === null && has === null) return true;
187
+ if (wanted !== null && has !== null) return String(wanted) === String(has);
188
+ return has === null && tenancy.adoptUnscoped === true;
189
+ }
190
+
176
191
  _pickOldest(records) {
177
192
  return [...records].sort((a, b) => {
178
193
  const diff =
@@ -184,26 +199,36 @@ class CreateIntegration {
184
199
  })[0];
185
200
  }
186
201
 
187
- async _buildInstance(integrationRecord) {
188
- const integrationClass = await resolveIntegrationClass(
189
- this.integrationClasses,
190
- integrationRecord.config.type
202
+ _requireClass(type) {
203
+ const integrationClass = this.integrationClasses.find(
204
+ (integrationClass) => integrationClass.Definition.name === type
191
205
  );
192
-
193
206
  if (!integrationClass) {
194
- throw new Error(
195
- `No integration class found for type: ${integrationRecord.config.type}`
196
- );
207
+ throw new Error(`No integration class found for type: ${type}`);
197
208
  }
209
+ return integrationClass;
210
+ }
198
211
 
212
+ async _loadModules(entityIds, userId) {
199
213
  const modules = [];
200
- for (const entityId of integrationRecord.entitiesIds) {
201
- const moduleInstance = await this.moduleFactory.getModuleInstance(
202
- entityId,
203
- integrationRecord.userId
214
+ for (const entityId of entityIds) {
215
+ modules.push(
216
+ await this.moduleFactory.getModuleInstance(entityId, userId)
204
217
  );
205
- modules.push(moduleInstance);
206
218
  }
219
+ return modules;
220
+ }
221
+
222
+ async _buildInstance(integrationRecord, preloaded) {
223
+ const integrationClass =
224
+ preloaded?.integrationClass ??
225
+ this._requireClass(integrationRecord.config.type);
226
+ const modules =
227
+ preloaded?.modules ??
228
+ (await this._loadModules(
229
+ integrationRecord.entitiesIds,
230
+ integrationRecord.userId
231
+ ));
207
232
 
208
233
  const integrationInstance = new integrationClass({
209
234
  id: integrationRecord.id,
@@ -1,4 +1,3 @@
1
- const { resolveIntegrationClass } = require('../integration-types');
2
1
  const Boom = require('@hapi/boom');
3
2
  const { getLogger } = require('../../logs');
4
3
 
@@ -45,9 +44,10 @@ class DeleteIntegrationForUser {
45
44
  );
46
45
  }
47
46
 
48
- const integrationClass = await resolveIntegrationClass(
49
- this.integrationClasses,
50
- integrationRecord.config.type
47
+ const integrationClass = this.integrationClasses.find(
48
+ (integrationClass) =>
49
+ integrationClass.Definition.name ===
50
+ integrationRecord.config.type
51
51
  );
52
52
 
53
53
  if (integrationRecord.userId !== userId) {
@@ -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 };
@@ -1,4 +1,3 @@
1
- const { resolveIntegrationClass } = require('../integration-types');
2
1
  // Removed Integration wrapper - using IntegrationBase directly
3
2
  const { mapIntegrationClassToIntegrationDTO } = require('../utils/map-integration-dto');
4
3
  const Boom = require('@hapi/boom');
@@ -48,9 +47,8 @@ class GetIntegrationForUser {
48
47
  throw Boom.forbidden('User does not have access to this integration');
49
48
  }
50
49
 
51
- const integrationClass = await resolveIntegrationClass(
52
- this.integrationClasses,
53
- integrationRecord.config.type
50
+ const integrationClass = this.integrationClasses.find(
51
+ (integrationClass) => integrationClass.Definition.name === integrationRecord.config.type
54
52
  );
55
53
 
56
54
  const modules = [];