@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,68 +0,0 @@
1
- /**
2
- * The small part of JSON Schema that extension `options` and `config` need, without a dependency: type, properties,
3
- * required, additionalProperties: false, items, enum, const, oneOf/anyOf, minimum/maximum, default. Pure.
4
- *
5
- * A function value is accepted for any property: binding options may depend on instance config (ADR-054 §4,
6
- * `subscribe: (config) => [...]`), and they are checked again once resolved.
7
- */
8
- const typeOf = (v) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v);
9
-
10
- function matchesType(value, type) {
11
- const t = typeOf(value);
12
- if (type === 'integer') return t === 'number' && Number.isInteger(value);
13
- return t === type;
14
- }
15
-
16
- /**
17
- * @param {object} schema
18
- * @param {*} value
19
- * @param {string} [path]
20
- * @returns {string[]} problems, empty when valid
21
- */
22
- function check(schema, value, path = '') {
23
- if (!schema || typeof schema !== 'object') return [];
24
- if (typeof value === 'function') return [];
25
- const at = path || '(root)';
26
- const problems = [];
27
- if (schema.type !== undefined) {
28
- const types = Array.isArray(schema.type) ? schema.type : [schema.type];
29
- if (!types.some((t) => matchesType(value, t))) return [`${at} must be ${types.join(' or ')}`];
30
- }
31
- if (schema.const !== undefined && value !== schema.const) problems.push(`${at} must be ${JSON.stringify(schema.const)}`);
32
- if (Array.isArray(schema.enum) && !schema.enum.includes(value)) problems.push(`${at} must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(', ')}`);
33
- if (typeof value === 'number') {
34
- if (schema.minimum !== undefined && value < schema.minimum) problems.push(`${at} must be >= ${schema.minimum}`);
35
- if (schema.maximum !== undefined && value > schema.maximum) problems.push(`${at} must be <= ${schema.maximum}`);
36
- }
37
- for (const key of ['oneOf', 'anyOf']) {
38
- if (Array.isArray(schema[key])) {
39
- const passing = schema[key].filter((s) => check(s, value, path).length === 0).length;
40
- if (key === 'oneOf' ? passing !== 1 : passing === 0) problems.push(`${at} does not match ${key === 'oneOf' ? 'exactly one' : 'any'} of the allowed shapes`);
41
- }
42
- }
43
- if (typeOf(value) === 'object') {
44
- for (const req of schema.required || []) {
45
- if (value[req] === undefined) problems.push(`${path ? `${path}.` : ''}${req} is required`);
46
- }
47
- const props = schema.properties || {};
48
- for (const [k, v] of Object.entries(value)) {
49
- if (props[k]) problems.push(...check(props[k], v, path ? `${path}.${k}` : k));
50
- else if (schema.additionalProperties === false) problems.push(`${path ? `${path}.` : ''}${k} is not an allowed option`);
51
- }
52
- }
53
- if (typeOf(value) === 'array' && schema.items) {
54
- value.forEach((item, i) => problems.push(...check(schema.items, item, `${path}[${i}]`)));
55
- }
56
- return problems;
57
- }
58
-
59
- /** The value with the schema's top-level property defaults filled in (shallow, as config forms do). */
60
- function withDefaults(schema, value = {}) {
61
- const out = { ...(value || {}) };
62
- for (const [k, prop] of Object.entries(schema?.properties || {})) {
63
- if (out[k] === undefined && prop && prop.default !== undefined) out[k] = prop.default;
64
- }
65
- return out;
66
- }
67
-
68
- module.exports = { check, withDefaults };
@@ -1,186 +0,0 @@
1
- /**
2
- * App-scope webhook subscriptions (ADR-054 §2): one set per provider app, shared by every account (HubSpot v3
3
- * subscriptions belong to the developer app, not to a portal). The desired set is the union over every integration
4
- * class in the app that enables the extension: a static `use()` option counts once per class, an option that is a
5
- * function of instance config counts once per ENABLED installation.
6
- *
7
- * subscriptions: {
8
- * scope: 'app',
9
- * desired: (options) => options.subscribe, // what one binding needs, as keys
10
- * list: (api, env) => api.listWebhookSubscriptions(…), // what the provider has
11
- * key: (sub) => 'deal.propertyChange:dealstage', // a listed subscription as a desired key
12
- * create: (api, env, key) => …,
13
- * remove: (api, env, sub) => …, // used only with prune
14
- * }
15
- *
16
- * Add-only by default. Reconcile creates what is missing and never removes anything unless it is called with
17
- * `prune: true`. A dry run reports `create` and `remove` (what pruning would do) and writes nothing. Pruning also
18
- * needs the full union: when an option depends on installation config and the installations cannot be listed (a
19
- * deploy step without database access), the group is not pruned and the result says why.
20
- *
21
- * Supported setup: one provider developer app per stage. The subscriptions are app-wide, so two stages (or two
22
- * Frigg apps) that share a developer app would see, and with `prune` remove, each other's subscriptions.
23
- *
24
- * Invokers (no recurring trigger):
25
- * (a) after deploy: `frigg deploy` (devtools) runs it add-only; `--prune-subscriptions` prunes;
26
- * (b) installation lifecycle: create and config update of an installation whose binding options are functions of
27
- * its config (`ensureInstallationSubscriptions`, add-only, that installation's needs only);
28
- * (c) the admin script (`createReconcileSubscriptionsScript`, with `prune` and `dryRun` inputs).
29
- */
30
- const { planOf } = require('./define-integration');
31
- const { resolveOptions, appApiOf, envFor } = require('./extension-binding');
32
-
33
- const hasFunctionOption = (binding) => Object.values(binding.options === true ? {} : binding.options || {}).some((v) => typeof v === 'function');
34
-
35
- /** Every ENABLED installation of a class, paged by id (what the trigger tick reads). Needs the database. */
36
- function defaultListInstallations() {
37
- let repository;
38
- return async function* list(IntegrationClass) {
39
- if (!repository) repository = require('../repositories/integration-repository-factory').createIntegrationRepository();
40
- let after;
41
- for (;;) {
42
- const page = await repository.findIntegrations({ type: IntegrationClass.Definition.name, status: 'ENABLED', limit: 500, after });
43
- if (!page?.length) return;
44
- yield* page;
45
- if (page.length < 500) return;
46
- after = page[page.length - 1].id;
47
- }
48
- };
49
- }
50
-
51
- /** The app-scope bindings of some classes, grouped by extension. */
52
- function groupsOf(integrations, only) {
53
- const groups = new Map(); // ext -> [{ Cls, binding }]
54
- for (const Cls of integrations) {
55
- for (const binding of planOf(Cls)?.extensionBindings || []) {
56
- if (binding.ext.subscriptions?.scope !== 'app') continue;
57
- if (only && binding.ext.name !== only) continue;
58
- if (!groups.has(binding.ext)) groups.set(binding.ext, []);
59
- groups.get(binding.ext).push({ Cls, binding });
60
- }
61
- }
62
- return groups;
63
- }
64
-
65
- function keyOf(subs) {
66
- return (s) => (typeof s === 'string' ? s : subs.key ? subs.key(s) : s.key);
67
- }
68
-
69
- /**
70
- * @param {object} params
71
- * @param {Function[]} params.integrations the app's integration classes (appDefinition.integrations)
72
- * @param {((Cls: Function) => AsyncIterable<{ id, config }>)|null} [params.listInstallations] null: no database
73
- * @param {boolean} [params.prune] remove what no binding needs (default false: add-only)
74
- * @param {boolean} [params.dryRun] plan only: report create and remove, write nothing
75
- * @param {string} [params.extension] only this extension (by name)
76
- * @param {object} [params.log]
77
- * @returns {Promise<Array<{ extension, module, desired, create, remove, kept, created, removed, complete, pruneSkipped? }>>}
78
- */
79
- async function reconcileAppSubscriptions({ integrations = [], listInstallations = defaultListInstallations(), prune = false, dryRun = false, extension, log } = {}) {
80
- const results = [];
81
- for (const [ext, entries] of groupsOf(integrations, extension)) {
82
- const subs = ext.subscriptions;
83
- const desired = new Set();
84
- let complete = true;
85
- const want = (options) => { for (const k of subs.desired(options) || []) desired.add(typeof k === 'string' ? k : subs.key(k)); };
86
- for (const { Cls, binding } of entries) {
87
- if (!hasFunctionOption(binding)) { want(resolveOptions(binding, {})); continue; }
88
- if (!listInstallations) { complete = false; continue; }
89
- for await (const installation of listInstallations(Cls)) {
90
- try {
91
- want(resolveOptions(binding, installation.config || {}));
92
- } catch (error) {
93
- complete = false;
94
- log?.warn?.('Installation options skipped in subscription reconcile', { eventName: 'frigg.subscriptions.options_invalid', integrationId: installation.id, error });
95
- }
96
- }
97
- }
98
- const { binding } = entries[0];
99
- const api = appApiOf(binding);
100
- const env = envFor(ext);
101
- const existing = (await subs.list(api, env)) || [];
102
- const k = keyOf(subs);
103
- const existingKeys = new Map(existing.map((s) => [k(s), s]));
104
- const create = [...desired].filter((key) => !existingKeys.has(key));
105
- const remove = subs.remove ? [...existingKeys.keys()].filter((key) => !desired.has(key)) : [];
106
- const kept = [...existingKeys.keys()].filter((key) => desired.has(key));
107
- let pruneSkipped;
108
- if (prune && !subs.remove) pruneSkipped = 'the extension has no subscriptions.remove';
109
- else if (prune && !complete) pruneSkipped = 'installation config could not be read for every binding, so the desired set is incomplete';
110
- const created = [], removed = [];
111
- if (!dryRun) {
112
- for (const key of create) { await subs.create(api, env, key); created.push(key); }
113
- if (prune && !pruneSkipped) for (const key of remove) { await subs.remove(api, env, existingKeys.get(key)); removed.push(key); }
114
- }
115
- log?.info?.('Webhook subscriptions reconciled', { eventName: 'frigg.subscriptions.reconciled', extension: ext.name, created: created.length, removed: removed.length, wouldRemove: remove.length, prune, dryRun });
116
- results.push({ extension: ext.name, module: binding.moduleName, desired: [...desired], create, remove, kept, created, removed, complete, ...(pruneSkipped ? { pruneSkipped } : {}) });
117
- }
118
- return results;
119
- }
120
-
121
- /**
122
- * Lifecycle invoker (b): the subscriptions one installation needs, created when missing. Only bindings whose options
123
- * are functions of installation config take part (a static option was handled at deploy). Add-only: a deleted or
124
- * reconfigured installation's old subscriptions stay until a reconcile runs with `prune`.
125
- * @param {object} integration a hydrated defineIntegration instance
126
- * @returns {Promise<Array<{ extension, created: string[] }>>}
127
- */
128
- async function ensureInstallationSubscriptions(integration) {
129
- const plan = planOf(integration.constructor);
130
- const out = [];
131
- for (const binding of plan?.extensionBindings || []) {
132
- const subs = binding.ext.subscriptions;
133
- if (subs?.scope !== 'app' || !hasFunctionOption(binding)) continue;
134
- const desired = (subs.desired(resolveOptions(binding, integration.resolvedConfig())) || []).map((d) => (typeof d === 'string' ? d : subs.key(d)));
135
- const api = appApiOf(binding);
136
- const env = envFor(binding.ext);
137
- const have = new Set(((await subs.list(api, env)) || []).map(keyOf(subs)));
138
- const created = [];
139
- for (const key of desired) if (!have.has(key)) { await subs.create(api, env, key); created.push(key); }
140
- out.push({ extension: binding.ext.name, created });
141
- }
142
- return out;
143
- }
144
-
145
- /**
146
- * Admin-script invoker (c): an admin script class (the AdminScriptBase shape, no import needed) that runs the
147
- * reconcile in the deployed stack, where the installations can be read. Inputs: `prune`, `dryRun` (or
148
- * `args: ['--prune', '--dry-run']`).
149
- *
150
- * const { createReconcileSubscriptionsScript } = require('@friggframework/core');
151
- * module.exports = createReconcileSubscriptionsScript({ name: 'hubspot.reconcile-subscriptions', extension: 'hubspot-webhooks' });
152
- */
153
- function createReconcileSubscriptionsScript({ name = 'frigg.subscriptions.reconcile', extension, description, loadIntegrations } = {}) {
154
- return class ReconcileSubscriptionsScript {
155
- static Definition = {
156
- name,
157
- version: '1.0.0',
158
- description: description || `Create the app-scope webhook subscriptions${extension ? ` of ${extension}` : ''} that installed integrations need; with prune, remove the ones none needs.`,
159
- source: 'BUILTIN',
160
- inputSchema: {
161
- type: 'object',
162
- properties: {
163
- prune: { type: 'boolean', default: false, description: 'Remove subscriptions no binding needs (one developer app per stage only)' },
164
- dryRun: { type: 'boolean', default: false, description: 'Report what would be created and removed; change nothing' },
165
- args: { type: 'array', items: { type: 'string', enum: ['--prune', '--dry-run'] }, description: 'CLI form of the flags' },
166
- },
167
- },
168
- config: { timeout: 300000, requireIntegrationInstance: false },
169
- display: { category: 'maintenance', icon: null },
170
- };
171
-
172
- constructor(params = {}) { this.context = params.context || null; }
173
-
174
- /** `{ prune, dryRun }`, or the CLI form `{ args: ['--prune', '--dry-run'] }`. */
175
- async execute({ prune = false, dryRun = false, args = [] } = {}) {
176
- const flags = Array.isArray(args) ? args : String(args).split(/\s+/);
177
- prune = Boolean(prune) || flags.includes('--prune');
178
- dryRun = Boolean(dryRun) || flags.includes('--dry-run');
179
- const integrations = loadIntegrations ? await loadIntegrations() : require('../../handlers/app-definition-loader').loadAppDefinition().integrations;
180
- const results = await reconcileAppSubscriptions({ integrations, prune: Boolean(prune), dryRun: Boolean(dryRun), extension });
181
- return { prune: Boolean(prune), dryRun: Boolean(dryRun), results };
182
- }
183
- };
184
- }
185
-
186
- module.exports = { reconcileAppSubscriptions, ensureInstallationSubscriptions, createReconcileSubscriptionsScript, hasFunctionOption };
@@ -1,109 +0,0 @@
1
- /**
2
- * A test stand-in for @friggframework/api-module-hubspot with its webhooks extension on contract 2: the same shape
3
- * as the module change (api-module-library `claude/hubspot-contract-2`), reduced to what example (a) touches. Real
4
- * HTTP through core's OAuth2Requester, so tests nock api.hubapi.com.
5
- */
6
- const crypto = require('crypto');
7
- const { OAuth2Requester } = require('../../../../modules/requester/oauth-2');
8
-
9
- const BASE = 'https://api.hubapi.com';
10
- const OBJECT_TYPE_IDS = { contact: '0-1', company: '0-2', deal: '0-3' };
11
-
12
- class Api extends OAuth2Requester {
13
- constructor(params = {}) {
14
- super(params);
15
- this.portalId = params.portalId ?? null;
16
- this.urls = { record: (type, id) => `https://app.hubspot.com/contacts/${this.portalId}/record/${OBJECT_TYPE_IDS[type] || type}/${id}` };
17
- }
18
- getOwnerById(id) { return this._get({ url: `${BASE}/crm/v3/owners/${id}` }); }
19
- batchReadObjects(type, body) { return this._post({ url: `${BASE}/crm/v3/objects/${type}/batch/read`, body }); }
20
- getPipelineDetails(type) { return this._get({ url: `${BASE}/crm/v3/pipelines/${type}` }); }
21
- async listWebhookSubscriptions({ appId }) { return (await this._get({ url: `${BASE}/webhooks/v3/${appId}/subscriptions` })).results || []; }
22
- createWebhookSubscription({ appId, eventType, propertyName }) { return this._post({ url: `${BASE}/webhooks/v3/${appId}/subscriptions`, body: { eventType, ...(propertyName ? { propertyName } : {}), active: true } }); }
23
- deleteWebhookSubscription({ appId, subscriptionId }) { return this._delete({ url: `${BASE}/webhooks/v3/${appId}/subscriptions/${subscriptionId}` }); }
24
- }
25
-
26
- const Definition = { moduleName: 'hubspot', getName: () => 'hubspot', API: Api };
27
-
28
- /** HubSpot v3: base64(HMAC-SHA256(secret, method + url + rawBody + timestamp)), 5 minutes. */
29
- function verify(input, { env }) {
30
- const h = Object.fromEntries(Object.entries(input.headers || {}).map(([k, v]) => [k.toLowerCase(), v]));
31
- const ts = Number(h['x-hubspot-request-timestamp']);
32
- if (!env.HUBSPOT_CLIENT_SECRET) return { valid: false, reason: 'missing client secret' };
33
- if (!h['x-hubspot-signature-v3'] || !Number.isFinite(ts)) return { valid: false, reason: 'missing signature headers' };
34
- if (Math.abs(Date.now() - ts) > 5 * 60 * 1000) return { valid: false, reason: 'timestamp outside 5 minutes' };
35
- const body = input.rawBody ?? JSON.stringify(input.body ?? '');
36
- const expected = crypto.createHmac('sha256', env.HUBSPOT_CLIENT_SECRET).update(`${input.method}${input.url}${body}${h['x-hubspot-request-timestamp']}`).digest('base64');
37
- const a = Buffer.from(expected), b = Buffer.from(String(h['x-hubspot-signature-v3']));
38
- return { valid: a.length === b.length && crypto.timingSafeEqual(a, b), reason: 'signature mismatch' };
39
- }
40
-
41
- const VERBS = { creation: 'created', propertyChange: 'changed', deletion: 'deleted' };
42
- const PLURAL = { deal: 'deals', contact: 'contacts', company: 'companies' };
43
-
44
- async function normalize(batch, { api, options }) {
45
- const out = [];
46
- for (const e of batch) {
47
- const [object, kind] = String(e.subscriptionType || '').split('.');
48
- if (!PLURAL[object] || !VERBS[kind]) continue;
49
- if (api && api.portalId == null) api.portalId = String(e.portalId);
50
- const changes = kind === 'propertyChange' ? { [e.propertyName]: { to: e.propertyValue } } : {};
51
- let record = { id: String(e.objectId), properties: kind === 'propertyChange' ? { [e.propertyName]: e.propertyValue } : {} };
52
- if (options.hydrate && kind !== 'deletion') {
53
- const properties = Array.isArray(options.hydrate) ? options.hydrate : undefined;
54
- const res = await api.batchReadObjects(PLURAL[object], { inputs: [{ id: String(e.objectId) }], ...(properties ? { properties } : {}) });
55
- record = res.results?.[0] || record;
56
- }
57
- out.push({ name: `${object}.${VERBS[kind]}`, payload: { [object]: record, changes, event: e } });
58
- }
59
- return out;
60
- }
61
-
62
- const webhooks = {
63
- name: 'hubspot-webhooks',
64
- contract: 2,
65
- requires: { core: '>=2.0.0-next.0 <3' },
66
- useDatabase: false,
67
- env: ['HUBSPOT_CLIENT_SECRET', 'HUBSPOT_APP_ID', 'HUBSPOT_DEVELOPER_API_KEY'],
68
- options: {
69
- type: 'object',
70
- properties: {
71
- subscribe: { type: 'array', items: { type: 'string' } },
72
- hydrate: { oneOf: [{ type: 'boolean' }, { type: 'array', items: { type: 'string' } }], default: false },
73
- },
74
- required: ['subscribe'],
75
- additionalProperties: false,
76
- },
77
- routes: [{ path: '/webhooks', method: 'POST' }],
78
- verify,
79
- receive: (input) => (Array.isArray(input.body) ? input.body : []).map((e) => ({ externalId: e?.portalId, body: e })),
80
- resolve: { module: 'hubspot' },
81
- emits: {
82
- 'deal.created': {}, 'deal.changed': {}, 'deal.deleted': {},
83
- 'contact.created': {}, 'contact.changed': {}, 'contact.deleted': {},
84
- 'company.created': {}, 'company.changed': {}, 'company.deleted': {},
85
- },
86
- normalize,
87
- subscriptions: {
88
- scope: 'app',
89
- desired: (options) => options.subscribe,
90
- key: (s) => (s.propertyName ? `${s.eventType}:${s.propertyName}` : s.eventType),
91
- list: (api, env) => api.listWebhookSubscriptions({ appId: env.HUBSPOT_APP_ID }),
92
- create: (api, env, key) => { const [eventType, propertyName] = key.split(':'); return api.createWebhookSubscription({ appId: env.HUBSPOT_APP_ID, eventType, propertyName }); },
93
- remove: (api, env, sub) => api.deleteWebhookSubscription({ appId: env.HUBSPOT_APP_ID, subscriptionId: sub.id }),
94
- },
95
- };
96
-
97
- const fields = {
98
- dealStages: ({ title = 'Deal stage', multiple = false, required = false, default: dflt } = {}) => ({
99
- module: 'hubspot',
100
- required,
101
- schema: multiple ? { type: 'array', title, items: { type: 'string' }, ...(dflt !== undefined ? { default: dflt } : {}) } : { type: 'string', title, ...(dflt !== undefined ? { default: dflt } : {}) },
102
- options: async (api) => {
103
- const res = await api.getPipelineDetails('deals');
104
- return (res.results || []).flatMap((p) => (p.stages || []).map((s) => ({ const: s.id, title: `${p.label}: ${s.label}` })));
105
- },
106
- }),
107
- };
108
-
109
- module.exports = { Api, Definition, extensions: { webhooks }, fields };
@@ -1,26 +0,0 @@
1
- /**
2
- * A test stand-in for @friggframework/api-module-slack as ADR-054 item 6 describes it (bot token, fields.channel):
3
- * no extension, only an Api and a config field. Real HTTP through core's OAuth2Requester, so tests nock slack.com.
4
- */
5
- const { OAuth2Requester } = require('../../../../modules/requester/oauth-2');
6
-
7
- const BASE = 'https://slack.com/api';
8
-
9
- class Api extends OAuth2Requester {
10
- postMessage(body) { return this._post({ url: `${BASE}/chat.postMessage`, body, headers: { 'Content-Type': 'application/json; charset=utf-8' } }); }
11
- lookupUserByEmail(email) { return this._get({ url: `${BASE}/users.lookupByEmail`, query: { email } }); }
12
- listConversations() { return this._get({ url: `${BASE}/conversations.list`, query: { types: 'public_channel,private_channel', exclude_archived: true } }); }
13
- }
14
-
15
- const Definition = { moduleName: 'slack', getName: () => 'slack', API: Api };
16
-
17
- const fields = {
18
- channel: ({ title = 'Channel', required = false } = {}) => ({
19
- module: 'slack',
20
- required,
21
- schema: { type: 'string', title },
22
- options: async (api) => ((await api.listConversations()).channels || []).map((c) => ({ const: c.id, title: `#${c.name}` })),
23
- }),
24
- };
25
-
26
- module.exports = { Api, Definition, extensions: {}, fields };
@@ -1,39 +0,0 @@
1
- /**
2
- * `use(module, extensionOptions?)` (ADR-054 §1): a module binding that enables some of the module's extensions.
3
- *
4
- * modules: {
5
- * crm: use(hubspot, { webhooks: { subscribe: ['deal.creation'] } }), // enable hubspot.extensions.webhooks
6
- * chat: use(slack), // no extension
7
- * erp: erp, // the module export alone
8
- * old: { definition: erp.Definition }, // the shape Definition.modules has today
9
- * }
10
- *
11
- * The key (`crm`) is the integration author's: it names `ctx.modules.crm`, the routes, events, triggers and fields
12
- * the module's extensions contribute. `use` is core's, so a module does not implement it.
13
- */
14
- const BINDING = Symbol.for('frigg.define.binding');
15
-
16
- function use(module, extensions = {}) {
17
- if (!module || typeof module !== 'object' || !module.Definition) {
18
- throw new Error('use(module, options): the first argument must be an API module export (it has a Definition)');
19
- }
20
- if (!extensions || typeof extensions !== 'object' || Array.isArray(extensions)) {
21
- throw new Error(`use(${module.Definition.moduleName || 'module'}, options): options are an object keyed by extension name`);
22
- }
23
- return { [BINDING]: true, module, extensions };
24
- }
25
-
26
- /** Any accepted form of a `modules` entry, as `{ key, module, extensions, moduleName }`. */
27
- function normalizeBinding(key, value) {
28
- let module, extensions = {};
29
- if (value && value[BINDING]) ({ module, extensions } = value);
30
- else if (value?.Definition) module = value;
31
- else if (value?.definition) module = { Definition: value.definition };
32
- else throw new Error(`modules.${key}: expected use(module), a module export, or { definition }`);
33
- const def = module.Definition;
34
- const moduleName = typeof def.getName === 'function' ? def.getName() : def.moduleName;
35
- if (!moduleName) throw new Error(`modules.${key}: the module's Definition has no moduleName`);
36
- return { key, module, extensions, moduleName };
37
- }
38
-
39
- module.exports = { use, normalizeBinding, BINDING };