@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,122 +0,0 @@
1
- # defineIntegration: design note
2
-
3
- ADR-054's stable, event-driven path (target file (a)) built on Stack B's integration runtime (ADR-063, #693–#700).
4
- The same decisions are recorded as an amendment in ADR-054 (#669).
5
-
6
- ## 1. One runtime, two front doors
7
-
8
- Both front doors compile to one target: an `IntegrationBase` subclass with a static `Definition` and an instance
9
- `events` map. Everything downstream reads only that target: the devtools builders, the defined-routes and webhook
10
- routers, the queue worker, the ADR-055 tick (`RunTriggerTick`, one tick, per-installation due decisions), the run
11
- (`IntegrationBase#onTriggerRun` with `RunTrigger`), `integration-types` and the management API.
12
-
13
- | | Code (`defineIntegration`, this folder) | Document (ADR-063, `definitions/`) |
14
- |---|---|---|
15
- | Entry | `defineIntegration({...})` | `fromDefinitionDocument(...)`, `definitionDocumentHost(...)`, `embedDefinitionDocument(Cls, ...)` |
16
- | `Definition.modules` | from `modules: { key: use(module) }` | from the document's `modules` |
17
- | Webhooks | contract-2 extension, compiled to a contract-1 `Definition.extensions` binding | capability manifest `webhook` block, `Definition.webhooks` |
18
- | Recurring work | `Definition.triggers` (ADR-055) | `triggersFor()` / `Definition.instanceTriggers` (ADR-055) |
19
- | Handlers | functions `(payload, ctx)` | workflow steps (interpreter), `code` steps |
20
-
21
- Neither is a serialisation of the other. They compose: `embedDefinitionDocument(DealAlerts, { definition })` gives a
22
- `defineIntegration` class a document's workflows, and both kinds of trigger come out of one `triggersFor` list
23
- (`tests/front-doors.test.js`).
24
-
25
- Base branch: Stack B's tip (`claude/shared-conversation-8hgmbr-stack-9-module-methods`). Its compile target was
26
- suitable as is; the only changes to Stack B code are additive (`webhookRequestFacts` exported, `static visible`
27
- honoured by `integration-types`, the document-door names below as aliases).
28
-
29
- ## 2. Names
30
-
31
- | Collision | Resolution |
32
- |---|---|
33
- | `defineIntegrationClass` beside `defineIntegration` | `defineIntegration` is the code front door. The document door is named for documents: `fromDefinitionDocument` (was `defineIntegrationClass`), `definitionDocumentHost` (was `defineSharedIntegrationClass`), `embedDefinitionDocument` (was `extendIntegrationClass`). The old names are aliases until Stack B merges. |
34
- | "Definition" means three things | `Definition` is only the static on the class (the compile target). The ADR-063 JSON is an *integration document* (its schema becomes `integration-document.v0.1.schema.json`). `config.definition` is the installation's *document pin*. |
35
- | workflow `trigger` vs ADR-055 `triggers` | One registry: `Definition.triggers`. `defineIntegration`'s `triggers` is that key (functions wrapped to get `ctx`); a contract-2 extension's `triggers` merge as `<moduleKey>.<name>` (dotted names are reserved for them). A document's `workflows[].trigger` is a workflow's start condition, not a registry; its `schedule` kind compiles into the same list through `triggersFor`. |
36
- | Webhook facts in contract 2 and in capability manifests | Declared once, by the module. Verification is an Api method (`verifyWebhookSignature(input)`, with #699's input `{ headers, body, query, rawBody, method, url }`); contract 2 may name it (`verify: 'verifyWebhookSignature'`) as a manifest's `webhook.verify` does. Emitted names, account resolution and the subscription set live in the contract-2 extension; a manifest's `webhook`/`events` blocks should be generated from it (follow-up), not written beside it. `use()` options (`subscribe`, `hydrate`) reference those facts and never restate them. |
37
-
38
- ## 3. API (as built)
39
-
40
- ```text
41
- defineIntegration({ name, version, display, usage, modules: { <key>: use(module, { <ext>: opts }) },
42
- config, on, triggers, actions, visible, webhooks?, routes? }) // -> IntegrationBase subclass
43
- defineApp({ name, integrations, ...appDefinition }) // -> { Definition }
44
- use(module, extensionOptions?) // core's, not the module's
45
- defineExtension(bundle) // optional; a plain object works
46
- reconcileAppSubscriptions({ integrations, listInstallations?, prune?, dryRun? }) // app-scope subscriptions, add-only unless prune
47
- createReconcileSubscriptionsScript({ name, extension }) // the admin script (prune, dryRun inputs)
48
- ```
49
-
50
- - Handlers are `(payload, ctx)`, called without `this`. `ctx = { modules, config, log, telemetry, commands,
51
- integration }`, frozen. `ctx.modules.<key>` is the installation's Api for that binding. `ctx.config` is the stored
52
- config with the form defaults. `ctx.integration` is a read-only record (`id, name, status, userId, entities,
53
- version`) — ADR-054 open question 6, lean taken. `commands` is created on first use.
54
- - Unknown keys, an `on` key no bound extension emits, method-name handlers, bad `use()` options, trigger problems,
55
- name collisions and a contract or core-range mismatch all throw when the class is defined.
56
- - **Module keys cannot name an `IntegrationBase` member** (`config`, `events`, `send`, `id`, …). `_appendModules`
57
- attaches each module to the instance as `this[key]`, so such a key would overwrite the member. Inside
58
- `ctx.modules` any name is fine; the instance is the constraint. (ADR-054 said any identifier works.)
59
- - `config` entries are JSON Schema fragments (`required: true` and `ui` are lifted) or **module fields**:
60
- `module.fields.<name>(opts)` returns `{ module, schema, required, ui, options(api, { config }) }`. Fields are a
61
- module export, not an extension key, because a module bound without an extension (Slack in (a)) offers them too.
62
- - `actions` become `USER_ACTION` events (`DATA_ACTION`), run as `run(input, ctx)`.
63
- - `visible(context)` becomes `static visible`; `integration-types` honours it when a caller passes a context.
64
-
65
- ## 4. Extension contract 2 (the stable minimum)
66
-
67
- A contract-2 extension enabled with `use()` compiles to a contract-1 binding under `<moduleKey>-<extKey>`, so devtools
68
- gives it its function (`deal-alerts__crmwebhooks`, no Prisma layer unless `useDatabase`), the router mounts its
69
- routes under `/api/<integration>-integration/crm-webhooks`, and the queue worker runs its events:
70
-
71
- 1. **Receipt** (`CRM__WEBHOOKS__RECEIVED`, no database): `handshake` (answered synchronously), `verify` (401 with a
72
- logged reason), `receive` splits the delivery into `{ externalId, body }` items, one `RESOLVE` message each.
73
- 2. **Resolve** (`CRM__WEBHOOKS__RESOLVE`, worker, dry): `resolve: { module }` looks up the installations of this
74
- integration type connected to the account (the entity reverse lookup, filtered by type) and queues one
75
- `DISPATCH` per installation. It fans out instead of throwing on two matches, as Stack B's app-level webhooks do.
76
- 3. **Dispatch** (`CRM__WEBHOOKS__DISPATCH`, worker, hydrated): `normalize(batch, { api, options, config, env, log })`
77
- turns the body into `{ name, payload }` events (the HubSpot module hydrates here, with the installation's
78
- credential); each runs `on['crm.<name>']`, else the extension's default handler, else it is acked and counted
79
- (`frigg.extension.event_unhandled`). `CRM__WEBHOOKS__DEAL_CHANGED` etc. exist for replays.
80
- 4. **Subscriptions, app scope**: `reconcileAppSubscriptions` takes the union of `desired(options)` over every class
81
- that binds the extension (per ENABLED installation when an option is a function of config), lists what the
82
- provider has and creates what is missing. **Add-only by default**: it removes what no binding needs only with
83
- `prune: true`, and only when it could read every installation's config (otherwise it reports `pruneSkipped`). A
84
- dry run reports `create` and `remove` and writes nothing. **Supported setup: one provider developer app per
85
- stage**, because subscriptions are app-wide; stages sharing one developer app would see, and with `prune` remove,
86
- each other's. It runs at three points, with no recurring trigger:
87
- - (a) **after deploy**: `frigg deploy` runs it add-only with no database (`--prune-subscriptions` prunes groups
88
- whose options are all static; `--skip-subscriptions` skips it). A failure is reported, never a failed deploy.
89
- - (b) **installation lifecycle**: on create and config update, an ENABLED installation whose binding options are
90
- functions of its config gets the subscriptions it needs (`ensureInstallationSubscriptions`, add-only). On delete
91
- nothing is called: removal needs the whole app's union, so it is a pruning reconcile's job. A failure is a
92
- warning on the installation. The call sites are `onCreate`/`onUpdate`/`onDelete` overrides until the §5
93
- dispatcher exists.
94
- - (c) **admin script**: `createReconcileSubscriptionsScript(...)` with `prune` and `dryRun` inputs, run in the
95
- stack where installations can be read. `defineApp` appends scripts that bound modules export (`module.scripts`);
96
- the HubSpot module exports `hubspot.reconcile-subscriptions`.
97
- 5. **One bundle, both contracts**: a contract-2 bundle may keep its contract-1 face (`events` and `routes[].event`),
98
- which a class binding it by hand through `Definition.extensions` (or an older core) runs. `defineIntegration`
99
- ignores both. That is how the HubSpot module moves `extensions.webhooks` to contract 2 without a major version.
100
- 6. **Options**: `use()` options are checked against the extension's `options` schema when the class is defined;
101
- function values are resolved with the installation's config and checked again.
102
-
103
- Not in this PR (checklist on the PR): extension `lifecycle` hooks and the §5 dispatcher (the subscription call sites
104
- move there), ADR-027 `env` scoping
105
- in devtools and `frigg validate`, `instance`-scope subscriptions (experimental), the Slack module promotion, and
106
- generating capability-manifest webhook blocks from contract 2.
107
-
108
- ## 5. Open questions
109
-
110
- 1. *(Decided: no recurring reconcile. It runs after deploy, on installation lifecycle, and from the admin script.)*
111
- 2. *(Decided: add-only by default, `prune` on request; one developer app per stage is the supported setup.)*
112
- Alternative under evaluation: HubSpot's Webhooks Journal API makes subscriptions per portal (installation scope),
113
- at the cost of a polling consumer. See the ADR-054 amendment.
114
- 3. **Where verification runs.** Contract 2 verifies at receipt (no database, app-scope secret from env). Stack B
115
- verifies manifest webhooks in the worker (raw body carried in the message, #699). One place for both doors? Lean:
116
- receipt when the secret is app-scope, the worker when it is per installation.
117
- 4. **Event names.** The HubSpot manifest names provider topics (`deal.propertyChange`); contract 2 emits normalized
118
- names (`deal.changed`, payload `{ deal, changes }`). Lean: manifest event keys become the contract-2 names, with
119
- the provider topic in `match`.
120
- 5. **Base trigger ctx.** `IntegrationBase#onTriggerRun` passes `{ modules: <module instances>, config, log,
121
- integration: this }` to function handlers on classic classes; `defineIntegration` gives its handlers the ADR-054
122
- ctx. Align the base to ADR-054 (`modules` -> Api) before Stack B merges?
@@ -1,98 +0,0 @@
1
- /**
2
- * `config` (ADR-054 §4): the end user's per-install form, compiled to what `getConfigOptions()` returns today
3
- * (`{ jsonSchema, uiSchema }`), so `validateConfig()`, NEEDS_CONFIG and the config/options routes work unchanged.
4
- *
5
- * An entry is either a JSON Schema fragment, with two conveniences:
6
- *
7
- * minAmount: { type: 'number', title: 'Only deals worth at least', default: 0, required: true, ui: { 'ui:help': '…' } }
8
- *
9
- * or a module field: what a module's `fields.<name>(opts)` returns. A field is a schema fragment plus a loader for
10
- * its choices, run with the installation's own credential for that module:
11
- *
12
- * { module: 'slack', schema: { type: 'string', title }, required, ui, options: async (api, { config }) => [{ const, title }] }
13
- *
14
- * The loader runs against the binding of that module. When a module is bound twice, the author picks one with
15
- * `{ ...slack.fields.channel(), from: 'chatA' }`. No field reaches a form unless the integration author put it in
16
- * `config`, which keeps ADR-054 §4's rule: an extension never adds to the end user's form by itself.
17
- */
18
- const { withDefaults } = require('./json-schema-lite');
19
-
20
- const isField = (v) => Boolean(v && typeof v === 'object' && v.schema && typeof v.schema === 'object' && typeof v.options === 'function');
21
-
22
- /**
23
- * @param {object} config the `config` given to defineIntegration
24
- * @param {Array<{ key, moduleName }>} bindings
25
- * @param {string} where for messages
26
- */
27
- function compileConfig(config = {}, bindings = [], where = 'defineIntegration') {
28
- if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error(`${where}: "config" must be an object keyed by field name`);
29
- const properties = {}, uiSchema = {}, required = [], fields = {};
30
- for (const [name, entry] of Object.entries(config)) {
31
- if (!entry || typeof entry !== 'object') throw new Error(`${where}: config.${name} must be a JSON Schema fragment or a module field`);
32
- if (isField(entry)) {
33
- const candidates = bindings.filter((b) => (entry.from ? b.key === entry.from : b.moduleName === entry.module));
34
- if (candidates.length === 0) {
35
- throw new Error(`${where}: config.${name} is a ${entry.module} field${entry.from ? ` from "${entry.from}"` : ''}, but no such module is bound in "modules"`);
36
- }
37
- if (candidates.length > 1) {
38
- throw new Error(`${where}: config.${name} is a ${entry.module} field and ${entry.module} is bound as ${candidates.map((b) => b.key).join(' and ')}; say which with { ...field, from: '<key>' }`);
39
- }
40
- fields[name] = { field: entry, moduleKey: candidates[0].key };
41
- properties[name] = { ...entry.schema };
42
- if (entry.ui) uiSchema[name] = { ...entry.ui };
43
- if (entry.required) required.push(name);
44
- continue;
45
- }
46
- const { required: isRequired, ui, ...schema } = entry;
47
- if (isRequired !== undefined && typeof isRequired !== 'boolean') throw new Error(`${where}: config.${name}.required must be true or false`);
48
- properties[name] = schema;
49
- if (ui) uiSchema[name] = { ...ui };
50
- if (isRequired) required.push(name);
51
- }
52
- const jsonSchema = { type: 'object', properties, ...(required.length ? { required } : {}) };
53
- return { jsonSchema, uiSchema, fields };
54
- }
55
-
56
- /** The config a handler sees: the stored config with the form's defaults filled in. */
57
- function resolveConfig(plan, stored = {}) {
58
- return withDefaults(plan.jsonSchema, stored || {});
59
- }
60
-
61
- const asChoice = (c) => (c && typeof c === 'object'
62
- ? { const: c.const ?? c.value ?? c.id, title: String(c.title ?? c.label ?? c.name ?? c.const ?? c.value ?? c.id) }
63
- : { const: c, title: String(c) });
64
-
65
- /**
66
- * The form with each module field's choices loaded from the connected account.
67
- * @param {object} plan compileConfig's result
68
- * @param {(moduleKey: string) => object|undefined} apiFor the installation's Api for a binding key
69
- * @param {object} values the stored config, or the form's current values on refresh
70
- * @param {{ warn: Function }} [log]
71
- */
72
- async function configOptions(plan, apiFor, values = {}, log) {
73
- const jsonSchema = JSON.parse(JSON.stringify(plan.jsonSchema));
74
- const uiSchema = JSON.parse(JSON.stringify(plan.uiSchema));
75
- const config = resolveConfig(plan, values);
76
- for (const [name, { field, moduleKey }] of Object.entries(plan.fields)) {
77
- const api = apiFor(moduleKey);
78
- let choices = [];
79
- if (api) {
80
- try {
81
- choices = (await field.options(api, { config })) || [];
82
- } catch (error) {
83
- log?.warn?.('Config field options did not load', { eventName: 'frigg.define.config_options_failed', field: name, error });
84
- uiSchema[name] = { ...(uiSchema[name] || {}), 'ui:help': 'The options could not be loaded from the connected account. Try again.' };
85
- }
86
- } else {
87
- uiSchema[name] = { ...(uiSchema[name] || {}), 'ui:help': `Connect ${field.module} to choose.` };
88
- }
89
- const oneOf = choices.map(asChoice);
90
- if (!oneOf.length) continue;
91
- const prop = { ...field.schema, ...(field.schema.title ? { title: field.schema.title } : {}) };
92
- if (prop.type === 'array') jsonSchema.properties[name] = { ...prop, uniqueItems: true, items: { ...(prop.items || { type: 'string' }), oneOf } };
93
- else jsonSchema.properties[name] = { ...prop, oneOf };
94
- }
95
- return { jsonSchema, uiSchema };
96
- }
97
-
98
- module.exports = { compileConfig, resolveConfig, configOptions, isField };
@@ -1,66 +0,0 @@
1
- /**
2
- * `defineApp` (ADR-054 §1): the app definition, as `backend/index.js` exports it today (`{ Definition }`).
3
- *
4
- * module.exports = defineApp({ name, integrations: [DealAlerts], database: { postgres: { enable: true } } });
5
- *
6
- * It checks what can be checked when the file loads: every integration is an IntegrationBase subclass with a
7
- * `Definition.name`, names are unique, and the app definition passes ADR-051's `app-definition.schema.json`
8
- * (through `@friggframework/schemas`, when the app has it installed; the schema treats each integration as an
9
- * object, so the classes are checked here and summarised for it).
10
- *
11
- * It also appends the admin scripts the bound modules export (`module.scripts`, e.g. HubSpot's
12
- * `hubspot.reconcile-subscriptions`) to `adminScripts`, once each by name; a script the app lists itself wins.
13
- */
14
- const { IntegrationBase } = require('../integration-base');
15
- const { planOf } = require('./define-integration');
16
-
17
- function loadSchemas() {
18
- try {
19
- // eslint-disable-next-line global-require
20
- return require('@friggframework/schemas');
21
- } catch (error) {
22
- if (error?.code === 'MODULE_NOT_FOUND') return null;
23
- throw error;
24
- }
25
- }
26
-
27
- function defineApp(appDefinition = {}, { schemas = loadSchemas() } = {}) {
28
- const where = `defineApp(${appDefinition?.name ? `"${appDefinition.name}"` : ''})`;
29
- if (!appDefinition || typeof appDefinition !== 'object') throw new Error(`${where}: expects the app definition object`);
30
- const integrations = appDefinition.integrations;
31
- if (!Array.isArray(integrations) || !integrations.length) throw new Error(`${where}: "integrations" must list at least one integration`);
32
- const names = new Set();
33
- for (const [i, Cls] of integrations.entries()) {
34
- if (typeof Cls !== 'function' || !(Cls.prototype instanceof IntegrationBase)) {
35
- throw new Error(`${where}: integrations[${i}] is not an integration class (defineIntegration or an IntegrationBase subclass)`);
36
- }
37
- const name = Cls.Definition?.name;
38
- if (!name) throw new Error(`${where}: integrations[${i}] has no Definition.name`);
39
- if (names.has(name)) throw new Error(`${where}: two integrations are named "${name}"`);
40
- names.add(name);
41
- }
42
- if (schemas?.validateAppDefinition) {
43
- const summary = { ...appDefinition, integrations: integrations.map((Cls) => ({ name: Cls.Definition.name })) };
44
- const result = schemas.validateAppDefinition(summary);
45
- if (result && result.valid === false) {
46
- const errors = (result.errors || []).map((e) => (typeof e === 'string' ? e : `${e.instancePath || e.path || ''} ${e.message || ''}`.trim()));
47
- throw new Error(`${where}: the app definition does not match app-definition.schema.json: ${errors.join('; ')}`);
48
- }
49
- }
50
- const adminScripts = [...(appDefinition.adminScripts || [])];
51
- const named = new Set(adminScripts.map((S) => S?.Definition?.name));
52
- for (const Cls of integrations) {
53
- for (const binding of planOf(Cls)?.bindings || []) {
54
- const scripts = binding.module.scripts;
55
- for (const Script of Array.isArray(scripts) ? scripts : Object.values(scripts || {})) {
56
- const scriptName = Script?.Definition?.name;
57
- if (!scriptName || named.has(scriptName)) continue;
58
- named.add(scriptName);
59
- adminScripts.push(Script);
60
- }
61
- }
62
- }
63
- return { Definition: { ...appDefinition, integrations: [...integrations], ...(adminScripts.length ? { adminScripts } : {}) } };
64
- }
65
-
66
- module.exports = { defineApp };
@@ -1,154 +0,0 @@
1
- /**
2
- * Extension contract 2 (ADR-054 §2): what an API module's extension may declare beyond contract 1's routes and
3
- * events. Everything is optional, and a contract-1 bundle (no `contract`, or `contract: 1`) stays valid as it is.
4
- *
5
- * module.exports = defineExtension({ // or a plain object with `contract: 2`: modules need no core import
6
- * name: 'hubspot-webhooks',
7
- * contract: 2,
8
- * requires: { core: '>=2.0.0-next.0 <3' },
9
- * useDatabase: false,
10
- * env: ['HUBSPOT_CLIENT_SECRET'],
11
- * options: { type: 'object', properties: { subscribe: {...}, hydrate: {...} } }, // schema of use(m, { <key>: opts })
12
- * routes: [{ path: '/webhooks', method: 'POST' }],
13
- * handshake: (input, ctx) => ({ status, headers, body }) | null, // answered at receipt, nothing queued
14
- * verify: (input, ctx) => ({ valid, reason }) | boolean, // or the name of an Api method (see below)
15
- * receive: (input, ctx) => [{ externalId, body }], // split one delivery, at receipt (no DB)
16
- * resolve: { module: 'hubspot' }, // externalId -> installations, in the worker
17
- * emits: { 'deal.changed': {}, ... }, // integration-facing names, prefixed by the module key
18
- * normalize: async (batch, ctx) => [{ name: 'deal.changed', payload }], // in the worker, with the installation's api
19
- * subscriptions: { scope: 'app', desired, list, create, remove, key },
20
- * triggers: {}, // ADR-055 shape, merged as `<moduleKey>.<name>`
21
- * events: {}, // the contract-1 face (see below)
22
- * });
23
- *
24
- * One bundle can serve both contracts: `events` and `routes[].event` are what a class that binds the bundle by hand
25
- * through `Definition.extensions` runs (and what an older core runs), and `defineIntegration` ignores both, because
26
- * core receives a contract-2 route itself and never adds bare event names. A module can therefore move its
27
- * extension to contract 2 without breaking the integrations that bind it the contract-1 way.
28
- *
29
- * `input` is the delivery as signature checks need it, the same object Stack B's runtime passes to a capability
30
- * manifest's `webhook.verify` (IntegrationBase#webhookVerificationInput): `{ headers, body, query, rawBody, method, url }`.
31
- * `verify` may name an Api method instead of being a function: that is how a capability manifest names it too, so a
32
- * module declares how deliveries are verified once, on its Api (`verifyWebhookSignature`), for both front doors.
33
- *
34
- * Not in the stable 2.0.1 path (ADR-054 decisions 2026-10-03): `subscriptions.scope: 'instance'` (experimental),
35
- * extension `queues`/`workers` (ADR-047), `scripts` and `lifecycle` (follow-up in this PR's checklist).
36
- */
37
- const CORE_VERSION = require('../../package.json').version;
38
-
39
- const STABLE_KEYS = new Set([
40
- 'name', 'contract', 'requires', 'useDatabase', 'env', 'options', 'routes', 'handshake', 'verify', 'receive',
41
- 'resolve', 'emits', 'normalize', 'subscriptions', 'triggers', 'events', 'description',
42
- ]);
43
- const LATER_KEYS = new Set(['scripts', 'lifecycle', 'queues', 'workers', 'fields']);
44
- const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head']);
45
-
46
- // ---- a tiny semver range check: space-separated comparators (>=, >, <=, <, =), prereleases ordered as semver does.
47
- function parseVersion(v) {
48
- const m = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/.exec(String(v).trim());
49
- if (!m) return null;
50
- return { nums: [Number(m[1]), Number(m[2] || 0), Number(m[3] || 0)], pre: m[4] ? m[4].split('.') : [] };
51
- }
52
- function compareVersions(a, b) {
53
- for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] < b.nums[i] ? -1 : 1;
54
- if (!a.pre.length || !b.pre.length) return a.pre.length === b.pre.length ? 0 : a.pre.length ? -1 : 1;
55
- for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
56
- const x = a.pre[i], y = b.pre[i];
57
- if (x === undefined) return -1;
58
- if (y === undefined) return 1;
59
- const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
60
- if (nx && ny && Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1;
61
- if (nx !== ny) return nx ? -1 : 1;
62
- if (x !== y) return x < y ? -1 : 1;
63
- }
64
- return 0;
65
- }
66
- function satisfies(version, range) {
67
- const v = parseVersion(version);
68
- if (!v) return false;
69
- return String(range).trim().split(/\s+/).every((part) => {
70
- const m = /^(>=|<=|>|<|=)?(.+)$/.exec(part);
71
- const target = parseVersion(m[2]);
72
- if (!target) return false;
73
- const c = compareVersions(v, target);
74
- switch (m[1] || '=') {
75
- case '>=': return c >= 0;
76
- case '>': return c > 0;
77
- case '<=': return c <= 0;
78
- case '<': return c < 0;
79
- default: return c === 0;
80
- }
81
- });
82
- }
83
-
84
- const contractOf = (ext) => (ext && ext.contract !== undefined ? ext.contract : 1);
85
- const isContract2 = (ext) => contractOf(ext) === 2;
86
-
87
- /**
88
- * Validate a contract-2 bundle. Throws with the extension name and what is wrong. Returns the bundle.
89
- * @param {object} ext
90
- * @param {{ coreVersion?: string, where?: string }} [opts]
91
- */
92
- function validateExtension(ext, { coreVersion = CORE_VERSION, where } = {}) {
93
- const name = ext?.name || '(unnamed)';
94
- const fail = (msg) => { throw new Error(`${where ? `${where}: ` : ''}extension "${name}" ${msg}`); };
95
- if (!ext || typeof ext !== 'object') fail('must be an object');
96
- if (typeof ext.name !== 'string' || !ext.name) fail('needs a "name"');
97
- const contract = contractOf(ext);
98
- if (contract !== 1 && contract !== 2) fail(`declares contract ${contract}; this core supports contracts 1 and 2`);
99
- if (contract === 1) return ext;
100
- if (ext.requires?.core && !satisfies(coreVersion, ext.requires.core)) {
101
- fail(`requires @friggframework/core ${ext.requires.core}, but this is ${coreVersion}`);
102
- }
103
- for (const key of Object.keys(ext)) {
104
- if (key === 'fields') fail('declares "fields": config fields are a module export (module.fields), so a module bound without an extension can offer them');
105
- if (LATER_KEYS.has(key)) fail(`declares "${key}", which the stable defineIntegration does not run yet (ADR-054 phasing)`);
106
- if (!STABLE_KEYS.has(key)) fail(`has an unknown key "${key}"`);
107
- }
108
- if (ext.useDatabase !== undefined && typeof ext.useDatabase !== 'boolean') fail('"useDatabase" must be a boolean');
109
- if (ext.env !== undefined && (!Array.isArray(ext.env) || ext.env.some((e) => typeof e !== 'string'))) fail('"env" must be a list of variable names');
110
- if (ext.options !== undefined && (typeof ext.options !== 'object' || ext.options === null)) fail('"options" must be a JSON Schema object');
111
- for (const fn of ['handshake', 'receive', 'normalize']) {
112
- if (ext[fn] !== undefined && ext[fn] !== null && typeof ext[fn] !== 'function') fail(`"${fn}" must be a function`);
113
- }
114
- if (ext.verify !== undefined && ext.verify !== null && typeof ext.verify !== 'function' && typeof ext.verify !== 'string') {
115
- fail('"verify" must be a function or the name of an Api method');
116
- }
117
- const routes = ext.routes || [];
118
- if (!Array.isArray(routes)) fail('"routes" must be an array');
119
- for (const r of routes) {
120
- if (!r || typeof r.path !== 'string' || !r.path.startsWith('/')) fail('has a route without a "/path"');
121
- if (typeof r.method !== 'string' || !METHODS.has(r.method.toLowerCase())) fail(`route ${r.path} has an unsupported method`);
122
- // A route's `event` is the bundle's contract-1 face: a class that binds the bundle by hand through
123
- // Definition.extensions (or an older core) dispatches it. defineIntegration ignores it: core receives.
124
- if (r.event !== undefined && !Object.prototype.hasOwnProperty.call(ext.events || {}, r.event)) {
125
- fail(`route ${r.path} names event "${r.event}", which is not in "events" (a contract-2 route needs no event: core receives it)`);
126
- }
127
- }
128
- if (routes.length && typeof ext.receive !== 'function') fail('has routes but no "receive"');
129
- if (routes.length && !ext.resolve) fail('has routes but no "resolve"');
130
- if (ext.resolve && typeof ext.resolve !== 'function' && typeof ext.resolve?.module !== 'string') fail('"resolve" must be { module } or a function');
131
- if (ext.emits !== undefined && (typeof ext.emits !== 'object' || Array.isArray(ext.emits))) fail('"emits" must be an object keyed by event name');
132
- for (const [k, v] of Object.entries(ext.emits || {})) {
133
- if (!/^[a-z][A-Za-z0-9_-]*(\.[a-z][A-Za-z0-9_-]*)*$/.test(k)) fail(`emits "${k}": names are dotted lower-camel words (deal.changed)`);
134
- if (v?.handler !== undefined && typeof v.handler !== 'function') fail(`emits "${k}": "handler" must be a function`);
135
- }
136
- const subs = ext.subscriptions;
137
- if (subs) {
138
- if (subs.scope === 'instance') fail('declares instance-scope subscriptions, which are experimental (@friggframework/core/experimental)');
139
- if (subs.scope !== 'app') fail('subscriptions.scope must be "app"');
140
- for (const fn of ['desired', 'list', 'create']) if (typeof subs[fn] !== 'function') fail(`subscriptions.${fn} must be a function`);
141
- for (const fn of ['remove', 'key']) if (subs[fn] !== undefined && typeof subs[fn] !== 'function') fail(`subscriptions.${fn} must be a function`);
142
- }
143
- if (ext.triggers !== undefined && (typeof ext.triggers !== 'object' || Array.isArray(ext.triggers))) fail('"triggers" must be an object');
144
- return ext;
145
- }
146
-
147
- /** Core's helper for module authors: validates and freezes a contract-2 bundle. A plain object works the same. */
148
- function defineExtension(ext) {
149
- const bundle = { contract: 2, ...ext };
150
- validateExtension(bundle);
151
- return Object.freeze(bundle);
152
- }
153
-
154
- module.exports = { defineExtension, validateExtension, isContract2, contractOf, satisfies, CORE_VERSION };