@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.693.d8859c2.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 (261) 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 +0 -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/index.js +3 -65
  44. package/infrastructure/scheduler/index.js +6 -19
  45. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  46. package/integrations/data-defined/README.md +169 -0
  47. package/integrations/data-defined/canonical.js +11 -0
  48. package/integrations/data-defined/capabilities/acmeerp.capabilities.json +608 -0
  49. package/integrations/data-defined/capabilities/asana.capabilities.json +265 -0
  50. package/integrations/data-defined/capabilities/calendly.capabilities.json +385 -0
  51. package/integrations/data-defined/capabilities/docusign.capabilities.json +314 -0
  52. package/integrations/data-defined/capabilities/fathom.capabilities.json +228 -0
  53. package/integrations/data-defined/capabilities/github.capabilities.json +563 -0
  54. package/integrations/data-defined/capabilities/hubspot.capabilities.json +715 -0
  55. package/integrations/data-defined/capabilities/jira.capabilities.json +376 -0
  56. package/integrations/data-defined/capabilities/klaviyo.capabilities.json +475 -0
  57. package/integrations/data-defined/capabilities/linear.capabilities.json +324 -0
  58. package/integrations/data-defined/capabilities/netsuite.capabilities.json +491 -0
  59. package/integrations/data-defined/capabilities/notion.capabilities.json +208 -0
  60. package/integrations/data-defined/capabilities/phone.capabilities.json +470 -0
  61. package/integrations/data-defined/capabilities/pipedrive.capabilities.json +388 -0
  62. package/integrations/data-defined/capabilities/quickbooks.capabilities.json +615 -0
  63. package/integrations/data-defined/capabilities/salesforce.capabilities.json +507 -0
  64. package/integrations/data-defined/capabilities/shopify.capabilities.json +1082 -0
  65. package/integrations/data-defined/capabilities/slack.capabilities.json +363 -0
  66. package/integrations/data-defined/capabilities/stripe.capabilities.json +580 -0
  67. package/integrations/data-defined/capabilities/webflow.capabilities.json +196 -0
  68. package/integrations/data-defined/capabilities/zendesk.capabilities.json +409 -0
  69. package/integrations/data-defined/compile.js +77 -0
  70. package/integrations/data-defined/config-form.js +110 -0
  71. package/integrations/data-defined/data-defined-integration.js +366 -0
  72. package/integrations/data-defined/examples/acme-erp-quickbooks.definition.json +467 -0
  73. package/integrations/data-defined/examples/asana-slack.definition.json +220 -0
  74. package/integrations/data-defined/examples/calendly-salesforce.definition.json +459 -0
  75. package/integrations/data-defined/examples/docusign-salesforce.definition.json +305 -0
  76. package/integrations/data-defined/examples/fathom-hubspot-notion.definition.json +575 -0
  77. package/integrations/data-defined/examples/github-linear.definition.json +316 -0
  78. package/integrations/data-defined/examples/hubspot-slack.definition.json +409 -0
  79. package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +339 -0
  80. package/integrations/data-defined/examples/phone-hubspot.definition.json +637 -0
  81. package/integrations/data-defined/examples/pipedrive-hubspot.definition.json +136 -0
  82. package/integrations/data-defined/examples/pipedrive-phone.definition.json +340 -0
  83. package/integrations/data-defined/examples/records/phone-hubspot.definition-version.json +652 -0
  84. package/integrations/data-defined/examples/records/phone-hubspot.integration.json +86 -0
  85. package/integrations/data-defined/examples/records/phone-hubspot.mappings.json +39 -0
  86. package/integrations/data-defined/examples/records/phone-hubspot.workflow-run.json +62 -0
  87. package/integrations/data-defined/examples/shopify-klaviyo.definition.json +488 -0
  88. package/integrations/data-defined/examples/shopify-netsuite.definition.json +622 -0
  89. package/integrations/data-defined/examples/stripe-quickbooks.definition.json +476 -0
  90. package/integrations/data-defined/examples/webflow-hubspot-slack.definition.json +262 -0
  91. package/integrations/data-defined/examples/zendesk-jira.definition.json +604 -0
  92. package/integrations/data-defined/expressions.js +225 -0
  93. package/integrations/data-defined/index.js +39 -0
  94. package/integrations/data-defined/interpreter.js +89 -0
  95. package/integrations/data-defined/manifests.js +125 -0
  96. package/integrations/data-defined/repositories/definition-version-repository.js +67 -0
  97. package/integrations/data-defined/repositories/workflow-run-repository.js +59 -0
  98. package/integrations/data-defined/router.js +97 -0
  99. package/integrations/data-defined/runtime.js +208 -0
  100. package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +377 -0
  101. package/integrations/data-defined/schema/installation.v0.1.schema.json +83 -0
  102. package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +848 -0
  103. package/integrations/data-defined/schema/records.md +98 -0
  104. package/integrations/data-defined/schema-check.js +58 -0
  105. package/integrations/data-defined/scripts/validate.js +66 -0
  106. package/integrations/data-defined/steps.js +89 -0
  107. package/integrations/data-defined/transforms.js +91 -0
  108. package/integrations/data-defined/use-cases/definition-use-cases.js +187 -0
  109. package/integrations/data-defined/validate.js +370 -0
  110. package/integrations/data-defined/versioning.js +161 -0
  111. package/integrations/data-defined/webhooks.js +119 -0
  112. package/integrations/index.js +0 -1
  113. package/integrations/integration-base.js +29 -0
  114. package/integrations/integration-router.js +101 -131
  115. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  116. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  117. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  118. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  119. package/integrations/repositories/integration-mapping-repository.js +29 -0
  120. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  121. package/integrations/repositories/integration-repository-interface.js +1 -17
  122. package/integrations/repositories/integration-repository-mongo.js +77 -60
  123. package/integrations/repositories/integration-repository-postgres.js +76 -62
  124. package/integrations/repositories/process-repository-mongo.js +1 -1
  125. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  126. package/integrations/use-cases/create-integration.js +20 -55
  127. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  128. package/integrations/use-cases/index.js +1 -7
  129. package/integrations/utils/map-integration-dto.js +14 -43
  130. package/modules/index.js +0 -14
  131. package/modules/module-factory.js +0 -12
  132. package/modules/module.js +2 -53
  133. package/modules/repositories/module-repository-documentdb.js +6 -51
  134. package/modules/repositories/module-repository-interface.js +2 -19
  135. package/modules/repositories/module-repository-mongo.js +74 -67
  136. package/modules/repositories/module-repository-postgres.js +74 -66
  137. package/modules/repositories/module-repository.js +1 -2
  138. package/modules/requester/requester.js +5 -35
  139. package/modules/use-cases/get-entities-for-user.js +13 -26
  140. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  141. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  142. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  143. package/modules/use-cases/get-module.js +4 -10
  144. package/modules/use-cases/process-authorization-callback.js +19 -53
  145. package/modules/use-cases/refresh-entity-options.js +4 -10
  146. package/modules/use-cases/test-module-auth.js +4 -10
  147. package/package.json +6 -6
  148. package/prisma-mongodb/schema.prisma +50 -86
  149. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +66 -0
  150. package/prisma-postgresql/schema.prisma +48 -81
  151. package/queues/index.js +0 -25
  152. package/types/index.d.ts +0 -2
  153. package/user/repositories/user-repository-documentdb.js +2 -27
  154. package/user/repositories/user-repository-interface.js +0 -18
  155. package/user/repositories/user-repository-mongo.js +0 -24
  156. package/user/repositories/user-repository-postgres.js +0 -18
  157. package/user/use-cases/authenticate-user.js +1 -7
  158. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  159. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  160. package/user/user.js +1 -64
  161. package/credential/use-cases/credential-operations-for-user.js +0 -202
  162. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  163. package/database/organization-where.js +0 -18
  164. package/database/unique-violation.js +0 -25
  165. package/extensions/bootstrap-runner.js +0 -43
  166. package/extensions/extension-loader.js +0 -187
  167. package/extensions/index.js +0 -41
  168. package/extensions/initialize-app.js +0 -123
  169. package/extensions/route-mounter.js +0 -103
  170. package/extensions/schema-composer.js +0 -188
  171. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  172. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  173. package/integrations/repositories/integration-not-found.js +0 -14
  174. package/integrations/schedule-config.js +0 -80
  175. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  176. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  177. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  178. package/management-api/create-management-api-router.js +0 -49
  179. package/management-api/error-response.js +0 -116
  180. package/management-api/management-api-config.js +0 -151
  181. package/management-api/meta-router.js +0 -76
  182. package/management-api/openapi/build-openapi.js +0 -144
  183. package/management-api/openapi/components.js +0 -415
  184. package/management-api/openapi/v1-operations.js +0 -83
  185. package/management-api/openapi/v2-operations.js +0 -264
  186. package/management-api/route-registry.js +0 -411
  187. package/management-api/test-support/http-client.js +0 -39
  188. package/management-api/use-cases/get-management-api-meta.js +0 -57
  189. package/management-api/v1-deprecation.js +0 -120
  190. package/management-api/v2/create-v2-router.js +0 -85
  191. package/management-api/v2/dependencies.js +0 -281
  192. package/management-api/v2/dto.js +0 -143
  193. package/management-api/v2/handlers/authorization.js +0 -94
  194. package/management-api/v2/handlers/credentials.js +0 -33
  195. package/management-api/v2/handlers/entities.js +0 -77
  196. package/management-api/v2/handlers/integrations.js +0 -128
  197. package/management-api/v2/handlers/proxy.js +0 -34
  198. package/management-api/v2/request.js +0 -85
  199. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  200. package/modules/domain/authorization-session.js +0 -144
  201. package/modules/entity-scope.js +0 -15
  202. package/modules/entity-type-registry.js +0 -311
  203. package/modules/proxy/proxy-bases.js +0 -169
  204. package/modules/proxy/proxy-errors.js +0 -31
  205. package/modules/proxy/proxy-path.js +0 -183
  206. package/modules/proxy/proxy-policy.js +0 -277
  207. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  208. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  209. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  210. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  211. package/modules/repositories/entity-not-found.js +0 -18
  212. package/modules/requester/proxy-request.js +0 -245
  213. package/modules/scope-config.js +0 -192
  214. package/modules/scope-policy.js +0 -112
  215. package/modules/scope-resolution.js +0 -288
  216. package/modules/scope-visibility.js +0 -247
  217. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  218. package/modules/tests/doubles/ownership-matching.js +0 -18
  219. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  220. package/modules/use-cases/create-entity-from-credential.js +0 -113
  221. package/modules/use-cases/entity-operations-for-user.js +0 -162
  222. package/modules/use-cases/entity-types.js +0 -139
  223. package/modules/use-cases/execute-entity-proxy-request.js +0 -331
  224. package/modules/use-cases/get-authorization-requirements.js +0 -100
  225. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  226. package/modules/use-cases/manage-access.js +0 -21
  227. package/modules/use-cases/multi-step-authorization.js +0 -329
  228. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  229. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  230. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  231. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  232. package/providers/index.js +0 -23
  233. package/providers/resolve-provider.js +0 -93
  234. package/providers/runtime-detection.js +0 -124
  235. package/queues/providers/index.js +0 -11
  236. package/queues/providers/netlify-background-provider.js +0 -150
  237. package/queues/providers/qstash-queue-provider.js +0 -219
  238. package/queues/providers/sqs-queue-provider.js +0 -70
  239. package/queues/queue-client-interface.js +0 -55
  240. package/queues/queue-message-auth.js +0 -193
  241. package/queues/queue-provider-factory.js +0 -101
  242. package/queues/queue-provider.js +0 -65
  243. package/queues/sqs-queue-client.js +0 -81
  244. package/types/modules/index.d.ts +0 -214
  245. package/types/user/index.d.ts +0 -134
  246. package/user/adopter-jwt-config.js +0 -177
  247. package/user/adopter-jwt-errors.js +0 -59
  248. package/user/adopter-token-verifier.js +0 -122
  249. package/user/app-user-helpers.js +0 -97
  250. package/user/create-user-authentication.js +0 -64
  251. package/user/membership.js +0 -51
  252. package/user/repositories/organization-membership-record.js +0 -19
  253. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  254. package/user/repositories/organization-membership-repository-factory.js +0 -42
  255. package/user/repositories/organization-membership-repository-interface.js +0 -57
  256. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  257. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  258. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  259. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  260. package/user/use-cases/ensure-organization-membership.js +0 -92
  261. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Turning a provider's webhook delivery into the module events a definition listens for. Pure; no I/O.
