@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
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Response shapes for Management API v2. Use cases return domain records;
3
+ * these functions decide what leaves the server. Credential material never
4
+ * does (see maskCredential).
5
+ */
6
+
7
+ const idOf = (value) => {
8
+ if (value === undefined || value === null) return null;
9
+ if (typeof value === 'object') {
10
+ const id = value.id ?? value._id;
11
+ return id === undefined || id === null ? null : String(id);
12
+ }
13
+ return String(value);
14
+ };
15
+
16
+ /**
17
+ * `{ scope, owner, organizationId, access }` (ADR-056 §3.4): whose a record is
18
+ * and what the caller may do with it. Empty when the use case did not say.
19
+ */
20
+ function tenancyFields(tenancy) {
21
+ if (!tenancy) return {};
22
+ return {
23
+ scope: tenancy.scope,
24
+ owner: tenancy.owner,
25
+ organizationId: tenancy.organizationId ?? null,
26
+ access: tenancy.access,
27
+ };
28
+ }
29
+
30
+ /** An entity (connected account). */
31
+ function toEntityDto(entity) {
32
+ const credential =
33
+ entity.credential && typeof entity.credential === 'object'
34
+ ? entity.credential
35
+ : null;
36
+ return {
37
+ id: String(entity.id),
38
+ type: entity.moduleName ?? null,
39
+ name: entity.name ?? null,
40
+ externalId: entity.externalId ?? null,
41
+ credentialId: idOf(entity.credential ?? entity.credentialId),
42
+ userId: idOf(entity.userId ?? entity.user),
43
+ authIsValid:
44
+ typeof credential?.authIsValid === 'boolean'
45
+ ? credential.authIsValid
46
+ : null,
47
+ ...tenancyFields(entity.tenancy),
48
+ };
49
+ }
50
+
51
+ /** An integration, from a hydrated instance or a stored DTO. */
52
+ function toIntegrationDto(integration) {
53
+ return {
54
+ id: String(integration.id),
55
+ entities: integration.entities ?? [],
56
+ status: integration.status ?? null,
57
+ config: integration.config ?? {},
58
+ version: integration.version ?? null,
59
+ messages: integration.messages ?? {},
60
+ ...tenancyFields(integration.tenancy),
61
+ };
62
+ }
63
+
64
+ // Fields every repository adds around the stored data; everything else on a
65
+ // credential record came from the provider and is treated as secret.
66
+ const CREDENTIAL_METADATA = new Set([
67
+ 'id',
68
+ '_id',
69
+ 'userId',
70
+ 'user',
71
+ 'externalId',
72
+ 'authIsValid',
73
+ 'createdAt',
74
+ 'updatedAt',
75
+ 'type',
76
+ 'entityIds',
77
+ 'status',
78
+ // Added by the credential use cases (ADR-056 §3.4); never provider data.
79
+ 'tenancy',
80
+ ]);
81
+
82
+ /**
83
+ * Masks one stored credential value. Strings keep at most their last four
84
+ * characters, and only when long enough that four characters reveal nothing
85
+ * useful; objects and arrays are hidden whole. Numbers, booleans and null
86
+ * (expiry times, flags) are not secret and pass through.
87
+ */
88
+ function maskValue(value) {
89
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
90
+ return value;
91
+ }
92
+ if (typeof value === 'string') {
93
+ return value.length >= 16 ? `****${value.slice(-4)}` : '****';
94
+ }
95
+ return '****';
96
+ }
97
+
98
+ const toIso = (value) => {
99
+ if (!value) return null;
100
+ const date = value instanceof Date ? value : new Date(value);
101
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
102
+ };
103
+
104
+ /**
105
+ * A credential with every provider field masked. The keys stay visible so a
106
+ * client can tell an OAuth credential from an API-key one; the values never
107
+ * leave the server.
108
+ */
109
+ function maskCredential(credential) {
110
+ const data = {};
111
+ for (const [key, value] of Object.entries(credential)) {
112
+ if (CREDENTIAL_METADATA.has(key) || value === undefined) continue;
113
+ data[key] = maskValue(value);
114
+ }
115
+ const entityIds = Array.isArray(credential.entityIds)
116
+ ? credential.entityIds.map(String)
117
+ : [];
118
+ return {
119
+ id: String(credential.id),
120
+ type: credential.type ?? null,
121
+ externalId: credential.externalId ?? null,
122
+ userId: idOf(credential.userId),
123
+ authIsValid:
124
+ typeof credential.authIsValid === 'boolean'
125
+ ? credential.authIsValid
126
+ : null,
127
+ entityIds,
128
+ entityCount: entityIds.length,
129
+ createdAt: toIso(credential.createdAt),
130
+ updatedAt: toIso(credential.updatedAt),
131
+ ...tenancyFields(credential.tenancy),
132
+ data,
133
+ };
134
+ }
135
+
136
+ module.exports = {
137
+ tenancyFields,
138
+ toEntityDto,
139
+ toIntegrationDto,
140
+ maskCredential,
141
+ maskValue,
142
+ idOf,
143
+ };
@@ -0,0 +1,94 @@
1
+ const { toEntityDto, maskCredential } = require('../dto');
2
+ const { objectBody, requireFields, parseStep, optionalString } = require('../request');
3
+
4
+ /** The v2 body for a submitted step: masked records when complete. */
5
+ function toAuthorizationResponse(result) {
6
+ if (result.status !== 'complete') return result;
7
+ return {
8
+ status: 'complete',
9
+ entity: result.entity ? toEntityDto(result.entity) : null,
10
+ credential: result.credential ? maskCredential(result.credential) : null,
11
+ ...(result.previousCredentialId && {
12
+ previousCredentialId: result.previousCredentialId,
13
+ }),
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Management API v2 authorization handlers (multi-step authorize and
19
+ * credential re-authorization), keyed by operationId.
20
+ */
21
+ function createAuthorizationHandlers(useCases) {
22
+ const {
23
+ getAuthorizationStep,
24
+ submitAuthorizationStep,
25
+ getCredentialReauthorization,
26
+ reauthorizeCredentialForUser,
27
+ } = useCases;
28
+
29
+ const stepQuery = (req) => ({
30
+ step: parseStep(req.query.step),
31
+ sessionId: optionalString(req.query.sessionId, 'sessionId'),
32
+ state: optionalString(req.query.state, 'state'),
33
+ });
34
+
35
+ const stepBody = (req) => {
36
+ const body = objectBody(req);
37
+ return {
38
+ body,
39
+ step: parseStep(body.step),
40
+ sessionId: optionalString(body.sessionId, 'sessionId'),
41
+ };
42
+ };
43
+
44
+ return {
45
+ async getAuthorizationRequirements(req, res, user) {
46
+ const entityType = optionalString(req.query.entityType, 'entityType');
47
+ requireFields({ entityType }, ['entityType']);
48
+ res.json(
49
+ await getAuthorizationStep.execute({
50
+ user,
51
+ entityType,
52
+ ...stepQuery(req),
53
+ })
54
+ );
55
+ },
56
+
57
+ async authorize(req, res, user) {
58
+ const { body, step, sessionId } = stepBody(req);
59
+ requireFields(body, ['entityType', 'data']);
60
+ const result = await submitAuthorizationStep.execute({
61
+ user,
62
+ entityType: optionalString(body.entityType, 'entityType'),
63
+ data: body.data,
64
+ step,
65
+ sessionId,
66
+ scope: optionalString(body.scope, 'scope'),
67
+ });
68
+ res.json(toAuthorizationResponse(result));
69
+ },
70
+
71
+ async getCredentialReauthorizeRequirements(req, res, user) {
72
+ res.json(
73
+ await getCredentialReauthorization.execute(
74
+ req.params.credentialId,
75
+ user,
76
+ stepQuery(req)
77
+ )
78
+ );
79
+ },
80
+
81
+ async reauthorizeCredential(req, res, user) {
82
+ const { body, step, sessionId } = stepBody(req);
83
+ requireFields(body, ['data']);
84
+ const result = await reauthorizeCredentialForUser.execute(
85
+ req.params.credentialId,
86
+ user,
87
+ { data: body.data, step, sessionId }
88
+ );
89
+ res.json(toAuthorizationResponse(result));
90
+ },
91
+ };
92
+ }
93
+
94
+ module.exports = { createAuthorizationHandlers, toAuthorizationResponse };
@@ -0,0 +1,33 @@
1
+ const { maskCredential } = require('../dto');
2
+ const { parseListFilters } = require('../request');
3
+
4
+ /** Management API v2 credential handlers, keyed by operationId. */
5
+ function createCredentialHandlers(useCases) {
6
+ const { listCredentialsForUser, getCredentialForUser, deleteCredentialForUser } =
7
+ useCases;
8
+
9
+ return {
10
+ async listCredentials(req, res, user) {
11
+ const credentials = await listCredentialsForUser.execute(
12
+ user,
13
+ parseListFilters(req.query)
14
+ );
15
+ res.json({ credentials: credentials.map(maskCredential) });
16
+ },
17
+
18
+ async getCredential(req, res, user) {
19
+ res.json(
20
+ maskCredential(
21
+ await getCredentialForUser.execute(req.params.credentialId, user)
22
+ )
23
+ );
24
+ },
25
+
26
+ async deleteCredential(req, res, user) {
27
+ await deleteCredentialForUser.execute(req.params.credentialId, user);
28
+ res.status(204).end();
29
+ },
30
+ };
31
+ }
32
+
33
+ module.exports = { createCredentialHandlers };
@@ -0,0 +1,77 @@
1
+ const { toEntityDto } = require('../dto');
2
+ const { objectBody, parseStep, parseListFilters } = require('../request');
3
+
4
+ /** Management API v2 entity and entity-type handlers, keyed by operationId. */
5
+ function createEntityHandlers(useCases) {
6
+ const {
7
+ listEntitiesForUser,
8
+ getEntityForUser,
9
+ deleteEntityForUser,
10
+ testEntityAuthForUser,
11
+ getEntityOptionsForUser,
12
+ refreshEntityOptionsForUser,
13
+ listEntityTypes,
14
+ getEntityType,
15
+ getAuthorizationRequirements,
16
+ } = useCases;
17
+
18
+ return {
19
+ async listEntities(req, res, user) {
20
+ const entities = await listEntitiesForUser.execute(
21
+ user,
22
+ parseListFilters(req.query)
23
+ );
24
+ res.json({ entities: entities.map(toEntityDto) });
25
+ },
26
+
27
+ async listEntityTypes(_req, res, user) {
28
+ res.json({ types: await listEntityTypes.execute({ user }) });
29
+ },
30
+
31
+ async getEntityType(req, res, user) {
32
+ res.json(await getEntityType.execute(req.params.entityType, { user }));
33
+ },
34
+
35
+ // Describes a step's requirements without starting an authorization
36
+ // session; GET /api/v2/authorize starts one.
37
+ async getEntityTypeRequirements(req, res, user) {
38
+ res.json(
39
+ await getAuthorizationRequirements.execute({
40
+ entityType: req.params.entityType,
41
+ step: parseStep(req.query.step),
42
+ userId: user.getId(),
43
+ })
44
+ );
45
+ },
46
+
47
+ async getEntity(req, res, user) {
48
+ res.json(toEntityDto(await getEntityForUser.execute(req.params.entityId, user)));
49
+ },
50
+
51
+ async deleteEntity(req, res, user) {
52
+ await deleteEntityForUser.execute(req.params.entityId, user);
53
+ res.status(204).end();
54
+ },
55
+
56
+ async testEntityAuth(req, res, user) {
57
+ const ok = await testEntityAuthForUser.execute(req.params.entityId, user);
58
+ res.json({ status: ok ? 'ok' : 'failed' });
59
+ },
60
+
61
+ async getEntityOptions(req, res, user) {
62
+ res.json(await getEntityOptionsForUser.execute(req.params.entityId, user));
63
+ },
64
+
65
+ async refreshEntityOptions(req, res, user) {
66
+ res.json(
67
+ await refreshEntityOptionsForUser.execute(
68
+ req.params.entityId,
69
+ user,
70
+ objectBody(req)
71
+ )
72
+ );
73
+ },
74
+ };
75
+ }
76
+
77
+ module.exports = { createEntityHandlers };
@@ -0,0 +1,128 @@
1
+ const { toIntegrationDto } = require('../dto');
2
+ const { objectBody, optionalString, parseListFilters } = require('../request');
3
+
4
+ /**
5
+ * Management API v2 integration handlers, keyed by registry operationId.
6
+ * Each takes (req, res, user) after authentication and only calls use cases.
7
+ */
8
+ function createIntegrationHandlers(useCases) {
9
+ const {
10
+ getIntegrationsForUser,
11
+ getPossibleIntegrations,
12
+ createIntegrationForUser,
13
+ getIntegrationInstanceForUser,
14
+ updateIntegrationForUser,
15
+ removeIntegrationForUser,
16
+ sendIntegrationEventForUser,
17
+ runIntegrationActionForUser,
18
+ testIntegrationAuthForUser,
19
+ } = useCases;
20
+
21
+ const send = (req, user, event, payload) =>
22
+ sendIntegrationEventForUser.execute(
23
+ req.params.integrationId,
24
+ user,
25
+ event,
26
+ payload
27
+ );
28
+
29
+ return {
30
+ async listIntegrations(req, res, user) {
31
+ const integrations = await getIntegrationsForUser.execute(
32
+ user,
33
+ parseListFilters(req.query)
34
+ );
35
+ res.json({ integrations: integrations.map(toIntegrationDto) });
36
+ },
37
+
38
+ async listIntegrationOptions(_req, res) {
39
+ res.json({ integrations: await getPossibleIntegrations.execute() });
40
+ },
41
+
42
+ async createIntegration(req, res, user) {
43
+ const { entities, config } = objectBody(req);
44
+ const integration = await createIntegrationForUser.execute(
45
+ { entities, config },
46
+ user
47
+ );
48
+ res.status(201).json(toIntegrationDto(integration));
49
+ },
50
+
51
+ async getIntegration(req, res, user) {
52
+ const instance = await getIntegrationInstanceForUser.execute(
53
+ req.params.integrationId,
54
+ user
55
+ );
56
+ res.json(toIntegrationDto(instance));
57
+ },
58
+
59
+ async updateIntegration(req, res, user) {
60
+ const { config } = objectBody(req);
61
+ const integration = await updateIntegrationForUser.execute(
62
+ req.params.integrationId,
63
+ user,
64
+ config
65
+ );
66
+ res.json(toIntegrationDto(integration));
67
+ },
68
+
69
+ async deleteIntegration(req, res, user) {
70
+ await removeIntegrationForUser.execute(req.params.integrationId, user);
71
+ res.status(204).end();
72
+ },
73
+
74
+ async getIntegrationConfigOptions(req, res, user) {
75
+ res.json(await send(req, user, 'GET_CONFIG_OPTIONS'));
76
+ },
77
+
78
+ async refreshIntegrationConfigOptions(req, res, user) {
79
+ res.json(await send(req, user, 'REFRESH_CONFIG_OPTIONS', objectBody(req)));
80
+ },
81
+
82
+ async listIntegrationActions(req, res, user) {
83
+ const actionType = optionalString(req.query.actionType, 'actionType');
84
+ res.json(
85
+ await send(req, user, 'GET_USER_ACTIONS', actionType ? { actionType } : {})
86
+ );
87
+ },
88
+
89
+ async getIntegrationActionOptions(req, res, user) {
90
+ res.json(
91
+ await send(req, user, 'GET_USER_ACTION_OPTIONS', {
92
+ actionId: req.params.actionId,
93
+ data: objectBody(req),
94
+ })
95
+ );
96
+ },
97
+
98
+ async refreshIntegrationActionOptions(req, res, user) {
99
+ res.json(
100
+ await send(req, user, 'REFRESH_USER_ACTION_OPTIONS', {
101
+ actionId: req.params.actionId,
102
+ data: objectBody(req),
103
+ })
104
+ );
105
+ },
106
+
107
+ async runIntegrationAction(req, res, user) {
108
+ res.json(
109
+ await runIntegrationActionForUser.execute(
110
+ req.params.integrationId,
111
+ user,
112
+ req.params.actionId,
113
+ objectBody(req)
114
+ )
115
+ );
116
+ },
117
+
118
+ async testIntegrationAuth(req, res, user) {
119
+ const { ok, errors } = await testIntegrationAuthForUser.execute(
120
+ req.params.integrationId,
121
+ user
122
+ );
123
+ res.json(ok ? { status: 'ok' } : { status: 'failed', errors });
124
+ },
125
+ };
126
+ }
127
+
128
+ module.exports = { createIntegrationHandlers };
@@ -0,0 +1,34 @@
1
+ const { toV2ErrorBody } = require('../../error-response');
2
+
3
+ /**
4
+ * POST /api/v2/entities/:entityId/proxy (ADR-052). The HTTP status follows the
5
+ * upstream status. Proxy failures answer in the api-proxy.schema.json error
6
+ * shape, which is also the v2 error shape plus `success` and `status`.
7
+ */
8
+ function createProxyHandlers({ executeEntityProxyRequest }) {
9
+ return {
10
+ async proxyEntityRequest(req, res, user) {
11
+ try {
12
+ const result = await executeEntityProxyRequest.execute(
13
+ req.params.entityId,
14
+ user,
15
+ req.body
16
+ );
17
+ res.status(result.status).json(result.body);
18
+ } catch (error) {
19
+ if (!error?.isBoom || !error.data?.proxy) throw error;
20
+ const status = error.output.statusCode;
21
+ if (error.data.retryAfter) {
22
+ res.set('Retry-After', String(error.data.retryAfter));
23
+ }
24
+ res.status(status).json({
25
+ success: false,
26
+ status,
27
+ ...toV2ErrorBody(error),
28
+ });
29
+ }
30
+ },
31
+ };
32
+ }
33
+
34
+ module.exports = { createProxyHandlers };
@@ -0,0 +1,85 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { LIST_OWNERS } = require('../../modules/scope-visibility');
3
+
4
+ /** The JSON body as an object, or a 400 VALIDATION_ERROR. */
5
+ function objectBody(req) {
6
+ const body = req.body;
7
+ if (body === undefined || body === null || body === '') return {};
8
+ if (typeof body !== 'object' || Array.isArray(body)) {
9
+ throw Boom.badRequest('Request body must be a JSON object', {
10
+ code: 'VALIDATION_ERROR',
11
+ });
12
+ }
13
+ return body;
14
+ }
15
+
16
+ /** Fields that must be present (non-empty) in the body. */
17
+ function requireFields(body, fields) {
18
+ const missing = fields.filter(
19
+ (field) =>
20
+ body[field] === undefined || body[field] === null || body[field] === ''
21
+ );
22
+ if (missing.length) {
23
+ throw Boom.badRequest(
24
+ `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
25
+ { code: 'VALIDATION_ERROR', details: { missing } }
26
+ );
27
+ }
28
+ return body;
29
+ }
30
+
31
+ /** A 1-based step number from a query or body value; 1 when absent. */
32
+ function parseStep(value) {
33
+ if (value === undefined || value === null || value === '') return 1;
34
+ const step = Number(value);
35
+ if (!Number.isInteger(step) || step < 1) {
36
+ throw Boom.badRequest('step must be a positive integer', {
37
+ code: 'INVALID_STEP',
38
+ });
39
+ }
40
+ return step;
41
+ }
42
+
43
+ /** An optional string query/body value. */
44
+ function optionalString(value, name) {
45
+ if (value === undefined || value === null || value === '') return undefined;
46
+ if (typeof value !== 'string') {
47
+ throw Boom.badRequest(`${name} must be a string`, {
48
+ code: 'VALIDATION_ERROR',
49
+ details: { field: name },
50
+ });
51
+ }
52
+ return value;
53
+ }
54
+
55
+ const LIST_SCOPES = ['individual', 'organization'];
56
+
57
+ /**
58
+ * `?scope=individual|organization` and `?owner=me|org` on a list (ADR-056
59
+ * §3.4). Anything else is a 400.
60
+ */
61
+ function parseListFilters(query = {}) {
62
+ const scope = optionalString(query.scope, 'scope');
63
+ const owner = optionalString(query.owner, 'owner');
64
+ if (scope !== undefined && !LIST_SCOPES.includes(scope)) {
65
+ throw Boom.badRequest(`scope must be one of ${LIST_SCOPES.join(', ')}`, {
66
+ code: 'VALIDATION_ERROR',
67
+ details: { field: 'scope' },
68
+ });
69
+ }
70
+ if (owner !== undefined && !Object.hasOwn(LIST_OWNERS, owner)) {
71
+ throw Boom.badRequest('owner must be me or org', {
72
+ code: 'VALIDATION_ERROR',
73
+ details: { field: 'owner' },
74
+ });
75
+ }
76
+ return { scope, owner };
77
+ }
78
+
79
+ module.exports = {
80
+ objectBody,
81
+ requireFields,
82
+ parseStep,
83
+ optionalString,
84
+ parseListFilters,
85
+ };