@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
@@ -19,19 +19,48 @@ function mapIntegrationClassToIntegrationDTO(integration) {
19
19
  };
20
20
  }
21
21
 
22
+ let deprecationLogged = false;
22
23
 
23
- const getModulesDefinitionFromIntegrationClasses = (integrationClasses) => {
24
- return [
25
- ...new Set(
26
- integrationClasses
27
- .map((integration) =>
28
- Object.values(integration.Definition.modules).map(
29
- (module) => module.definition
30
- )
31
- )
32
- .flat()
33
- ),
34
- ];
35
- };
24
+ /**
25
+ * @deprecated Since ADR-059. Read `entityTypeRegistry` / `moduleDefinitions`
26
+ * from `loadAppDefinition()`, or call `getEntityTypeRegistry(appDefinition)`
27
+ * and read each entry's `definition`.
28
+ *
29
+ * Kept as a wrapper over the entity type registry so callers that imported it
30
+ * keep working. It returns the registry's definitions, so it follows the
31
+ * registry's rules: one definition per entity type name (the first wins when
32
+ * two integrations bind different definitions under one name, as
33
+ * ModuleFactory already picked), and, when `entities` is passed, registry-only
34
+ * types are included and conflicts throw `EntityRegistryError`.
35
+ *
36
+ * @param {Array<Function>} integrationClasses - integration classes
37
+ * @param {Object} [entities] - `appDefinition.entities` (ADR-059)
38
+ * @returns {Array<Object>} API module definitions
39
+ */
40
+ function getModulesDefinitionFromIntegrationClasses(
41
+ integrationClasses,
42
+ entities
43
+ ) {
44
+ // Required here, not at the top: modules/ must not load at require time
45
+ // of every integration use case that only wants the DTO mapper.
46
+ const {
47
+ getEntityTypeRegistry,
48
+ getEntityTypeDefinitions,
49
+ } = require('../../modules/entity-type-registry');
50
+ if (!deprecationLogged) {
51
+ deprecationLogged = true;
52
+ const { getLogger } = require('../../logs');
53
+ getLogger('frigg.integrations').warn(
54
+ 'getModulesDefinitionFromIntegrationClasses is deprecated; use getEntityTypeRegistry (ADR-059)',
55
+ { eventName: 'frigg.integrations.deprecated_module_definitions' }
56
+ );
57
+ }
58
+ return getEntityTypeDefinitions(
59
+ getEntityTypeRegistry({ integrations: integrationClasses, entities })
60
+ );
61
+ }
36
62
 