3
+ *
4
+ * Providers disagree about everything: where the topic is (a body field, a header, two fields combined), whether one
5
+ * POST carries one event or a batch (HubSpot sends an array, QuickBooks nests entities inside notifications), which
6
+ * account it is about, and whether the first request is a handshake to answer synchronously (Slack url_verification,
7
+ * Asana X-Hook-Secret). A manifest says so in its `webhook` block, with the same template expressions definitions
8
+ * use, so none of it is code:
9
+ *
10
+ * "webhook": {
11
+ * "scope": "app", // one URL for every account ("installation": one per install)
12
+ * "batch": "body", // path to the array of events; [outer, inner] for two levels
13
+ * "topic": "{{item.subscriptionType}}", // the provider's topic; a list tries each until one is non-empty
14
+ * "eventId": "{{item.eventId}}", // dedupe id
15
+ * "account": "{{item.portalId}}", // the connected account (entity externalId) the event is about
16
+ * "verify": "verifyWebhookSignature", // optional Api method: ({ headers, body, query }) -> boolean
17
+ * "handshake": { "when": "...", "status": 200, "headers": {...}, "body": {...} },
18
+ * "unwrap": "payload" // a form-encoded field holding the JSON body (Slack interactivity)
19
+ * }
20
+ *
21
+ * and each event says which provider topic it is (`match`, default: the event key) and optionally a condition on the
22
+ * record (`when`): a phone module reports a missed call as call.completed with status missed.
23
+ *
24
+ * Template context: { body, headers, query, item, parent }. `item` is the event (the body itself when there is no
25
+ * batch); `parent` is the outer item of a two-level batch (a QuickBooks notification, which carries the realmId).
26
+ */
27
+ const { resolve, evaluate, getPath } = require('./expressions');
28
+
29
+ const list = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
30
+ const str = (v) => (v == null ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v));
31
+
32
+ /** The first template in `tpls` that renders to a non-empty string. */
33
+ function renderFirst(tpls, ctx) {
34
+ for (const t of list(tpls)) {
35
+ const v = str(resolve(t, ctx)).trim();
36
+ if (v) return v;
37
+ }
38
+ return '';
39
+ }
40
+
41
+ /** Lower-case header names, as Express and Lambda deliver them, so manifests can always write headers.x-shopify-topic. */
42
+ function normaliseHeaders(headers = {}) {
43
+ return Object.fromEntries(Object.entries(headers || {}).map(([k, v]) => [k.toLowerCase(), Array.isArray(v) ? v[0] : v]));
44
+ }
45
+
46
+ /** The delivery as the manifest sees it: lower-case headers, and the JSON body unwrapped from a form field if declared. */
47
+ function prepare(webhook = {}, delivery) {
48
+ let body = delivery.body;
49
+ if (webhook.unwrap && body && typeof body[webhook.unwrap] === 'string') {
50
+ try { body = JSON.parse(body[webhook.unwrap]); } catch { /* not JSON: leave the body as delivered */ }
51
+ }
52
+ return { body, headers: normaliseHeaders(delivery.headers), query: delivery.query || {} };
53
+ }
54
+
55
+ /** Split one delivery into events: [{ item, parent }]. */
56
+ function splitBatch(webhook = {}, input) {
57
+ const paths = list(webhook.batch);
58
+ const at = (obj, p) => (p === 'body' ? obj.body : p === '$' ? obj : getPath(obj, '$.' + p));
59
+ if (!paths.length) return [{ item: input.body, parent: null }];
60
+ const outer = at(input, paths[0]);
61
+ const outerItems = Array.isArray(outer) ? outer : outer == null ? [] : [outer];
62
+ if (paths.length === 1) return outerItems.map((item) => ({ item, parent: null }));
63
+ return outerItems.flatMap((parent) => {
64
+ const inner = getPath(parent, '$.' + paths[1]);
65
+ return (Array.isArray(inner) ? inner : inner == null ? [] : [inner]).map((item) => ({ item, parent }));
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Every module event in one delivery.
71
+ * @param {object} manifest the module's capability manifest
72
+ * @param {{ body, headers?, query? }} delivery
73
+ * @returns {Array<{ topic: string, providerTopic: string, eventId: string|null, account: string|null, event: object, record: any }>}
74
+ * topic is the manifest's event key (what definitions name); event is the item the workflow sees as $.event.
75
+ */
76
+ function eventsIn(manifest, delivery) {
77
+ const webhook = manifest?.webhook;
78
+ if (!webhook) return [];
79
+ const input = prepare(webhook, delivery);
80
+ const out = [];
81
+ for (const { item, parent } of splitBatch(webhook, input)) {
82
+ const ctx = { ...input, item, parent };
83
+ const providerTopic = renderFirst(webhook.topic, ctx);
84
+ if (!providerTopic) continue;
85
+ const eventId = renderFirst(webhook.eventId, ctx) || null;
86
+ const account = webhook.account ? renderFirst(webhook.account, ctx) || null : null;
87
+ for (const [key, ev] of Object.entries(manifest.events || {})) {
88
+ if (ev.delivery && ev.delivery !== 'webhook') continue;
89
+ if ((ev.match ?? key) !== providerTopic) continue;
90
+ const record = ev.record ? getPath(item, '$.' + ev.record) : item;
91
+ if (ev.when && !evaluate(ev.when, { event: item, record, headers: input.headers, parent })) continue;
92
+ out.push({ topic: key, providerTopic, eventId, account, event: item, record });
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+
98
+ /** A handshake to answer at receipt, before anything is queued: { status, headers, body } or null. */
99
+ function handshakeFor(manifest, delivery) {
100
+ const h = manifest?.webhook?.handshake;
101
+ if (!h) return null;
102
+ const ctx = prepare(manifest.webhook, delivery);
103
+ if (!evaluate(h.when, ctx)) return null;
104
+ return { status: h.status || 200, headers: resolve(h.headers || {}, ctx), body: h.body === undefined ? '' : resolve(h.body, ctx) };
105
+ }
106
+
107
+ /**
108
+ * Which module of a definition a delivery came from. The URL names it (?module=<module name>); without that, the one
109
+ * module whose manifest recognises an event in the body. Returns module keys of the definition (usually one).
110
+ */
111
+ function modulesFor(def, manifests, delivery) {
112
+ const named = delivery.query?.module;
113
+ const keys = Object.entries(def.modules || {}).filter(([, m]) => (named ? m.module === named : true)).map(([k]) => k);
114
+ if (named) return keys;
115
+ const recognised = keys.filter((k) => eventsIn(manifests[def.modules[k].module], delivery).length > 0);
116
+ return recognised.length === 1 ? recognised : [];
117
+ }
118
+
119
+ module.exports = { eventsIn, handshakeFor, modulesFor, splitBatch, renderFirst, normaliseHeaders };
@@ -21,7 +21,6 @@ module.exports = {
21
21
  Options,
22
22
  createIntegrationRouter,
23
23
  checkRequiredParams,
24
- // Deprecated (ADR-059): a wrapper over getEntityTypeRegistry.
25
24
  getModulesDefinitionFromIntegrationClasses,
26
25
  LoadIntegrationContextUseCase,
27
26
  validateExtensionBinding,
@@ -457,6 +457,35 @@ class IntegrationBase {
457
457
  );
458
458
  }
459
459
 
460
+ /**
461
+ * Insert-only mapping write; usable as an atomic claim/lock row.
462
+ * @throws {MappingAlreadyExistsError} If a mapping for sourceId exists
463
+ */
464
+ async createMapping(sourceId, mapping) {
465
+ if (!sourceId) {
466
+ throw new Error(`sourceId must be set`);
467
+ }
468
+ return await this.integrationMappingRepository.createMapping(
469
+ this.id,
470
+ sourceId,
471
+ mapping
472
+ );
473
+ }
474
+
475
+ /**
476
+ * Deletes the mapping for sourceId. A missing mapping is not an error
477
+ * (resolves with deletedCount 0).
478
+ */
479
+ async deleteMapping(sourceId) {
480
+ if (!sourceId) {
481
+ throw new Error(`sourceId must be set`);
482
+ }
483
+ return await this.integrationMappingRepository.deleteMapping(
484
+ this.id,
485
+ sourceId
486
+ );
487
+ }
488
+
460
489
  /**
461
490
  * CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
462
491
  */
@@ -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