@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,335 +0,0 @@
1
- /**
2
- * `defineIntegration` (ADR-054 §1): the code front door to the integration runtime.
3
- *
4
- * It returns an ordinary IntegrationBase subclass with a static `Definition` and an instance `events` map: the same
5
- * compile target an ADR-063 integration document reaches through `fromDefinitionDocument` (definitions/). Builders,
6
- * routers, the queue worker, the ADR-055 trigger tick and the management API read that target and nothing else, so
7
- * the two front doors share one runtime; neither is a serialisation of the other.
8
- *
9
- * const DealAlerts = defineIntegration({
10
- * name: 'deal-alerts', version: '1.0.0', display: { label, description, category },
11
- * modules: { crm: use(hubspot, { webhooks: { subscribe: [...] } }), chat: use(slack) },
12
- * config: { channel: slack.fields.channel({ required: true }), minAmount: { type: 'number', default: 0 } },
13
- * on: { 'crm.deal.changed': async ({ deal, changes }, { modules: { crm, chat }, config, log }) => { … } },
14
- * triggers: { digest: { cron: '0 8 * * 1-5', timezone: 'UTC', handler: async (payload, ctx) => { … } } },
15
- * actions: { SEND_TEST: { title: 'Send a test', run: async (input, ctx) => { … } } },
16
- * visible: (context) => true,
17
- * });
18
- *
19
- * Handlers are `(payload, ctx)` and are called without `this`. `ctx` has a fixed set of core-owned keys:
20
- * { modules: { <key>: api }, config, log, telemetry, commands, integration }
21
- * The keys inside `ctx.modules` are the author's (`crm`, `chat`); nothing else in `ctx` is.
22
- *
23
- * Compiles to:
24
- * Definition.modules { crm: { definition: hubspot.Definition }, chat: { definition: slack.Definition } }
25
- * Definition.extensions { 'crm-webhooks': <contract-1 binding> } (extension-binding.js)
26
- * Definition.triggers { digest, 'crm.<ext trigger>' } (ADR-055, run by Stack B's tick)
27
- * this.events SEND_TEST (USER_ACTION) + the extension's queue events (merged at initialize)
28
- * getConfigOptions() { jsonSchema, uiSchema } with module field choices loaded from the account
29
- */
30
- const { IntegrationBase } = require('../integration-base');
31
- const { validateTrigger } = require('../triggers');
32
- const { normalizeBinding } = require('./use');
33
- const { validateExtension, isContract2 } = require('./define-extension');
34
- const { compileBinding, resolveOptions } = require('./extension-binding');
35
- const { compileConfig, resolveConfig, configOptions } = require('./config');
36
- const { check, withDefaults } = require('./json-schema-lite');
37
-
38
- const SPEC_KEYS = new Set(['name', 'version', 'display', 'usage', 'modules', 'config', 'on', 'triggers', 'actions', 'visible', 'webhooks', 'routes']);
39
- const NAME = /^[a-z0-9][a-z0-9-]*$/;
40
- const ACTION_KEY = /^[A-Z][A-Z0-9_]*$/;
41
- const PLAN = Symbol.for('frigg.define.plan');
42
-
43
- /**
44
- * Module keys that cannot be used: IntegrationBase attaches each module to the instance under its key
45
- * (`_appendModules`: `this[key] = module`), so a key that names an instance field or method would overwrite it.
46
- * Inside `ctx.modules` any name is fine; the instance is the constraint.
47
- */
48
- const INSTANCE_FIELDS = ['id', 'userId', 'entities', 'config', 'status', 'version', 'messages', 'modules', 'record', 'events', 'on',
49
- 'defaultEvents', 'logger', 'telemetry', 'def', 'compiled', 'ports', 'integrationRepository', 'integrationMappingRepository',
50
- 'updateIntegrationStatus', 'updateIntegrationMessages', 'updateIntegrationConfig', 'patchIntegrationConfig', 'triggerProcessRepository',
51
- 'commands', 'constructor'];
52
- let reservedKeys;
53
- function isReservedModuleKey(key) {
54
- if (!reservedKeys) {
55
- reservedKeys = new Set(INSTANCE_FIELDS);
56
- for (let p = IntegrationBase.prototype; p && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
57
- for (const n of Object.getOwnPropertyNames(p)) reservedKeys.add(n);
58
- }
59
- for (const n of ['handlerContext', 'resolvedConfig', 'runEmitted', 'syncInstallationSubscriptions']) reservedKeys.add(n);
60
- }
61
- return reservedKeys.has(key) || key.startsWith('_');
62
- }
63
-
64
- const pascal = (s) => s.split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join('') || 'Integration';
65
-
66
- function fail(name, msg) {
67
- throw new Error(`defineIntegration(${name ? `"${name}"` : ''}): ${msg}`);
68
- }
69
-
70
- /** Every module binding, each enabled extension validated and compiled. */
71
- function compileModules(name, modules) {
72
- if (!modules || typeof modules !== 'object' || !Object.keys(modules).length) fail(name, '"modules" must bind at least one API module');
73
- const bindings = [];
74
- const extensionBindings = [];
75
- const definitionModules = {};
76
- const contract1 = {};
77
- for (const [key, value] of Object.entries(modules)) {
78
- if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) fail(name, `module key "${key}" must be an identifier (it is ctx.modules.${key})`);
79
- if (isReservedModuleKey(key)) fail(name, `module key "${key}" names a member of IntegrationBase, where modules are also attached (this.${key}); choose another key`);
80
- const b = normalizeBinding(key, value);
81
- bindings.push(b);
82
- definitionModules[key] = { definition: b.module.Definition };
83
- for (const [extKey, options] of Object.entries(b.extensions)) {
84
- if (options === false || options === undefined) continue;
85
- const ext = b.module.extensions?.[extKey];
86
- if (!ext) fail(name, `modules.${key}: ${b.moduleName} has no extension "${extKey}" (it has ${Object.keys(b.module.extensions || {}).join(', ') || 'none'})`);
87
- validateExtension(ext, { where: `defineIntegration("${name}") modules.${key}` });
88
- if (!isContract2(ext)) {
89
- // A contract-1 bundle binds as it does today, under the derived key; its event names stay global.
90
- if (options !== true && Object.keys(options || {}).length) fail(name, `modules.${key}: extension "${extKey}" is contract 1 and takes no options`);
91
- contract1[`${key}-${extKey}`] = { extension: ext };
92
- continue;
93
- }
94
- const raw = options === true ? {} : options;
95
- // Function values depend on instance config (ADR-054 §4): present now, checked once resolved.
96
- const problems = check(ext.options, withDefaults(ext.options, raw));
97
- if (problems.length) fail(name, `use(${b.moduleName}, { ${extKey} }): ${problems.join('; ')}`);
98
- extensionBindings.push({ moduleKey: key, extKey, ext, module: b.module, moduleName: b.moduleName, options: raw });
99
- }
100
- }
101
- return { bindings, extensionBindings, definitionModules, contract1 };
102
- }
103
-
104
- function compileTriggers(name, triggers = {}, extensionBindings) {
105
- if (typeof triggers !== 'object' || Array.isArray(triggers)) fail(name, '"triggers" must be an object keyed by trigger name');
106
- const out = {};
107
- const add = (triggerName, t, handlerOf) => {
108
- if (out[triggerName]) fail(name, `trigger "${triggerName}" is declared twice`);
109
- if (typeof t?.handler !== 'function') fail(name, `trigger "${triggerName}": "handler" must be a function (payload, ctx)`);
110
- const problems = validateTrigger(triggerName, t);
111
- if (problems.length) fail(name, problems.join('; '));
112
- out[triggerName] = { ...t, handler: handlerOf(t.handler) };
113
- };
114
- // Called by IntegrationBase#onTriggerRun with `this` = the hydrated installation; the author's handler gets ctx.
115
- const wrap = (fn) => function runTrigger(payload) { return fn(payload, this.handlerContext()); };
116
- for (const [n, t] of Object.entries(triggers)) {
117
- if (n.includes('.')) fail(name, `trigger "${n}": dotted names are reserved for module extensions (<moduleKey>.<name>)`);
118
- add(n, t, wrap);
119
- }
120
- for (const b of extensionBindings) {
121
- for (const [n, t] of Object.entries(b.ext.triggers || {})) {
122
- // An extension trigger gets the extension ctx (its own api and options), not the integration's.
123
- add(`${b.moduleKey}.${n}`, t, (fn) => function runExtensionTrigger(payload) {
124
- const config = this.resolvedConfig();
125
- return fn(payload, this.handlerContext({ api: this[b.moduleKey]?.api, options: resolveOptions(b, config) }));
126
- });
127
- }
128
- }
129
- return out;
130
- }
131
-
132
- function compileActions(name, actions = {}) {
133
- if (typeof actions !== 'object' || Array.isArray(actions)) fail(name, '"actions" must be an object keyed by action id');
134
- const out = {};
135
- for (const [id, a] of Object.entries(actions)) {
136
- if (!ACTION_KEY.test(id)) fail(name, `action "${id}": ids are UPPER_SNAKE_CASE (they are USER_ACTION event names)`);
137
- if (typeof a?.run !== 'function') fail(name, `action "${id}": "run" must be a function (input, ctx)`);
138
- if (typeof a.title !== 'string' || !a.title) fail(name, `action "${id}": needs a "title"`);
139
- out[id] = a;
140
- }
141
- return out;
142
- }
143
-
144
- function defineIntegration(spec = {}) {
145
- const { name } = spec;
146
- if (typeof name !== 'string' || !NAME.test(name)) fail(name, '"name" must be lower-kebab-case (it names routes, functions and queues)');
147
- for (const key of Object.keys(spec)) if (!SPEC_KEYS.has(key)) fail(name, `unknown key "${key}"`);
148
- if (spec.version !== undefined && typeof spec.version !== 'string') fail(name, '"version" must be a string');
149
- if (spec.visible !== undefined && typeof spec.visible !== 'function') fail(name, '"visible" must be a function (context) => boolean');
150
-
151
- const { bindings, extensionBindings, definitionModules, contract1 } = compileModules(name, spec.modules);
152
- const extensions = { ...contract1 };
153
- const emitted = new Map(); // 'crm.deal.changed' -> { binding, name }
154
- const internalEvents = new Set();
155
- for (const b of extensionBindings) {
156
- const c = compileBinding(b);
157
- if (extensions[c.key]) fail(name, `two bindings derive the key "${c.key}"`);
158
- extensions[c.key] = c.compiled;
159
- for (const ev of Object.keys(c.compiled.extension.events)) internalEvents.add(ev);
160
- for (const n of Object.keys(b.ext.emits || {})) emitted.set(`${b.moduleKey}.${n}`, { binding: b, name: n });
161
- }
162
-
163
- const on = spec.on || {};
164
- if (typeof on !== 'object' || Array.isArray(on)) fail(name, '"on" must be an object keyed by event name');
165
- for (const [event, handler] of Object.entries(on)) {
166
- if (typeof handler !== 'function') fail(name, `on["${event}"]: handlers are functions (payload, ctx); method names are not accepted`);
167
- if (!emitted.has(event)) {
168
- const known = [...emitted.keys()];
169
- fail(name, `on["${event}"]: no bound extension emits it${known.length ? ` (bound extensions emit ${known.join(', ')})` : ' (no bound extension emits events)'}`);
170
- }
171
- }
172
- const actions = compileActions(name, spec.actions);
173
- for (const id of Object.keys(actions)) if (internalEvents.has(id)) fail(name, `action "${id}" collides with an extension event`);
174
- const triggers = compileTriggers(name, spec.triggers, extensionBindings);
175
- const configPlan = compileConfig(spec.config, bindings, `defineIntegration("${name}")`);
176
-
177
- const plan = Object.freeze({ name, bindings, extensionBindings, on: { ...on }, actions, configPlan, emitted });
178
-
179
- const Definition = {
180
- name,
181
- version: spec.version || '1.0.0',
182
- modules: definitionModules,
183
- display: { label: name, description: '', category: '', ...(spec.display || {}) },
184
- ...(spec.usage ? { usage: spec.usage } : {}),
185
- ...(Object.keys(extensions).length ? { extensions } : {}),
186
- ...(Object.keys(triggers).length ? { triggers } : {}),
187
- ...(spec.webhooks !== undefined ? { webhooks: spec.webhooks } : {}),
188
- ...(spec.routes ? { routes: spec.routes } : {}),
189
- };
190
-
191
- class DefinedIntegration extends IntegrationBase {
192
- static Definition = Definition;
193
-
194
- static [PLAN] = plan;
195
-
196
- constructor(params) {
197
- super(params);
198
- for (const [id, a] of Object.entries(actions)) {
199
- this.events[id] = {
200
- type: 'USER_ACTION',
201
- userActionType: a.userActionType || 'DATA_ACTION',
202
- title: a.title,
203
- ...(a.description ? { description: a.description } : {}),
204
- handler: (input) => a.run(input ?? {}, this.handlerContext()),
205
- };
206
- }
207
- }
208
-
209
- /** The stored config with the form's defaults filled in: what handlers see as ctx.config. */
210
- resolvedConfig() {
211
- return resolveConfig(plan.configPlan, this.config || {});
212
- }
213
-
214
- /**
215
- * The context every handler gets (ADR-054 §1). Built per dispatch, frozen. `integration` is a read-only
216
- * record (ADR-054 open question 6, lean: read-only); `commands` is created on first use. `extra` adds the
217
- * extension keys (`api`, `options`) for an extension's own handlers.
218
- */
219
- handlerContext(extra = {}) {
220
- const self = this;
221
- const modules = {};
222
- for (const b of plan.bindings) {
223
- Object.defineProperty(modules, b.key, { enumerable: true, get: () => self[b.key]?.api });
224
- }
225
- let commands;
226
- const integration = Object.freeze({
227
- id: this.id ?? null,
228
- name: plan.name,
229
- status: this.status ?? null,
230
- userId: this.userId ?? null,
231
- entities: this.entities ?? null,
232
- version: this.version ?? Definition.version,
233
- });
234
- const ctx = {
235
- modules: Object.freeze(modules),
236
- config: Object.freeze(this.resolvedConfig()),
237
- log: this.logger,
238
- telemetry: this.telemetry,
239
- integration,
240
- ...extra,
241
- };
242
- Object.defineProperty(ctx, 'commands', {
243
- enumerable: true,
244
- get() {
245
- if (!commands) {
246
- const { createFriggCommands } = require('../../application');
247
- commands = createFriggCommands({ integrationClass: self.constructor });
248
- }
249
- return commands;
250
- },
251
- });
252
- return Object.freeze(ctx);
253
- }
254
-
255
- /**
256
- * Run one emitted event: the `on` handler, else the extension's default handler, else ack and count it.
257
- * @returns {Promise<'handled'|'unhandled'|'undeclared'>}
258
- */
259
- async runEmitted(binding, eventName, payload) {
260
- const full = `${binding.moduleKey}.${eventName}`;
261
- if (!binding.ext.emits || !Object.prototype.hasOwnProperty.call(binding.ext.emits, eventName)) {
262
- this.logger.warn('Extension produced an event it does not declare', { eventName: 'frigg.extension.event_undeclared', event: full });
263
- return 'undeclared';
264
- }
265
- const handler = plan.on[full] || binding.ext.emits[eventName].handler;
266
- if (!handler) {
267
- try { this.telemetry?.event?.('frigg.extension.event_unhandled', { event: full }); } catch { /* telemetry never breaks dispatch */ }
268
- this.logger.info('Event has no handler', { eventName: 'frigg.extension.event_unhandled', event: full });
269
- return 'unhandled';
270
- }
271
- await handler(payload, this.handlerContext());
272
- return 'handled';
273
- }
274
-
275
- /*
276
- * Lifecycle invoker for app-scope subscriptions (ADR-054 §2, amended): after the base hook, an installation
277
- * whose binding options are functions of its config gets the subscriptions it needs, add-only. A failure is
278
- * a warning on the installation, never a failed create or update.
279
- * TODO(ADR-054 §5): move these call sites into the lifecycle dispatcher when it lands.
280
- */
281
- async onCreate(params) {
282
- await super.onCreate(params);
283
- await this.syncInstallationSubscriptions('create');
284
- }
285
-
286
- async onUpdate(params) {
287
- await super.onUpdate(params);
288
- await this.syncInstallationSubscriptions('update');
289
- }
290
-
291
- async onDelete(params) {
292
- await super.onDelete(params);
293
- await this.syncInstallationSubscriptions('delete');
294
- }
295
-
296
- async syncInstallationSubscriptions(event) {
297
- const { ensureInstallationSubscriptions, hasFunctionOption } = require('./subscriptions');
298
- const bindings = plan.extensionBindings.filter((b) => b.ext.subscriptions?.scope === 'app' && hasFunctionOption(b));
299
- if (!bindings.length) return [];
300
- if (event === 'delete') {
301
- // Add-only: nothing to create for an installation that is going away, and removing needs the whole
302
- // app's union, so its subscriptions stay until a reconcile runs with prune.
303
- this.logger.info('Subscriptions kept on delete; a pruning reconcile removes the ones no installation needs', { eventName: 'frigg.subscriptions.kept_on_delete', extensions: bindings.map((b) => b.ext.name) });
304
- return [];
305
- }
306
- if (this.status && this.status !== 'ENABLED') return [];
307
- try {
308
- return await ensureInstallationSubscriptions(this);
309
- } catch (error) {
310
- this.logger.warn('Webhook subscriptions not created', { eventName: 'frigg.subscriptions.ensure_failed', event, error });
311
- try {
312
- await this.updateIntegrationMessages?.execute(this.id, 'warnings', 'Webhook subscriptions not created', 'The provider subscriptions this installation needs could not be created. They are retried on the next config save or reconcile.', Date.now());
313
- } catch { /* a report, never a failure */ }
314
- return [];
315
- }
316
- }
317
-
318
- async getConfigOptions() {
319
- return configOptions(plan.configPlan, (key) => this[key]?.api, this.config || {}, this.logger);
320
- }
321
-
322
- async refreshConfigOptions(params = {}) {
323
- const values = params?.data ?? params?.formData ?? params ?? {};
324
- return configOptions(plan.configPlan, (key) => this[key]?.api, { ...(this.config || {}), ...values }, this.logger);
325
- }
326
- }
327
- Object.defineProperty(DefinedIntegration, 'name', { value: pascal(name) });
328
- if (spec.visible) DefinedIntegration.visible = spec.visible;
329
- return DefinedIntegration;
330
- }
331
-
332
- /** The compiled plan of a class made by defineIntegration, or null for any other class. */
333
- const planOf = (Cls) => (Cls && Object.prototype.hasOwnProperty.call(Cls, PLAN) ? Cls[PLAN] : null);
334
-
335
- module.exports = { defineIntegration, planOf };
@@ -1,140 +0,0 @@
1
- // =====================================================================================
2
- // (a) ONE-WAY, EVENT-DRIVEN: a HubSpot deal enters a chosen stage -> post to Slack.
3
- // This is the WHOLE app: backend/index.js (ADR-054 Appendix A, unchanged except these notes).
4
- // App code: ~70 lines. Nothing else in the repo but package.json and infrastructure.js.
5
- // Runs end to end in tests/example-deal-alerts.test.js (HubSpot and Slack nock'd).
6
- // =====================================================================================
7
- const { defineApp, defineIntegration, use } = require('@friggframework/core');
8
- const hubspot = require('@friggframework/api-module-hubspot');
9
- const slack = require('@friggframework/api-module-slack');
10
-
11
- const DealAlerts = defineIntegration({
12
- name: 'deal-alerts',
13
- version: '1.0.0',
14
- display: {
15
- label: 'Deal alerts in Slack',
16
- description: 'Posts to a Slack channel when a HubSpot deal reaches a chosen stage.',
17
- category: 'Notifications',
18
- },
19
-
20
- // The module key (crm, chat) is the namespace for everything a module's extensions
21
- // contribute: routes (/api/deal-alerts-integration/crm-webhooks/...), events
22
- // ('crm.deal.changed'), config fields, and admin scripts.
23
- modules: {
24
- crm: use(hubspot, {
25
- webhooks: {
26
- subscribe: ['deal.creation', 'deal.propertyChange:dealstage'],
27
- hydrate: ['dealname', 'amount', 'dealstage', 'pipeline', 'hubspot_owner_id'],
28
- },
29
- }),
30
- chat: use(slack),
31
- },
32
-
33
- // Per-install form shown to the end user. Module fields load their options from
34
- // the connected account (Slack channels, HubSpot pipelines and stages).
35
- config: {
36
- channel: slack.fields.channel({ title: 'Post to channel', required: true }),
37
- stages: hubspot.fields.dealStages({
38
- title: 'Alert when a deal enters',
39
- multiple: true,
40
- default: ['closedwon'],
41
- }),
42
- minAmount: { type: 'number', title: 'Only deals worth at least', default: 0 },
43
- mentionOwner: { type: 'boolean', title: 'Mention the deal owner', default: true },
44
- },
45
-
46
- // Handlers take (payload, ctx). ctx.modules holds the APIs under the keys chosen
47
- // above (crm, chat); every other ctx key is core's and fixed.
48
- on: {
49
- // Emitted by hubspot.extensions.webhooks after it verified the v3 signature,
50
- // resolved portalId -> this integration, and batch-read the deal (hydrate).
51
- async 'crm.deal.changed'({ deal, changes }, { modules: { crm, chat }, config, log }) {
52
- const stage = changes.dealstage?.to ?? deal.properties.dealstage;
53
- if (!config.stages.includes(stage)) return;
54
- if (Number(deal.properties.amount ?? 0) < config.minAmount) return;
55
-
56
- let mention = '';
57
- if (config.mentionOwner && deal.properties.hubspot_owner_id) {
58
- const owner = await crm.getOwnerById(deal.properties.hubspot_owner_id);
59
- const user = owner?.email && (await chat.lookupUserByEmail(owner.email).catch(() => null));
60
- if (user?.user?.id) mention = ` <@${user.user.id}>`;
61
- }
62
-
63
- await chat.postMessage({
64
- channel: config.channel,
65
- text: `Deal *${deal.properties.dealname}* moved to ${stage}${mention}`,
66
- blocks: [
67
- { type: 'section', text: { type: 'mrkdwn',
68
- text: `*${deal.properties.dealname}* ${stage}\nAmount: ${deal.properties.amount ?? 'n/a'}${mention}` } },
69
- { type: 'actions', elements: [{ type: 'button', text: { type: 'plain_text', text: 'Open in HubSpot' },
70
- url: crm.urls.record('deal', deal.id) }] },
71
- ],
72
- });
73
- log.info({ dealId: deal.id, stage }, 'deal alert posted');
74
- },
75
- },
76
-
77
- actions: {
78
- SEND_TEST_ALERT: {
79
- title: 'Send a test message',
80
- async run(_input, { modules: { chat }, config }) {
81
- await chat.postMessage({ channel: config.channel, text: 'Deal alerts are connected.' });
82
- return { ok: true };
83
- },
84
- },
85
- },
86
- });
87
-
88
- module.exports = defineApp({
89
- name: 'deal-alerts',
90
- integrations: [DealAlerts],
91
- database: { postgres: { enable: true } },
92
- encryption: { fieldLevelEncryptionMethod: 'kms' },
93
- });
94
-
95
- // =====================================================================================
96
- // WHAT EACH IMPORT CONTRIBUTES (none of it is written in this file)
97
- //
98
- // @friggframework/core
99
- // defineIntegration -> an IntegrationBase subclass with a static Definition, so every
100
- // existing builder, router, worker and the management API work unchanged.
101
- // modules -> Definition.modules; use(hubspot, { webhooks }) -> Definition.extensions['crm-webhooks'],
102
- // the contract-1 binding next already deploys (route /crm-webhooks/webhooks, function
103
- // deal-alerts__crmwebhooks). The same compile target as an ADR-063 integration document.
104
- // on/actions -> this.events (LIFE_CYCLE_EVENT / USER_ACTION), handler(payload, ctx),
105
- // ctx = { modules: { crm, chat }, config, log, telemetry, commands, integration }.
106
- // No `this`. The keys inside ctx.modules are this file's; the rest are core's.
107
- // config -> getConfigOptions() jsonSchema/uiSchema + validateConfig() (NEEDS_CONFIG).
108
- // Queue worker, DLQ, retries, delivery.isLastAttempt, structured logs, telemetry,
109
- // usage counters, encryption, token refresh: already on next.
110
- //
111
- // @friggframework/api-module-hubspot
112
- // Definition: OAuth2, scopes, entity identity (portalId = externalId).
113
- // Api: getOwnerById, batch reads, urls.record(); static rateLimit (ADR-049, descriptive).
114
- // fields.dealStages: config field with options loaded from the connected portal.
115
- // extensions.webhooks (contract 2):
116
- // route POST /webhooks, useDatabase: false, verify: HubSpot v3 HMAC + 5 min skew
117
- // resolve: portalId -> integration (the RESOLVE hop the extension does by hand today)
118
- // subscriptions: scope 'app' (HubSpot v3 subscriptions are per developer app);
119
- // desired set = union of every installed binding's `subscribe`; reconcileAppSubscriptions()
120
- // creates what is missing (add-only; `prune` removes what no binding needs). Runs after
121
- // `frigg deploy`, on installation create/update when options depend on config, and from
122
- // the admin script. One HubSpot developer app per stage.
123
- // emits: deal.created | deal.changed {deal, changes} | deal.deleted (+ contact.*, company.*)
124
- // hydrate: batch-reads the object with the listed properties before dispatch.
125
- // env: HUBSPOT_CLIENT_SECRET, HUBSPOT_APP_ID, HUBSPOT_DEVELOPER_API_KEY (ADR-027 scoping).
126
- // scripts: hubspot.reconcile-subscriptions (prune, dryRun), appended by defineApp;
127
- // hubspot.replay-webhook is a follow-up.
128
- //
129
- // @friggframework/api-module-slack
130
- // Definition: OAuth2 bot install, team_id = externalId.
131
- // Api: postMessage, lookupUserByEmail; static rateLimit (tiered, per-method).
132
- // fields.channel: config field with options from conversations.list.
133
- // (extensions.events — Slack Events API receiver with url_verification handshake — is
134
- // available but not enabled here, so no route is deployed for it.)
135
- //
136
- // Provisioned by devtools from the Definition (no infrastructure code in the app):
137
- // deal-alerts (HTTP catch-all), deal-alerts__crmwebhooks (receiver, no Prisma layer),
138
- // deal-alerts QueueWorker + queue + DLQ, and the admin-script functions that run
139
- // hubspot.reconcile-subscriptions.
140
- // =====================================================================================
@@ -1,174 +0,0 @@
1
- /**
2
- * A contract-2 extension enabled on a module binding, compiled into the contract-1 binding that `next` already runs
3
- * (ADR-054 §3). Nothing downstream changes: devtools gives the binding its own function, the defined-routes router
4
- * mounts it under the binding key, `_mergeExtensions` merges its events, and the queue worker dispatches them.
5
- *
6
- * For `modules: { crm: use(hubspot, { webhooks }) }`:
7
- *
8
- * binding key crm-webhooks route /api/{integration}-integration/crm-webhooks/webhooks
9
- * function {integration}__crmwebhooks (devtools, from Definition.extensions as today)
10
- * queue events CRM__WEBHOOKS__RECEIVED (receipt, no DB) -> CRM__WEBHOOKS__RESOLVE (worker, dry)
11
- * -> CRM__WEBHOOKS__DISPATCH (worker, the installation hydrated)
12
- * CRM__WEBHOOKS__DEAL_CHANGED, … one per emitted event (replays, tests)
13
- * integration events crm.deal.changed, … (the `on` map)
14
- * triggers crm.<name> (merged into Definition.triggers)
15
- *
16
- * Every name derives from the module key, so two modules that both emit `contact.changed`, or the same module bound
17
- * twice, never collide; no bare name reaches the global event map.
18
- */
19
- const { check, withDefaults } = require('./json-schema-lite');
20
-
21
- const upper = (s) => String(s).replace(/[^A-Za-z0-9]+/g, '_').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
22
- const bindingKeyOf = (moduleKey, extKey) => `${moduleKey}-${extKey}`;
23
- const eventPrefixOf = (moduleKey, extKey) => `${upper(moduleKey)}__${upper(extKey)}__`;
24
-
25
- function envFor(ext) {
26
- return Object.fromEntries((ext.env || []).map((k) => [k, process.env[k]]));
27
- }
28
-
29
- /**
30
- * The binding's options for one installation: function values resolved with its config, defaults applied, then
31
- * checked against the extension's schema.
32
- */
33
- function resolveOptions(binding, config = {}) {
34
- const raw = binding.options === true ? {} : binding.options || {};
35
- const resolved = Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, typeof v === 'function' ? v(config) : v]));
36
- const out = withDefaults(binding.ext.options, resolved);
37
- const problems = check(binding.ext.options, out);
38
- if (problems.length) throw new Error(`use(${binding.moduleName}, { ${binding.extKey} }) for this installation: ${problems.join('; ')}`);
39
- return out;
40
- }
41
-
42
- /** The Api a verify method or an app-scope subscription runs on: the module's Api class, without a credential. */
43
- function appApiOf(binding) {
44
- const Api = binding.module.Definition?.API || binding.module.Api;
45
- return Api ? new Api({}) : null;
46
- }
47
-
48
- async function runVerify(binding, input, ctx) {
49
- const { ext } = binding;
50
- let out;
51
- if (typeof ext.verify === 'string') {
52
- const api = appApiOf(binding);
53
- if (!api || typeof api[ext.verify] !== 'function') return { valid: false, reason: `the module Api has no ${ext.verify}()` };
54
- out = await api[ext.verify](input);
55
- } else {
56
- out = await ext.verify(input, ctx);
57
- }
58
- if (typeof out === 'boolean') return { valid: out, reason: out ? undefined : 'signature check failed' };
59
- return { valid: Boolean(out?.valid), reason: out?.reason };
60
- }
61
-
62
- /** The installations an external account belongs to, among this integration type: the entity reverse lookup. */
63
- async function defaultLookup(IntegrationClass, externalId, moduleName) {
64
- const { createIntegrationCommands } = require('../../application/commands/integration-commands');
65
- const commands = createIntegrationCommands({ integrationClass: IntegrationClass });
66
- const ids = await commands.listIntegrationsByEntityExternalId(String(externalId), moduleName);
67
- const out = [];
68
- for (const integrationId of ids || []) {
69
- const found = await commands.loadIntegrationContextById(integrationId);
70
- const type = found?.context?.record?.config?.type;
71
- if (type === undefined || type === IntegrationClass.Definition.name) out.push(integrationId);
72
- }
73
- return out;
74
- }
75
-
76
- /**
77
- * @param {object} binding { moduleKey, extKey, ext, module, moduleName, options }
78
- * @param {object} plan the integration's compiled plan (on handlers), read at call time
79
- * @returns {{ key: string, compiled: object, emitted: string[], prefix: string }}
80
- */
81
- function compileBinding(binding) {
82
- const { moduleKey, extKey, ext } = binding;
83
- const key = bindingKeyOf(moduleKey, extKey);
84
- const prefix = eventPrefixOf(moduleKey, extKey);
85
- const RECEIVED = `${prefix}RECEIVED`;
86
- const RESOLVE = `${prefix}RESOLVE`;
87
- const DISPATCH = `${prefix}DISPATCH`;
88
- const emits = ext.emits || {};
89
- const emitted = Object.keys(emits).map((n) => `${moduleKey}.${n}`);
90
- const env = () => envFor(ext);
91
- const where = `${moduleKey}-${extKey}`;
92
-
93
- /** Receipt: handshake, verification, split. No database: the binding's function has no Prisma layer. */
94
- async function received({ req, res }) {
95
- const { webhookRequestFacts } = require('../integration-base');
96
- const input = { headers: req.headers || {}, body: req.body, query: req.query || {}, params: req.params || {}, ...webhookRequestFacts(req) };
97
- const ctx = { env: env(), options: binding.options, log: this.logger };
98
- if (ext.handshake) {
99
- const h = await ext.handshake(input, ctx);
100
- if (h) {
101
- for (const [k, v] of Object.entries(h.headers || {})) res.set(k, String(v));
102
- res.status(h.status || 200);
103
- return h.body === undefined ? {} : h.body;
104
- }
105
- }
106
- if (ext.verify) {
107
- const v = await runVerify(binding, input, ctx);
108
- if (!v.valid) {
109
- this.logger.warn('Webhook rejected', { eventName: 'frigg.extension.webhook_rejected', binding: where, reason: v.reason });
110
- res.status(401);
111
- return { error: 'invalid signature' };
112
- }
113
- }
114
- const items = (await ext.receive(input, ctx)) || [];
115
- let queued = 0;
116
- for (const item of items) {
117
- if (item?.externalId === undefined || item?.externalId === null) {
118
- this.logger.warn('Webhook item without an account id skipped', { eventName: 'frigg.extension.webhook_item_skipped', binding: where });
119
- continue;
120
- }
121
- await this.queueWebhook({ event: RESOLVE, externalId: String(item.externalId), body: item.body });
122
- queued += 1;
123
- }
124
- return { received: items.length, queued };
125
- }
126
-
127
- /** Worker, dry instance: which installations of this integration the account belongs to; one message each. */
128
- async function resolve({ data = {} } = {}) {
129
- const Cls = this.constructor;
130
- const lookup = (externalId, moduleName) => (Cls.lookupInstallations || defaultLookup)(Cls, externalId, moduleName);
131
- const ids = typeof ext.resolve === 'function'
132
- ? await ext.resolve({ externalId: data.externalId, body: data.body }, { lookup, env: env(), log: this.logger })
133
- : await lookup(data.externalId, ext.resolve.module);
134
- if (!ids?.length) {
135
- this.logger.info('Webhook for an account with no installation', { eventName: 'frigg.extension.webhook_unresolved', binding: where });
136
- return { resolved: 0 };
137
- }
138
- for (const integrationId of ids) await this.queueWebhook({ event: DISPATCH, integrationId, body: data.body });
139
- return { resolved: ids.length };
140
- }
141
-
142
- /** Worker, the installation hydrated: normalize into named events and run the integration's handlers. */
143
- async function dispatch({ data = {} } = {}) {
144
- const config = this.resolvedConfig();
145
- const ctx = { api: this[moduleKey]?.api, options: resolveOptions(binding, config), config, env: env(), log: this.logger };
146
- const events = ext.normalize ? await ext.normalize([data.body], ctx) : [data.body];
147
- const results = [];
148
- for (const e of events || []) results.push(await this.runEmitted(binding, e?.name, e?.payload));
149
- return { dispatched: results.filter((r) => r === 'handled').length, events: results.length };
150
- }
151
-
152
- const events = {
153
- [RECEIVED]: { type: 'LIFE_CYCLE_EVENT', handler: received },
154
- [RESOLVE]: { type: 'LIFE_CYCLE_EVENT', handler: resolve },
155
- [DISPATCH]: { type: 'LIFE_CYCLE_EVENT', handler: dispatch },
156
- };
157
- for (const name of Object.keys(emits)) {
158
- events[`${prefix}${upper(name)}`] = {
159
- type: 'LIFE_CYCLE_EVENT',
160
- handler: async function emittedEvent({ data = {} } = {}) { return this.runEmitted(binding, name, data.payload); },
161
- };
162
- }
163
- const compiled = {
164
- extension: {
165
- name: ext.name,
166
- useDatabase: ext.useDatabase ?? false,
167
- routes: (ext.routes || []).map((r) => ({ path: r.path, method: r.method, event: RECEIVED })),
168
- events,
169
- },
170
- };
171
- return { key, compiled, emitted, prefix };
172
- }
173
-
174
- module.exports = { compileBinding, resolveOptions, appApiOf, runVerify, defaultLookup, bindingKeyOf, eventPrefixOf, envFor, upper };
@@ -1,11 +0,0 @@
1
- /**
2
- * One-file integrations (ADR-054, the stable event-driven path): `defineIntegration`, `defineApp`, `use`, and the
3
- * contract-2 extension helper. See DESIGN.md in this folder for how it shares the ADR-063 runtime.
4
- */
5
- const { defineIntegration, planOf } = require('./define-integration');
6
- const { defineApp } = require('./define-app');
7
- const { use } = require('./use');
8
- const { defineExtension, validateExtension, isContract2 } = require('./define-extension');
9
- const { reconcileAppSubscriptions, ensureInstallationSubscriptions, createReconcileSubscriptionsScript } = require('./subscriptions');
10
-
11
- module.exports = { defineIntegration, defineApp, use, defineExtension, validateExtension, isContract2, reconcileAppSubscriptions, ensureInstallationSubscriptions, createReconcileSubscriptionsScript, planOf };