@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
@@ -1,268 +0,0 @@
1
- /**
2
- * The pieces IntegrationRuntime wires to Frigg. Each takes its I/O as arguments, so the whole runtime can be
3
- * driven in a test with fakes and the same code runs in Lambda.
4
- *
5
- * makePorts capability calls go through the module manifest to an Api method; links live in IntegrationMapping
6
- * runWorkflow one delivered event -> one WorkflowRun (dedupe, filter, steps, trace)
7
- * runQuery a UI surface's data
8
- * renderSurface query + view -> a resolved component tree the host's generic renderer draws
9
- * runLifecycle onInstall / onUninstall steps
10
- */
11
- const { execute } = require('./interpreter');
12
- const { evaluate, resolve, getPath } = require('./expressions');
13
- const { TRANSFORMS } = require('./transforms');
14
-
15
- /**
16
- * @param {object} p
17
- * @param {object} p.def
18
- * @param {Record<string, object>} p.manifests manifest name -> manifest
19
- * @param {(moduleKey:string, method:string, args:object) => Promise<any>} p.callApi e.g. (k, m, a) => this[k].api[m](a)
20
- * @param {{ get(key:string): Promise<object|null>, put(key:string, value:object): Promise<void>, claim(key:string, value:object): Promise<boolean> }} p.store
21
- * Backed by IntegrationMapping: get = getMapping, put = upsertMapping, claim = createMapping (insert-only; false if it existed)
22
- * @param {(ref:string, args:object) => Promise<any>} [p.runCode]
23
- */
24
- /**
25
- * The arguments an Api method is called with. A capability with no `args` takes one object holding every input; one
26
- * with `args` (manifest `args`) takes them positionally: an input by name, "$rest" (an object of the inputs not
27
- * named in the list) or { value } (a constant, e.g. the sObject name of Salesforce's generic create/get).
28
- */
29
- function argumentsFor(input, order) {
30
- if (!Array.isArray(order)) return [input];
31
- const named = new Set(order.filter((a) => typeof a === 'string' && a !== '$rest'));
32
- return order.map((a) => {
33
- if (a === '$rest') return Object.fromEntries(Object.entries(input || {}).filter(([k]) => !named.has(k)));
34
- if (a && typeof a === 'object') return a.value;
35
- return input?.[a];
36
- });
37
- }
38
-
39
- function makePorts({ def, manifests, callApi, store, runCode }) {
40
- const manifestFor = (key) => manifests[def.modules[key]?.module];
41
- const normalise = (set, key, value) => {
42
- const m = (set.identity?.match || []).find((x) => x.key === key);
43
- const fn = m?.transform && TRANSFORMS[m.transform];
44
- return fn ? fn(value) : value;
45
- };
46
- /** A sync or a link set (definition.links): both store links the same way, under their own id. */
47
- const linkSetOf = ({ link, sync }) => {
48
- const id = link ?? sync;
49
- const s = link != null ? (def.links || []).find((x) => x.id === id) : (def.syncs || []).find((x) => x.id === id);
50
- if (!s) throw new Error(`Unknown ${link != null ? 'link set' : 'sync'} "${id}"`);
51
- return s;
52
- };
53
- return {
54
- async call(moduleKey, capability, args) {
55
- const c = manifestFor(moduleKey)?.capabilities?.[capability];
56
- if (!c) throw new Error(`${moduleKey}.${capability} is not declared by the module manifest`);
57
- return callApi(moduleKey, c.method, args, c.args);
58
- },
59
- links: {
60
- /** By one side's id: link:<set>:<module>:<id>. By identity key: idx:<set>:<key>:<normalised value>. */
61
- async find({ link, sync, from, id, key }) {
62
- const s = linkSetOf({ link, sync });
63
- if (from && id != null) return (await store.get(`link:${s.id}:${from}:${id}`)) || null;
64
- for (const [k, v] of Object.entries(key || {})) {
65
- if (v == null || v === '') continue;
66
- const hit = await store.get(`idx:${s.id}:${k}:${normalise(s, k, v)}`);
67
- if (hit) return hit;
68
- }
69
- return null;
70
- },
71
- /** meta (the sync engine's known/seen) rides on the link rows; ids are what everything else reads. */
72
- async upsert({ link, sync, ids, keys, meta }) {
73
- const s = linkSetOf({ link, sync });
74
- const value = { ...(meta || {}), ...ids, linkedAt: new Date().toISOString() };
75
- for (const [mod, id] of Object.entries(ids || {})) if (id != null) await store.put(`link:${s.id}:${mod}:${id}`, value);
76
- for (const [k, v] of Object.entries(keys || {})) if (v != null && v !== '') await store.put(`idx:${s.id}:${k}:${normalise(s, k, v)}`, value);
77
- return value;
78
- },
79
- },
80
- dedupe: {
81
- claim: (key) => store.claim(key, { at: new Date().toISOString() }),
82
- /** A failed run gives its claim back, so the queue's retry runs the steps instead of reading as a duplicate. */
83
- release: (key) => (store.release ? store.release(key) : undefined),
84
- },
85
- runCode,
86
- };
87
- }
88
-
89
- /** The record an event is about, per the manifest's `record` path (phone: data.object; HubSpot: the payload itself). */
90
- function recordOf(manifests, def, moduleKey, topic, payload) {
91
- const ev = manifests[def.modules[moduleKey]?.module]?.events?.[topic];
92
- return ev?.record ? getPath(payload, '$.' + ev.record) : payload;
93
- }
94
-
95
- /**
96
- * Run one workflow for one event.
97
- * @returns {Promise<object>} a WorkflowRun record (see schema/records.md)
98
- */
99
- async function runWorkflow({ def, installation, workflowId, event = {}, eventId, record, ports, manifests, now = () => new Date(), runId }) {
100
- const w = (def.workflows || []).find((x) => x.id === workflowId);
101
- if (!w) throw new Error(`Unknown workflow "${workflowId}"`);
102
- const t = w.trigger;
103
- if (record === undefined && t.type === 'webhook') record = recordOf(manifests, def, t.module, t.topic, event);
104
- const startedAt = now();
105
- const base = {
106
- id: runId || `run_${startedAt.getTime().toString(36)}${require('crypto').randomBytes(4).toString('hex')}`, integrationId: installation.integrationId || null,
107
- definition: { id: def.id, version: def.version }, workflowId, trigger: { type: t.type, eventId: eventId ?? event.id ?? null },
108
- startedAt: startedAt.toISOString(),
109
- };
110
- const settings = { ...defaultsOf(w.settings), ...(installation.workflows?.[w.id]?.settings || {}) };
111
- const install = installContext(def, installation);
112
- const ctx = { event, record, settings, install, now: startedAt.toISOString() };
113
-
114
- if (t.filter && !evaluate(t.filter, ctx)) return { ...base, status: 'filtered', endedAt: now().toISOString(), trace: [] };
115
- // The provider's event id (from the manifest's webhook.eventId, else event.id) makes a redelivery a no-op.
116
- const dedupeId = eventId ?? event.id;
117
- const claimKey = dedupeId && ports.dedupe ? `evt:${def.id}:${w.id}:${dedupeId}` : null;
118
- if (claimKey) {
119
- const fresh = await ports.dedupe.claim(claimKey);
120
- if (!fresh) return { ...base, status: 'duplicate', endedAt: now().toISOString(), trace: [] };
121
- }
122
- try {
123
- const out = await execute(w.steps, { def, installation, ctx, ports, canWait: true });
124
- if (out.status === 'waiting') return paused(base, out, { workflowId, event, record, eventId: base.trigger.eventId, settings, now: ctx.now });
125
- return { ...base, status: out.status, reason: out.reason, endedAt: now().toISOString(), trace: out.trace };
126
- } catch (e) {
127
- if (claimKey) await ports.dedupe.release?.(claimKey);
128
- return { ...base, status: 'failed', error: String(e.message || e), endedAt: now().toISOString(), trace: e.trace || [], retryable: true };
129
- }
130
- }
131
-
132
- /**
133
- * A run that reached a pausing step. `waitingFor` is safe to store on the run row (no data from the event);
134
- * `checkpoint` is everything needed to carry on and belongs in encrypted storage (it holds step results).
135
- */
136
- function paused(base, out, { workflowId, event, record, eventId, settings, now }) {
137
- const { index, id, ...wait } = out.wait;
138
- return {
139
- ...base, status: 'waiting', trace: out.trace, resumeAt: wait.until || null,
140
- waitingFor: { stepId: id, kind: wait.kind, until: wait.until || null, ...(wait.kind === 'approval' ? { prompt: wait.prompt, approvers: wait.approvers, onTimeout: wait.onTimeout } : {}) },
141
- // The trace so far (without the waiting entry) travels with the checkpoint, so the finished run has every step.
142
- checkpoint: { workflowId, index, stepId: id, wait, steps: out.steps, vars: out.vars, event, record, eventId, settings, startedAt: base.startedAt, firstRunAt: now, trace: out.trace.filter((t) => !(t.status === 'waiting' && t.id === id)) },
143
- };
144
- }
145
-
146
- /**
147
- * Carry on a paused run. `result` is what the pausing step evaluates to now ({ resumedAt } for a delay, the decision
148
- * for an approval); `stop` ends the run there instead (an approval timing out with onTimeout: stop).
149
- * @returns {Promise<object>} the same run, completed, stopped, failed or waiting again at a later step
150
- */
151
- async function resumeWorkflow({ def, installation, checkpoint, result, stop, ports, now = () => new Date(), runId }) {
152
- const w = (def.workflows || []).find((x) => x.id === checkpoint.workflowId);
153
- if (!w) throw new Error(`Unknown workflow "${checkpoint.workflowId}"`);
154
- const resumedAt = now();
155
- const base = {
156
- id: runId, integrationId: installation.integrationId || null, definition: { id: def.id, version: def.version },
157
- workflowId: w.id, trigger: { type: w.trigger.type, eventId: checkpoint.eventId ?? null }, startedAt: checkpoint.startedAt,
158
- };
159
- const step = { id: checkpoint.stepId, use: w.steps[checkpoint.index]?.use, at: `steps[${checkpoint.index}]` };
160
- if (stop) return { ...base, status: 'stopped', reason: stop, endedAt: resumedAt.toISOString(), trace: [...(checkpoint.trace || []), { ...step, status: 'stopped', reason: stop }] };
161
- const ctx = {
162
- event: checkpoint.event, record: checkpoint.record, settings: checkpoint.settings, install: installContext(def, installation),
163
- now: resumedAt.toISOString(), steps: { ...checkpoint.steps, [checkpoint.stepId]: result }, vars: { ...checkpoint.vars },
164
- };
165
- const head = [...(checkpoint.trace || []), { ...step, status: 'ok', resumedAt: resumedAt.toISOString() }];
166
- try {
167
- const out = await execute(w.steps, { def, installation, ctx, ports, canWait: true, startAt: checkpoint.index + 1 });
168
- if (out.status === 'waiting') return paused(base, { ...out, trace: head.concat(out.trace) }, { workflowId: w.id, event: checkpoint.event, record: checkpoint.record, eventId: checkpoint.eventId, settings: checkpoint.settings, now: ctx.now });
169
- return { ...base, status: out.status, reason: out.reason, endedAt: now().toISOString(), trace: head.concat(out.trace) };
170
- } catch (e) {
171
- return { ...base, status: 'failed', error: String(e.message || e), endedAt: now().toISOString(), trace: head.concat(e.trace || []), retryable: true };
172
- }
173
- }
174
-
175
- async function runQuery({ def, installation, queryId, record, ports }) {
176
- const q = (def.queries || []).find((x) => x.id === queryId);
177
- if (!q) throw new Error(`Unknown query "${queryId}"`);
178
- const install = installContext(def, installation);
179
- const out = await execute(q.steps, { def, installation, ctx: { record, install, event: {} }, ports });
180
- if (out.status === 'stopped') return { stopped: out.reason };
181
- return resolve(q.returns, { steps: out.steps, record, install });
182
- }
183
-
184
- /** Resolve a view tree against data: drop nodes whose `when` is false, expand List/Table rows. */
185
- function resolveView(node, ctx) {
186
- if (!node) return null;
187
- if (node.when && !evaluate(node.when, ctx)) return null;
188
- const props = { ...(node.props || {}) };
189
- if (node.component === 'List') {
190
- const items = resolve(props.items, ctx) || [];
191
- props.items = (Array.isArray(items) ? items : []).map((item) => ({ title: resolve(props.title, { ...ctx, item }), meta: resolve(props.meta, { ...ctx, item }) }));
192
- delete props.title; delete props.meta;
193
- } else if (node.component === 'Table') {
194
- const rows = resolve(props.rows, ctx) || [];
195
- props.rows = (Array.isArray(rows) ? rows : []).map((row) => (props.columns || []).map((c) => resolve(c.value, { ...ctx, row })));
196
- props.columns = (props.columns || []).map((c) => c.label);
197
- } else {
198
- for (const [k, v] of Object.entries(props)) if (k !== 'action') props[k] = resolve(v, ctx);
199
- if (props.action?.url) props.action = { ...props.action, url: resolve(props.action.url, ctx) };
200
- }
201
- const children = (node.children || []).map((c) => resolveView(c, ctx)).filter(Boolean);
202
- return { component: node.component, props, ...(children.length ? { children } : {}) };
203
- }
204
-
205
- /** UI_<SURFACE> (POST /api/integrations/:id/actions/UI_<SURFACE>) -> { surface, view }: what a host renderer draws. */
206
- async function renderSurface({ def, installation, surfaceId, record, ports }) {
207
- const u = (def.ui || []).find((x) => x.id === surfaceId);
208
- if (!u) throw new Error(`Unknown UI surface "${surfaceId}"`);
209
- const data = await runQuery({ def, installation, queryId: u.data.query, record, ports });
210
- const install = installContext(def, installation);
211
- return { surface: { id: u.id, host: u.host, placement: u.placement, definition: `${def.id}@${def.version}` }, view: resolveView(u.view, { data, record, install }) };
212
- }
213
-
214
- async function runLifecycle({ def, installation, hook, ports }) {
215
- const steps = def.lifecycle?.[hook] || [];
216
- if (!steps.length) return { status: 'completed', steps: {}, trace: [] };
217
- const install = installContext(def, installation);
218
- return execute(steps, { def, installation, ctx: { install, event: {}, record: null }, ports });
219
- }
220
-
221
- /**
222
- * The choices for an option setting, for the configure form. `options` is "module.list" or
223
- * { source: "module.list", with: { <input>: "$.settings.<other setting>" } } — the list's capability is called with
224
- * those inputs, resolved from the values the user has chosen so far, so a later dropdown depends on an earlier one
225
- * (Asana projects for the chosen workspace). Returns [] until every required input has a value.
226
- *
227
- * @param {{ def, manifests, ports, path: string, values?: object, integrationSettings?: object }} p
228
- * path: "settings.project" or "workflows.<id>.settings.<key>"; values: the form's current values at that level;
229
- * integrationSettings: the integration-level values, which a workflow setting's inputs read as $.install
230
- * @returns {Promise<Array<{ label: string, value: any }>>}
231
- */
232
- async function resolveOptions({ def, manifests, ports, path, values = {}, integrationSettings = {} }) {
233
- const segs = path.split('.');
234
- const setting = segs[0] === 'workflows'
235
- ? (def.workflows || []).find((w) => w.id === segs[1])?.settings?.[segs[3]]
236
- : def.settings?.[segs[1]];
237
- if (!setting || setting.type !== 'option') throw new Error(`"${path}" is not an option setting`);
238
- const ref = typeof setting.options === 'string' ? { source: setting.options } : setting.options;
239
- const [moduleKey, listName] = ref.source.split('.');
240
- const list = manifests[def.modules[moduleKey]?.module]?.options?.[listName];
241
- if (!list) throw new Error(`Module "${moduleKey}" has no option list "${listName}"`);
242
- const ctx = { settings: values, install: installContext(def, { settings: segs[0] === 'settings' ? values : integrationSettings }) };
243
- const inputs = resolve(ref.with || {}, ctx);
244
- for (const [name, spec] of Object.entries(list.inputs || {})) if (spec.required && (inputs[name] == null || inputs[name] === '')) return [];
245
- const items = await ports.call(moduleKey, list.capability, inputs);
246
- return (Array.isArray(items) ? items : []).map((item) => ({ label: String(resolve(list.label, { item }) ?? ''), value: resolve(list.value, { item }) }));
247
- }
248
-
249
- /**
250
- * $.install, the same everywhere a definition runs (workflows, queries, UI, lifecycle, option lists): the integration
251
- * settings with their defaults, the installation's webhook URL and id, `accounts` (per module key, the account fields
252
- * its manifest declares, e.g. $.install.accounts.quickbooks.realmId) and `runtime.installed` (each onInstall step's
253
- * result, by step id, so onUninstall can undo it).
254
- */
255
- function installContext(def, installation = {}) {
256
- return {
257
- ...defaultsOf(def.settings), ...(installation.settings || {}),
258
- webhookUrl: installation.webhookUrl, integrationId: installation.integrationId, portalId: installation.portalId,
259
- accounts: installation.accounts || {},
260
- runtime: { installed: installation.runtime?.installed || {} },
261
- };
262
- }
263
-
264
- function defaultsOf(settings) {
265
- return Object.fromEntries(Object.entries(settings || {}).filter(([, s]) => s.default !== undefined).map(([k, s]) => [k, s.default]));
266
- }
267
-
268
- module.exports = { argumentsFor, makePorts, runWorkflow, resumeWorkflow, runQuery, renderSurface, resolveView, runLifecycle, resolveOptions, installContext, recordOf, defaultsOf };