37
- module.exports = { mapIntegrationClassToIntegrationDTO, getModulesDefinitionFromIntegrationClasses };
63
+ module.exports = {
64
+ mapIntegrationClassToIntegrationDTO,
65
+ getModulesDefinitionFromIntegrationClasses,
66
+ };
@@ -0,0 +1,49 @@
1
+ const express = require('express');
2
+ const { resolveManagementApiConfig } = require('./management-api-config');
3
+ const {
4
+ createV1DeprecationRouter,
5
+ createV1DisabledRouter,
6
+ } = require('./v1-deprecation');
7
+
8
+ /**
9
+ * The Management API served by the auth Lambda (ADR-053): v2 under /api/v2
10
+ * and the frozen, deprecated v1 at the unprefixed paths. With
11
+ * `managementApi.v1: false` the v1 handlers are never built and the v1 paths
12
+ * answer 410.
13
+ *
14
+ * @param {Object} [params]
15
+ * @param {Object} [params.appDefinition] - defaults to loadAppDefinition().
16
+ * @param {Function} [params.createV1Router] - defaults to createIntegrationRouter.
17
+ * @param {Function} [params.createV2Router] - defaults to createManagementApiV2Router.
18
+ */
19
+ function createManagementApiRouter({
20
+ appDefinition,
21
+ createV1Router,
22
+ createV2Router,
23
+ } = {}) {
24
+ const definition =
25
+ appDefinition ||
26
+ require('../handlers/app-definition-loader').loadAppDefinition();
27
+ const config = resolveManagementApiConfig(definition.managementApi);
28
+
29
+ const buildV1 =
30
+ createV1Router ||
31
+ require('../integrations/integration-router').createIntegrationRouter;
32
+ const buildV2 =
33
+ createV2Router ||
34
+ require('./v2/create-v2-router').createManagementApiV2Router;
35
+
36
+ const router = express.Router();
37
+ router.use(buildV2({ appDefinition: definition, config }));
38
+
39
+ if (config.v1) {
40
+ router.use(createV1DeprecationRouter());
41
+ router.use(buildV1());
42
+ } else {
43
+ router.use(createV1DisabledRouter());
44
+ }
45
+
46
+ return router;
47
+ }
48
+
49
+ module.exports = { createManagementApiRouter };
@@ -0,0 +1,116 @@
1
+ const Boom = require('@hapi/boom');
2
+
3
+ /**
4
+ * Error responses for the HTTP boundary (handlers/app-handler-helpers.js).
5
+ *
6
+ * - v1 and other unversioned routes keep their historical body:
7
+ * `{ error: message }` for 4xx, `{ error: 'Internal Server Error' }` for 5xx.
8
+ * - Management API v2 and /api/meta answer `{ error: { code, message, details? } }`
9
+ * and carry `Frigg-API-Version: 2` even when the error happened before any
10
+ * v2 router ran (for example a body-parser failure).
11
+ */
12
+
13
+ const API_VERSION_HEADER = 'Frigg-API-Version';
14
+
15
+ const CODE_BY_STATUS = {
16
+ 400: 'BAD_REQUEST',
17
+ 401: 'UNAUTHORIZED',
18
+ 403: 'FORBIDDEN',
19
+ 404: 'NOT_FOUND',
20
+ 405: 'METHOD_NOT_ALLOWED',
21
+ 409: 'CONFLICT',
22
+ 410: 'GONE',
23
+ 413: 'PAYLOAD_TOO_LARGE',
24
+ 415: 'UNSUPPORTED_MEDIA_TYPE',
25
+ 422: 'UNPROCESSABLE_ENTITY',
26
+ 429: 'RATE_LIMITED',
27
+ 500: 'INTERNAL_ERROR',
28
+ 501: 'NOT_IMPLEMENTED',
29
+ 502: 'BAD_GATEWAY',
30
+ 503: 'SERVICE_UNAVAILABLE',
31
+ 504: 'GATEWAY_TIMEOUT',
32
+ };
33
+
34
+ // body-parser (via http-errors) tags its failures with `type`.
35
+ const CODE_BY_BODY_PARSER_TYPE = {
36
+ 'entity.parse.failed': 'INVALID_JSON',
37
+ 'entity.too.large': 'PAYLOAD_TOO_LARGE',
38
+ 'encoding.unsupported': 'UNSUPPORTED_MEDIA_TYPE',
39
+ 'charset.unsupported': 'UNSUPPORTED_MEDIA_TYPE',
40
+ 'request.aborted': 'BAD_REQUEST',
41
+ };
42
+
43
+ /** True for paths that use the v2 error contract. */
44
+ function isVersionedManagementApiPath(path = '') {
45
+ return (
46
+ path === '/api/v2' ||
47
+ path.startsWith('/api/v2/') ||
48
+ path === '/api/meta' ||
49
+ path.startsWith('/api/meta/')
50
+ );
51
+ }
52
+
53
+ /**
54
+ * The HTTP status for an error. Boom errors carry theirs. Other errors are
55
+ * trusted only when they declare a client error as safe to expose, as
56
+ * http-errors (and so body-parser) does; anything else is a 500, so an
57
+ * upstream FetchError's `statusCode` is never mistaken for ours.
58
+ */
59
+ function resolveStatusCode(err) {
60
+ if (err?.isBoom) return err.output.statusCode;
61
+ const status = err?.status ?? err?.statusCode;
62
+ if (
63
+ err?.expose === true &&
64
+ Number.isInteger(status) &&
65
+ status >= 400 &&
66
+ status < 500
67
+ ) {
68
+ return status;
69
+ }
70
+ return 500;
71
+ }
72
+
73
+ /** Normalises any thrown value into a Boom error with the right status. */
74
+ function toBoom(err) {
75
+ if (err?.isBoom) return err;
76
+ const error = err instanceof Error ? err : new Error(String(err));
77
+ return Boom.boomify(error, { statusCode: resolveStatusCode(err) });
78
+ }
79
+
80
+ /** The v2 error body for a Boom error. */
81
+ function toV2ErrorBody(boomError) {
82
+ const statusCode = boomError.output.statusCode;
83
+ const data =
84
+ boomError.data && typeof boomError.data === 'object'
85
+ ? boomError.data
86
+ : {};
87
+ const exposeServerError = statusCode >= 500 && data.expose === true;
88
+
89
+ const code =
90
+ (typeof data.code === 'string' && data.code) ||
91
+ CODE_BY_BODY_PARSER_TYPE[boomError.type] ||
92
+ CODE_BY_STATUS[statusCode] ||
93
+ (statusCode >= 500 ? 'INTERNAL_ERROR' : 'BAD_REQUEST');
94
+
95
+ const message =
96
+ statusCode < 500 || exposeServerError
97
+ ? boomError.message
98
+ : 'Internal Server Error';
99
+
100
+ const body = { code, message };
101
+ if (
102
+ data.details !== undefined &&
103
+ (statusCode < 500 || exposeServerError)
104
+ ) {
105
+ body.details = data.details;
106
+ }
107
+ return { error: body };
108
+ }
109
+
110
+ module.exports = {
111
+ API_VERSION_HEADER,
112
+ isVersionedManagementApiPath,
113
+ resolveStatusCode,
114
+ toBoom,
115
+ toV2ErrorBody,
116
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Resolves `appDefinition.managementApi` (ADR-052, ADR-053).
3
+ *
4
+ * ```js
5
+ * managementApi: {
6
+ * v1: false, // drop the deprecated v1 routes (default true)
7
+ * proxy: {
8
+ * enable: true, // entity proxy, off by default (ADR-052)
9
+ * timeoutMs: 25000, // whole upstream call, lower only
10
+ * maxRequestBytes: 1048576,
11
+ * maxResponseBytes: 5242880, // lower only
12
+ * scopes: ['individual'], // entity scopes the proxy accepts (ADR-056 §3.5)
13
+ * modules: { // per-module allow-list override
14
+ * hubspot: { allow: 'module' },
15
+ * erp: { allow: [{ method: 'GET', path: '/v1/customers/**' }] },
16
+ * },
17
+ * },
18
+ * }
19
+ * ```
20
+ *
21
+ * Limits that are out of range fall back to their defaults. `proxy.scopes` is a
22
+ * security setting, so it does not fall back: a value that is not a list of
23
+ * known tenant scopes throws ManagementApiConfigError, and loadAppDefinition
24
+ * calls this so the app stops at load.
25
+ */
26
+
27
+ const { BaseError } = require('../errors/base-error');
28
+ const { SCOPES } = require('../modules/scope-resolution');
29
+
30
+ /** Raised for an invalid `appDefinition.managementApi` setting. */
31
+ class ManagementApiConfigError extends BaseError {
32
+ constructor(message, options) {
33
+ super(message, options);
34
+ this.code = 'MANAGEMENT_API_CONFIG_INVALID';
35
+ }
36
+ }
37
+
38
+ /**
39
+ * The scopes `managementApi.proxy.scopes` may list. `global` is not one of
40
+ * them: a global entity is the app owner's and is never a tenant's to proxy.
41
+ */
42
+ const PROXY_SCOPE_VALUES = Object.freeze([SCOPES.INDIVIDUAL, SCOPES.ORGANIZATION]);
43
+
44
+ /**
45
+ * Narrowest by default (ADR-056 §3.5): proxying through a shared credential
46
+ * lets any member act on the organization's account, so an app opts in to
47
+ * `organization` explicitly.
48
+ */
49
+ const DEFAULT_PROXY_SCOPES = Object.freeze([SCOPES.INDIVIDUAL]);
50
+
51
+ // ADR-052 §7. API Gateway HTTP APIs time out at 30s and Lambda responses cap
52
+ // at 6MB, so the timeout and response defaults are also ceilings: an app may
53
+ // lower them, not raise them.
54
+ const DEFAULT_PROXY_LIMITS = Object.freeze({
55
+ timeoutMs: 25_000,
56
+ maxRequestBytes: 1024 * 1024,
57
+ maxResponseBytes: 5 * 1024 * 1024,
58
+ });
59
+
60
+ // The request-body limit has no platform ceiling below API Gateway's 10MB, so
61
+ // an app may raise it up to that.
62
+ const MAX_REQUEST_BYTES_CEILING = 10 * 1024 * 1024;
63
+
64
+ const isPositiveNumber = (value) =>
65
+ typeof value === 'number' && Number.isFinite(value) && value > 0;
66
+
67
+ /** The value, capped at `ceiling`; the ceiling when the value is unusable. */
68
+ function positiveCapped(value, ceiling) {
69
+ return isPositiveNumber(value) ? Math.min(value, ceiling) : ceiling;
70
+ }
71
+
72
+ /** The value, capped at `ceiling`; `fallback` when the value is unusable. */
73
+ function positiveOr(value, fallback, ceiling) {
74
+ return isPositiveNumber(value) ? Math.min(value, ceiling) : fallback;
75
+ }
76
+
77
+ const proxyScopesError = (message) =>
78
+ new ManagementApiConfigError(`managementApi.proxy.scopes: ${message}`);
79
+
80
+ /**
81
+ * `managementApi.proxy.scopes`: absent means the default; anything else must be
82
+ * a non-empty list of PROXY_SCOPE_VALUES, each once.
83
+ * @returns {ReadonlyArray<string>}
84
+ * @throws {ManagementApiConfigError}
85
+ */
86
+ function resolveProxyScopes(scopes) {
87
+ if (scopes === undefined) return DEFAULT_PROXY_SCOPES;
88
+ const allowed = PROXY_SCOPE_VALUES.join(', ');
89
+ if (!Array.isArray(scopes) || scopes.length === 0) {
90
+ throw proxyScopesError(`must be a non-empty list drawn from ${allowed}`);
91
+ }
92
+ for (const scope of scopes) {
93
+ if (scope === SCOPES.GLOBAL) {
94
+ throw proxyScopesError(
95
+ "'global' is not allowed: global entities belong to the app owner and are never proxied for a tenant"
96
+ );
97
+ }
98
+ if (!PROXY_SCOPE_VALUES.includes(scope)) {
99
+ throw proxyScopesError(`unknown scope ${JSON.stringify(scope)}; use ${allowed}`);
100
+ }
101
+ }
102
+ if (new Set(scopes).size !== scopes.length) {
103
+ throw proxyScopesError('lists a scope more than once');
104
+ }
105
+ return Object.freeze([...scopes]);
106
+ }
107
+
108
+ /**
109
+ * @param {Object} [managementApi] - `appDefinition.managementApi`
110
+ * @returns {{ v1: boolean, proxy: { enable: boolean, timeoutMs: number, maxRequestBytes: number, maxResponseBytes: number, scopes: ReadonlyArray<string>, modules: Object } }}
111
+ * @throws {ManagementApiConfigError} for an invalid `proxy.scopes`
112
+ */
113
+ function resolveManagementApiConfig(managementApi) {
114
+ const config =
115
+ managementApi && typeof managementApi === 'object' ? managementApi : {};
116
+ const proxy =
117
+ config.proxy && typeof config.proxy === 'object' ? config.proxy : {};
118
+
119
+ return {
120
+ v1: config.v1 !== false,
121
+ proxy: {
122
+ enable: proxy.enable === true,
123
+ timeoutMs: positiveCapped(
124
+ proxy.timeoutMs,
125
+ DEFAULT_PROXY_LIMITS.timeoutMs
126
+ ),
127
+ maxRequestBytes: positiveOr(
128
+ proxy.maxRequestBytes,
129
+ DEFAULT_PROXY_LIMITS.maxRequestBytes,
130
+ MAX_REQUEST_BYTES_CEILING
131
+ ),
132
+ maxResponseBytes: positiveCapped(
133
+ proxy.maxResponseBytes,
134
+ DEFAULT_PROXY_LIMITS.maxResponseBytes
135
+ ),
136
+ scopes: resolveProxyScopes(proxy.scopes),
137
+ modules:
138
+ proxy.modules && typeof proxy.modules === 'object'
139
+ ? proxy.modules
140
+ : {},
141
+ },
142
+ };
143
+ }
144
+
145
+ module.exports = {
146
+ resolveManagementApiConfig,
147
+ DEFAULT_PROXY_LIMITS,
148
+ DEFAULT_PROXY_SCOPES,
149
+ PROXY_SCOPE_VALUES,
150
+ ManagementApiConfigError,
151
+ };
@@ -0,0 +1,76 @@
1
+ const { Router } = require('express');
2
+ const Boom = require('@hapi/boom');
3
+ const { resolveManagementApiConfig } = require('./management-api-config');
4
+ const {
5
+ GetManagementApiMeta,
6
+ } = require('./use-cases/get-management-api-meta');
7
+ const { isValidAdminApiKey } = require('../handlers/middleware/admin-auth');
8
+ const { version: coreVersion } = require('../package.json');
9
+ const {
10
+ MANAGEMENT_API_VERSIONS,
11
+ PREFERRED_API_VERSION,
12
+ DOCS_ROUTES,
13
+ } = require('./route-registry');
14
+ const { buildOpenApiDocument } = require('./openapi/build-openapi');
15
+
16
+ /**
17
+ * Version-neutral discovery routes (ADR-053 §4, §5), mounted on the DB-free
18
+ * health Lambda. No authentication: the admin API key only adds the core
19
+ * version to the response.
20
+ *
21
+ * @param {Object} params
22
+ * @param {Object} params.appDefinition - the loaded app definition
23
+ * (`managementApi` is read).
24
+ */
25
+ function createManagementApiMetaRouter({ appDefinition = {} } = {}) {
26
+ const config = resolveManagementApiConfig(appDefinition.managementApi);
27
+ const getMeta = new GetManagementApiMeta({ config, coreVersion });
28
+
29
+ const router = Router();
30
+
31
+ router.get('/api/meta', (req, res) => {
32
+ const isAdmin = isValidAdminApiKey(req.get('x-frigg-admin-api-key'));
33
+ res.set('Cache-Control', 'no-store');
34
+ res.json(getMeta.execute({ isAdmin }));
35
+ });
36
+
37
+ // One OpenAPI document per major (ADR-053 §5). Static per deployment, so
38
+ // built once; the entity proxy appears only when the app enables it.
39
+ const documents = new Map();
40
+ for (const major of Object.keys(MANAGEMENT_API_VERSIONS)) {
41
+ router.get(`/api/meta/openapi/v${major}.json`, (_req, res) => {
42
+ if (!documents.has(major)) {
43
+ documents.set(
44
+ major,
45
+ buildOpenApiDocument(major, { proxy: config.proxy.enable })
46
+ );
47
+ }
48
+ res.set('Cache-Control', 'public, max-age=300');
49
+ res.json(documents.get(major));
50
+ });
51
+ }
52
+
53
+ // Anything else under /api/meta is a v2-shaped 404, not Express's page.
54
+ router.use('/api/meta', (req, _res, next) => {
55
+ next(Boom.notFound(`No Management API meta route for ${req.method} ${req.originalUrl.split('?')[0]}`));
56
+ });
57
+
58
+ return router;
59
+ }
60
+
61
+ /**
62
+ * GET /api/docs (DOCS_ROUTES): a redirect to the current OpenAPI document,
63
+ * mounted next to the meta router on the DB-free health Lambda. Public, like
64
+ * /api/meta.
65
+ */
66
+ function createDocsRedirectRouter() {
67
+ const router = Router();
68
+ for (const { path } of DOCS_ROUTES) {
69
+ router.get(path, (_req, res) =>
70
+ res.redirect(302, `/api/meta/openapi/v${PREFERRED_API_VERSION}.json`)
71
+ );
72
+ }
73
+ return router;
74
+ }
75
+
76
+ module.exports = { createManagementApiMetaRouter, createDocsRedirectRouter };
@@ -0,0 +1,144 @@
1
+ const {
2
+ MANAGEMENT_API_VERSIONS,
3
+ V1_ROUTES,
4
+ META_ROUTES,
5
+ V1_MIGRATION_DOCS_URL,
6
+ listV2Routes,
7
+ toGatewayPath,
8
+ } = require('../route-registry');
9
+ const { SCHEMAS, RESPONSES, SECURITY_SCHEMES } = require('./components');
10
+ const { V2_OPERATIONS, META_OPERATIONS } = require('./v2-operations');
11
+ const { V1_OPERATIONS } = require('./v1-operations');
12
+ const { version: coreVersion } = require('../../package.json');
13
+
14
+ const COMPATIBILITY_RULES = `
15
+ Compatibility (ADR-053): within a major, changes are additive only. Clients
16
+ must ignore response fields and enum values they do not know. Operations
17
+ marked \`x-frigg-stability: beta\` may still change in a core minor.
18
+
19
+ Errors: \`{ "error": { "code", "message", "details"? } }\`. Every response
20
+ carries \`Frigg-API-Version: 2\`.`;
21
+
22
+ const DESCRIPTIONS = {
23
+ 1: `The original, unprefixed Management API. Deprecated (ADR-053): every response carries
24
+ \`Deprecation\` and \`Link: <${V1_MIGRATION_DOCS_URL}>; rel="deprecation"\`. It is frozen,
25
+ and \`managementApi.v1: false\` turns it off. Use v2.`,
26
+ 2: `Integrations, entities, credentials, multi-step authorization and the entity proxy, under
27
+ \`/api/v2\`. Discover what a deployment serves with \`GET /api/meta\`.
28
+ ${COMPATIBILITY_RULES}`,
29
+ };
30
+
31
+ const TAGS = [
32
+ { name: 'Integrations', description: 'Integrations between connected entities' },
33
+ { name: 'Entities', description: 'Connected accounts, entity types and the entity proxy' },
34
+ { name: 'Credentials', description: 'Stored credentials (always masked) and re-authorization' },
35
+ { name: 'Authorization', description: 'Single- and multi-step authorization' },
36
+ { name: 'Meta', description: 'Version-neutral discovery' },
37
+ ];
38
+
39
+ /** Path parameters, derived from the Express path. */
40
+ function pathParameters(expressPath) {
41
+ return [...expressPath.matchAll(/:([A-Za-z0-9_]+)/g)].map(([, name]) => ({
42
+ name,
43
+ in: 'path',
44
+ required: true,
45
+ schema: { type: 'string' },
46
+ }));
47
+ }
48
+
49
+ function operationFor(route, docs, extra = {}) {
50
+ if (!docs) {
51
+ throw new Error(
52
+ `No OpenAPI documentation for ${route.method} ${route.path} (${route.operationId || 'v1'})`
53
+ );
54
+ }
55
+ const parameters = [...pathParameters(route.path), ...(docs.parameters || [])];
56
+ return {
57
+ operationId: route.operationId,
58
+ ...(route.tag && { tags: [route.tag] }),
59
+ ...docs,
60
+ ...(parameters.length && { parameters }),
61
+ ...(route.stability && { 'x-frigg-stability': route.stability }),
62
+ ...extra,
63
+ };
64
+ }
65
+
66
+ function addOperation(paths, route, operation) {
67
+ const path = toGatewayPath(route.path);
68
+ paths[path] = paths[path] || {};
69
+ // An ANY route (router.all in v1) is documented as GET and POST, the two
70
+ // methods callers use.
71
+ const methods = route.method === 'ANY' ? ['get', 'post'] : [route.method.toLowerCase()];
72
+ for (const method of methods) {
73
+ paths[path][method] = {
74
+ ...operation,
75
+ ...(methods.length > 1 && {
76
+ operationId: `${operation.operationId}${method === 'get' ? '' : 'Post'}`,
77
+ }),
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Builds the OpenAPI 3 document for one Management API major from the route
84
+ * registry (ADR-053 §5).
85
+ *
86
+ * @param {string} major - '1' or '2'
87
+ * @param {Object} [options]
88
+ * @param {boolean} [options.proxy=true] - include the entity proxy (served
89
+ * documents follow managementApi.proxy.enable)
90
+ * @returns {Object}
91
+ */
92
+ function buildOpenApiDocument(major, { proxy = true } = {}) {
93
+ const info = MANAGEMENT_API_VERSIONS[major];
94
+ if (!info) throw new Error(`Unknown Management API version: ${major}`);
95
+
96
+ const paths = {};
97
+ if (major === '1') {
98
+ for (const route of V1_ROUTES) {
99
+ const key = `${route.method} ${route.path}`;
100
+ addOperation(
101
+ paths,
102
+ route,
103
+ operationFor({ ...route, operationId: V1_OPERATIONS[key]?.operationId }, V1_OPERATIONS[key], {
104
+ deprecated: true,
105
+ })
106
+ );
107
+ }
108
+ } else {
109
+ for (const route of listV2Routes({ proxy })) {
110
+ addOperation(paths, route, operationFor(route, V2_OPERATIONS[route.operationId]));
111
+ }
112
+ }
113
+ for (const route of META_ROUTES) {
114
+ addOperation(
115
+ paths,
116
+ route,
117
+ operationFor({ ...route, tag: 'Meta' }, META_OPERATIONS[route.operationId])
118
+ );
119
+ }
120
+
121
+ return {
122
+ openapi: '3.0.3',
123
+ info: {
124
+ title: `Frigg Management API v${major}`,
125
+ version: coreVersion,
126
+ description: DESCRIPTIONS[major],
127
+ license: { name: 'MIT' },
128
+ },
129
+ 'x-frigg-api-version': major,
130
+ 'x-frigg-api-status': info.status,
131
+ ...(info.deprecatedAt && { 'x-frigg-deprecated-at': info.deprecatedAt }),
132
+ servers: [{ url: '/', description: 'This Frigg deployment' }],
133
+ security: [{ bearerAuth: [] }, { friggApiKey: [] }],
134
+ tags: TAGS,
135
+ paths,
136
+ components: {
137
+ schemas: SCHEMAS,
138
+ responses: RESPONSES,
139
+ securitySchemes: SECURITY_SCHEMES,
140
+ },
141
+ };
142
+ }
143
+
144
+ module.exports = { buildOpenApiDocument };