@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.694.f66a64a.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 (267) hide show
  1. package/CLAUDE.md +0 -28
  2. package/README.md +0 -105
  3. package/application/commands/integration-commands.js +10 -20
  4. package/application/commands/scheduler-commands.js +7 -11
  5. package/application/commands/user-commands.js +5 -17
  6. package/application/index.js +2 -6
  7. package/core/Worker.js +7 -31
  8. package/core/create-handler.js +0 -9
  9. package/credential/repositories/credential-repository-documentdb.js +0 -34
  10. package/credential/repositories/credential-repository-interface.js +0 -15
  11. package/credential/repositories/credential-repository-mongo.js +0 -23
  12. package/credential/repositories/credential-repository-postgres.js +0 -28
  13. package/database/config.js +24 -48
  14. package/database/documentdb-utils.js +2 -2
  15. package/database/encryption/encryption-schema-registry.js +12 -93
  16. package/database/prisma.js +2 -4
  17. package/errors/index.js +4 -0
  18. package/errors/mapping-already-exists-error.js +14 -0
  19. package/generated/prisma-mongodb/edge.js +38 -33
  20. package/generated/prisma-mongodb/index-browser.js +35 -30
  21. package/generated/prisma-mongodb/index.d.ts +1880 -3191
  22. package/generated/prisma-mongodb/index.js +38 -33
  23. package/generated/prisma-mongodb/package.json +1 -1
  24. package/generated/prisma-mongodb/schema.prisma +50 -86
  25. package/generated/prisma-mongodb/wasm.js +38 -33
  26. package/generated/prisma-postgresql/edge.js +38 -33
  27. package/generated/prisma-postgresql/index-browser.js +35 -30
  28. package/generated/prisma-postgresql/index.d.ts +2050 -3390
  29. package/generated/prisma-postgresql/index.js +38 -33
  30. package/generated/prisma-postgresql/package.json +1 -1
  31. package/generated/prisma-postgresql/schema.prisma +48 -81
  32. package/generated/prisma-postgresql/wasm.js +38 -33
  33. package/handlers/WEBHOOKS.md +29 -0
  34. package/handlers/app-definition-loader.js +19 -128
  35. package/handlers/app-handler-helpers.js +3 -25
  36. package/handlers/backend-utils.js +25 -31
  37. package/handlers/middleware/admin-auth.js +1 -14
  38. package/handlers/routers/auth.js +10 -46
  39. package/handlers/routers/health.js +4 -13
  40. package/handlers/routers/integration-defined-routers.js +92 -144
  41. package/handlers/routers/integration-webhook-routers.js +64 -102
  42. package/handlers/workers/integration-defined-workers.js +2 -5
  43. package/handlers/workers/integration-triggers.js +25 -0
  44. package/index.js +3 -65
  45. package/infrastructure/scheduler/index.js +6 -19
  46. package/infrastructure/scheduler/recurrence.js +204 -0
  47. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  48. package/integrations/data-defined/README.md +185 -0
  49. package/integrations/data-defined/canonical.js +11 -0
  50. package/integrations/data-defined/capabilities/acmeerp.capabilities.json +608 -0
  51. package/integrations/data-defined/capabilities/asana.capabilities.json +265 -0
  52. package/integrations/data-defined/capabilities/calendly.capabilities.json +385 -0
  53. package/integrations/data-defined/capabilities/docusign.capabilities.json +314 -0
  54. package/integrations/data-defined/capabilities/fathom.capabilities.json +228 -0
  55. package/integrations/data-defined/capabilities/github.capabilities.json +563 -0
  56. package/integrations/data-defined/capabilities/hubspot.capabilities.json +715 -0
  57. package/integrations/data-defined/capabilities/jira.capabilities.json +376 -0
  58. package/integrations/data-defined/capabilities/klaviyo.capabilities.json +475 -0
  59. package/integrations/data-defined/capabilities/linear.capabilities.json +324 -0
  60. package/integrations/data-defined/capabilities/netsuite.capabilities.json +491 -0
  61. package/integrations/data-defined/capabilities/notion.capabilities.json +208 -0
  62. package/integrations/data-defined/capabilities/phone.capabilities.json +470 -0
  63. package/integrations/data-defined/capabilities/pipedrive.capabilities.json +388 -0
  64. package/integrations/data-defined/capabilities/quickbooks.capabilities.json +615 -0
  65. package/integrations/data-defined/capabilities/salesforce.capabilities.json +507 -0
  66. package/integrations/data-defined/capabilities/shopify.capabilities.json +1082 -0
  67. package/integrations/data-defined/capabilities/slack.capabilities.json +363 -0
  68. package/integrations/data-defined/capabilities/stripe.capabilities.json +580 -0
  69. package/integrations/data-defined/capabilities/webflow.capabilities.json +196 -0
  70. package/integrations/data-defined/capabilities/zendesk.capabilities.json +409 -0
  71. package/integrations/data-defined/compile.js +80 -0
  72. package/integrations/data-defined/config-form.js +137 -0
  73. package/integrations/data-defined/data-defined-integration.js +417 -0
  74. package/integrations/data-defined/examples/acme-erp-quickbooks.definition.json +467 -0
  75. package/integrations/data-defined/examples/asana-slack.definition.json +220 -0
  76. package/integrations/data-defined/examples/calendly-salesforce.definition.json +459 -0
  77. package/integrations/data-defined/examples/docusign-salesforce.definition.json +305 -0
  78. package/integrations/data-defined/examples/fathom-hubspot-notion.definition.json +575 -0
  79. package/integrations/data-defined/examples/github-linear.definition.json +316 -0
  80. package/integrations/data-defined/examples/hubspot-slack.definition.json +409 -0
  81. package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +339 -0
  82. package/integrations/data-defined/examples/phone-hubspot.definition.json +637 -0
  83. package/integrations/data-defined/examples/pipedrive-hubspot.definition.json +136 -0
  84. package/integrations/data-defined/examples/pipedrive-phone.definition.json +340 -0
  85. package/integrations/data-defined/examples/records/phone-hubspot.definition-version.json +652 -0
  86. package/integrations/data-defined/examples/records/phone-hubspot.integration.json +86 -0
  87. package/integrations/data-defined/examples/records/phone-hubspot.mappings.json +39 -0
  88. package/integrations/data-defined/examples/records/phone-hubspot.workflow-run.json +62 -0
  89. package/integrations/data-defined/examples/shopify-klaviyo.definition.json +488 -0
  90. package/integrations/data-defined/examples/shopify-netsuite.definition.json +622 -0
  91. package/integrations/data-defined/examples/stripe-quickbooks.definition.json +476 -0
  92. package/integrations/data-defined/examples/webflow-hubspot-slack.definition.json +262 -0
  93. package/integrations/data-defined/examples/zendesk-jira.definition.json +604 -0
  94. package/integrations/data-defined/expressions.js +225 -0
  95. package/integrations/data-defined/index.js +39 -0
  96. package/integrations/data-defined/interpreter.js +89 -0
  97. package/integrations/data-defined/manifests.js +125 -0
  98. package/integrations/data-defined/repositories/definition-version-repository.js +67 -0
  99. package/integrations/data-defined/repositories/workflow-run-repository.js +59 -0
  100. package/integrations/data-defined/router.js +97 -0
  101. package/integrations/data-defined/runtime.js +208 -0
  102. package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +377 -0
  103. package/integrations/data-defined/schema/installation.v0.1.schema.json +130 -0
  104. package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +902 -0
  105. package/integrations/data-defined/schema/records.md +98 -0
  106. package/integrations/data-defined/schema-check.js +58 -0
  107. package/integrations/data-defined/scripts/validate.js +66 -0
  108. package/integrations/data-defined/steps.js +89 -0
  109. package/integrations/data-defined/transforms.js +91 -0
  110. package/integrations/data-defined/use-cases/definition-use-cases.js +187 -0
  111. package/integrations/data-defined/validate.js +384 -0
  112. package/integrations/data-defined/versioning.js +176 -0
  113. package/integrations/data-defined/webhooks.js +119 -0
  114. package/integrations/index.js +0 -1
  115. package/integrations/integration-base.js +117 -0
  116. package/integrations/integration-router.js +101 -131
  117. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  118. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  119. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  120. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  121. package/integrations/repositories/integration-mapping-repository.js +29 -0
  122. package/integrations/repositories/integration-repository-documentdb.js +9 -37
  123. package/integrations/repositories/integration-repository-interface.js +4 -17
  124. package/integrations/repositories/integration-repository-mongo.js +96 -64
  125. package/integrations/repositories/integration-repository-postgres.js +81 -63
  126. package/integrations/repositories/process-repository-mongo.js +1 -1
  127. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  128. package/integrations/triggers.js +244 -0
  129. package/integrations/use-cases/create-integration.js +20 -55
  130. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  131. package/integrations/use-cases/index.js +1 -7
  132. package/integrations/use-cases/run-trigger-tick.js +132 -0
  133. package/integrations/use-cases/run-trigger.js +77 -0
  134. package/integrations/utils/map-integration-dto.js +14 -43
  135. package/modules/index.js +0 -14
  136. package/modules/module-factory.js +0 -12
  137. package/modules/module.js +2 -53
  138. package/modules/repositories/module-repository-documentdb.js +6 -51
  139. package/modules/repositories/module-repository-interface.js +2 -19
  140. package/modules/repositories/module-repository-mongo.js +74 -67
  141. package/modules/repositories/module-repository-postgres.js +74 -66
  142. package/modules/repositories/module-repository.js +1 -2
  143. package/modules/requester/requester.js +5 -35
  144. package/modules/use-cases/get-entities-for-user.js +13 -26
  145. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  146. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  147. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  148. package/modules/use-cases/get-module.js +4 -10
  149. package/modules/use-cases/process-authorization-callback.js +19 -53
  150. package/modules/use-cases/refresh-entity-options.js +4 -10
  151. package/modules/use-cases/test-module-auth.js +4 -10
  152. package/package.json +6 -6
  153. package/prisma-mongodb/schema.prisma +50 -86
  154. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +66 -0
  155. package/prisma-postgresql/schema.prisma +48 -81
  156. package/queues/index.js +0 -25
  157. package/queues/queuer-util.js +9 -2
  158. package/types/index.d.ts +0 -2
  159. package/user/repositories/user-repository-documentdb.js +2 -27
  160. package/user/repositories/user-repository-interface.js +0 -18
  161. package/user/repositories/user-repository-mongo.js +0 -24
  162. package/user/repositories/user-repository-postgres.js +0 -18
  163. package/user/use-cases/authenticate-user.js +1 -7
  164. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  165. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  166. package/user/user.js +1 -64
  167. package/credential/use-cases/credential-operations-for-user.js +0 -202
  168. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  169. package/database/organization-where.js +0 -18
  170. package/database/unique-violation.js +0 -25
  171. package/extensions/bootstrap-runner.js +0 -43
  172. package/extensions/extension-loader.js +0 -187
  173. package/extensions/index.js +0 -41
  174. package/extensions/initialize-app.js +0 -123
  175. package/extensions/route-mounter.js +0 -103
  176. package/extensions/schema-composer.js +0 -188
  177. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  178. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  179. package/integrations/repositories/integration-not-found.js +0 -14
  180. package/integrations/schedule-config.js +0 -80
  181. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  182. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  183. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  184. package/management-api/create-management-api-router.js +0 -49
  185. package/management-api/error-response.js +0 -116
  186. package/management-api/management-api-config.js +0 -151
  187. package/management-api/meta-router.js +0 -76
  188. package/management-api/openapi/build-openapi.js +0 -144
  189. package/management-api/openapi/components.js +0 -415
  190. package/management-api/openapi/v1-operations.js +0 -83
  191. package/management-api/openapi/v2-operations.js +0 -264
  192. package/management-api/route-registry.js +0 -411
  193. package/management-api/test-support/http-client.js +0 -39
  194. package/management-api/use-cases/get-management-api-meta.js +0 -57
  195. package/management-api/v1-deprecation.js +0 -120
  196. package/management-api/v2/create-v2-router.js +0 -85
  197. package/management-api/v2/dependencies.js +0 -281
  198. package/management-api/v2/dto.js +0 -143
  199. package/management-api/v2/handlers/authorization.js +0 -94
  200. package/management-api/v2/handlers/credentials.js +0 -33
  201. package/management-api/v2/handlers/entities.js +0 -77
  202. package/management-api/v2/handlers/integrations.js +0 -128
  203. package/management-api/v2/handlers/proxy.js +0 -34
  204. package/management-api/v2/request.js +0 -85
  205. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  206. package/modules/domain/authorization-session.js +0 -144
  207. package/modules/entity-scope.js +0 -15
  208. package/modules/entity-type-registry.js +0 -311
  209. package/modules/proxy/proxy-bases.js +0 -169
  210. package/modules/proxy/proxy-errors.js +0 -31
  211. package/modules/proxy/proxy-path.js +0 -183
  212. package/modules/proxy/proxy-policy.js +0 -277
  213. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  214. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  215. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  216. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  217. package/modules/repositories/entity-not-found.js +0 -18
  218. package/modules/requester/proxy-request.js +0 -245
  219. package/modules/scope-config.js +0 -192
  220. package/modules/scope-policy.js +0 -112
  221. package/modules/scope-resolution.js +0 -288
  222. package/modules/scope-visibility.js +0 -247
  223. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  224. package/modules/tests/doubles/ownership-matching.js +0 -18
  225. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  226. package/modules/use-cases/create-entity-from-credential.js +0 -113
  227. package/modules/use-cases/entity-operations-for-user.js +0 -162
  228. package/modules/use-cases/entity-types.js +0 -139
  229. package/modules/use-cases/execute-entity-proxy-request.js +0 -331
  230. package/modules/use-cases/get-authorization-requirements.js +0 -100
  231. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  232. package/modules/use-cases/manage-access.js +0 -21
  233. package/modules/use-cases/multi-step-authorization.js +0 -329
  234. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  235. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  236. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  237. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  238. package/providers/index.js +0 -23
  239. package/providers/resolve-provider.js +0 -93
  240. package/providers/runtime-detection.js +0 -124
  241. package/queues/providers/index.js +0 -11
  242. package/queues/providers/netlify-background-provider.js +0 -150
  243. package/queues/providers/qstash-queue-provider.js +0 -219
  244. package/queues/providers/sqs-queue-provider.js +0 -70
  245. package/queues/queue-client-interface.js +0 -55
  246. package/queues/queue-message-auth.js +0 -193
  247. package/queues/queue-provider-factory.js +0 -101
  248. package/queues/queue-provider.js +0 -65
  249. package/queues/sqs-queue-client.js +0 -81
  250. package/types/modules/index.d.ts +0 -214
  251. package/types/user/index.d.ts +0 -134
  252. package/user/adopter-jwt-config.js +0 -177
  253. package/user/adopter-jwt-errors.js +0 -59
  254. package/user/adopter-token-verifier.js +0 -122
  255. package/user/app-user-helpers.js +0 -97
  256. package/user/create-user-authentication.js +0 -64
  257. package/user/membership.js +0 -51
  258. package/user/repositories/organization-membership-record.js +0 -19
  259. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  260. package/user/repositories/organization-membership-repository-factory.js +0 -42
  261. package/user/repositories/organization-membership-repository-interface.js +0 -57
  262. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  263. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  264. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  265. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  266. package/user/use-cases/ensure-organization-membership.js +0 -92
  267. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,139 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { ModuleConstants } = require('../ModuleConstants');
3
- const { usedByOf } = require('../entity-type-registry');
4
-
5
- const AUTH_TYPE_BY_REQUESTER = {
6
- [ModuleConstants.authType.oauth2]: 'oauth2',
7
- [ModuleConstants.authType.oauth1]: 'oauth1',
8
- [ModuleConstants.authType.apiKey]: 'api-key',
9
- [ModuleConstants.authType.basic]: 'basic',
10
- };
11
-
12
- const callIfFunction = (target, name) =>
13
- typeof target?.[name] === 'function' ? target[name]() : undefined;
14
-
15
- /** Number of authorization steps a module declares (1 unless multi-step). */
16
- function getAuthStepCount(definition) {
17
- const count = callIfFunction(definition, 'getAuthStepCount');
18
- return Number.isInteger(count) && count > 0 ? count : 1;
19
- }
20
-
21
- /**
22
- * Public description of an API module (an "entity type"). Reads only the
23
- * definition: display metadata, auth type and step count. Never credentials
24
- * or env values.
25
- */
26
- function describeEntityType(definition, typeName = definition.moduleName) {
27
- const stepCount = getAuthStepCount(definition);
28
- const display = definition.display || {};
29
- const description = {
30
- type: typeName,
31
- name:
32
- callIfFunction(definition, 'getDisplayName') ??
33
- display.label ??
34
- display.name ??
35
- typeName,
36
- authType:
37
- callIfFunction(definition, 'getAuthType') ??
38
- AUTH_TYPE_BY_REQUESTER[definition.API?.requesterType] ??
39
- 'custom',
40
- isMultiStep: stepCount > 1,
41
- stepCount,
42
- };
43
- const text = callIfFunction(definition, 'getDescription') ?? display.description;
44
- if (text) description.description = text;
45
- const capabilities = callIfFunction(definition, 'getCapabilities');
46
- if (capabilities !== undefined) description.capabilities = capabilities;
47
- return description;
48
- }
49
-
50
- function findDefinition(moduleDefinitions, entityType) {
51
- const definition = moduleDefinitions.find(
52
- (def) => def.moduleName === entityType
53
- );
54
- if (!definition) {
55
- throw Boom.notFound(`Entity type '${entityType}' not found`, {
56
- code: 'ENTITY_TYPE_NOT_FOUND',
57
- });
58
- }
59
- return definition;
60
- }
61
-
62
- /**
63
- * With a scope policy the entry also says which scope a new connection gets
64
- * and which a request may choose (ADR-056 §3.1), so a UI can offer the choice.
65
- * `global` is never offered: a tenant request cannot create one.
66
- */
67
- const scopeOf = (scopePolicy, name, user) => {
68
- if (!scopePolicy) return {};
69
- const { scope, source, allow } = scopePolicy.resolveScope({
70
- entityType: name,
71
- user,
72
- });
73
- return {
74
- scope: {
75
- default: scope,
76
- allow: allow.filter((value) => value !== 'global'),
77
- source,
78
- },
79
- };
80
- };
81
-
82
- const describeEntry = (name, entry, scopePolicy, user) => ({
83
- // The registry key is the name the API uses, whatever the definition spells.
84
- ...describeEntityType(entry.definition, name),
85
- usedBy: usedByOf(entry),
86
- ...scopeOf(scopePolicy, name, user),
87
- });
88
-
89
- function requireRegistry(entityTypeRegistry) {
90
- if (!(entityTypeRegistry instanceof Map)) {
91
- throw new TypeError('entityTypeRegistry is required (see getEntityTypeRegistry)');
92
- }
93
- return entityTypeRegistry;
94
- }
95
-
96
- /**
97
- * Lists the entity types this app can authorize: registry entries plus every
98
- * module an integration binds (ADR-059). `usedBy` names the integrations that
99
- * bind each type; it is empty for a registry-only type.
100
- */
101
- class ListEntityTypes {
102
- constructor({ entityTypeRegistry, scopePolicy }) {
103
- this.registry = requireRegistry(entityTypeRegistry);
104
- this.scopePolicy = scopePolicy;
105
- }
106
-
107
- /** @param {{user?: Object}} [params] - the caller, for the level-6 scope default */
108
- async execute({ user } = {}) {
109
- return [...this.registry]
110
- .map(([name, entry]) => describeEntry(name, entry, this.scopePolicy, user))
111
- .sort((a, b) => a.name.localeCompare(b.name));
112
- }
113
- }
114
-
115
- /** Describes one entity type, looked up by its registry key; 404 when absent. */
116
- class GetEntityType {
117
- constructor({ entityTypeRegistry, scopePolicy }) {
118
- this.registry = requireRegistry(entityTypeRegistry);
119
- this.scopePolicy = scopePolicy;
120
- }
121
-
122
- async execute(entityType, { user } = {}) {
123
- const entry = this.registry.get(entityType);
124
- if (!entry) {
125
- throw Boom.notFound(`Entity type '${entityType}' not found`, {
126
- code: 'ENTITY_TYPE_NOT_FOUND',
127
- });
128
- }
129
- return describeEntry(entityType, entry, this.scopePolicy, user);
130
- }
131
- }
132
-
133
- module.exports = {
134
- ListEntityTypes,
135
- GetEntityType,
136
- describeEntityType,
137
- getAuthStepCount,
138
- findDefinition,
139
- };
@@ -1,331 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { getLogger } = require('../../logs');
3
- const { getTelemetry } = require('../../telemetry/telemetry-runtime');
4
- const { ProxyRequestError, ProxyConfigError } = require('../proxy/proxy-errors');
5
- const { SCOPES } = require('../scope-resolution');
6
- const { grantsUse } = require('../scope-visibility');
7
- const { normalizeProxyPath, matchAllowRule } = require('../proxy/proxy-path');
8
- const { DEFAULT_BASE, resolveModuleBases } = require('../proxy/proxy-bases');
9
- const {
10
- validateProxyRequest,
11
- resolveAllowRules,
12
- filterRequestHeaders,
13
- filterResponseHeaders,
14
- decodeResponseBody,
15
- } = require('../proxy/proxy-policy');
16
-
17
- const log = getLogger('frigg.api.proxy');
18
-
19
- /** HTTP status and audit outcome for each proxy error code (ADR-052 §10). */
20
- const ERROR_MAP = {
21
- INVALID_PROXY_REQUEST: { status: 400, outcome: 'denied' },
22
- PROXY_UNKNOWN_BASE: { status: 400, outcome: 'denied' },
23
- PROXY_NOT_ALLOWED: { status: 403, outcome: 'denied' },
24
- PAYLOAD_TOO_LARGE: { status: 413, outcome: 'too_large' },
25
- UNSUPPORTED_MEDIA_TYPE: { status: 415, outcome: 'denied' },
26
- PROXY_NOT_CONFIGURED: { status: 501, outcome: 'denied' },
27
- INVALID_CREDENTIALS: { status: 401, outcome: 'upstream_error' },
28
- RATE_LIMITED: { status: 429, outcome: 'upstream_error' },
29
- UPSTREAM_ERROR: { status: 502, outcome: 'upstream_error' },
30
- NETWORK_ERROR: { status: 502, outcome: 'upstream_error' },
31
- RESPONSE_TOO_LARGE: { status: 502, outcome: 'too_large' },
32
- TIMEOUT: { status: 504, outcome: 'timeout' },
33
- };
34
-
35
- function toBoom(error, extra = {}) {
36
- const mapping = ERROR_MAP[error.code] || { status: 502 };
37
- const boom = new Boom.Boom(error.message, {
38
- statusCode: mapping.status,
39
- data: {
40
- code: error.code,
41
- details: error.details,
42
- // Every proxy message is written for the caller (no URLs, no
43
- // credential material), including the 5xx ones.
44
- expose: true,
45
- proxy: true,
46
- ...extra,
47
- },
48
- });
49
- return boom;
50
- }
51
-
52
- const authHeaderNamesOf = (api) =>
53
- [api?.api_key_name, api?.API_KEY_NAME].filter(
54
- (name) => typeof name === 'string' && name
55
- );
56
-
57
- /**
58
- * POST /api/v2/entities/:entityId/proxy (ADR-052): calls the entity's API
59
- * through its module with the stored credential, inside the limits the app
60
- * and the module set.
61
- *
62
- * Order of checks: request shape and size, path rules, entity visibility in
63
- * the active organization (404), `use` access (403), the entity's scope
64
- * against `managementApi.proxy.scopes` (403), the module's allow-list (403,
65
- * deny by default), credential readiness (401), then the call through
66
- * Requester._proxyRequest. Every call,
67
- * allowed or denied, writes one audit record and one usage count. Bodies,
68
- * headers, query values and the raw path are never logged.
69
- */
70
- class ExecuteEntityProxyRequest {
71
- /**
72
- * @param {Object} params
73
- * @param {import('./get-entity-module-for-user').GetEntityModuleForUser} params.getEntityModuleForUser
74
- * @param {ReturnType<import('../../management-api/management-api-config').resolveManagementApiConfig>['proxy']} params.proxyConfig
75
- * @param {Object} [params.telemetry]
76
- * @param {() => number} [params.now]
77
- */
78
- constructor({ getEntityModuleForUser, proxyConfig, telemetry, now }) {
79
- this.getEntityModuleForUser = getEntityModuleForUser;
80
- this.proxyConfig = proxyConfig;
81
- this.telemetry = telemetry;
82
- this.now = now || Date.now;
83
- }
84
-
85
- /**
86
- * @param {string} entityId
87
- * @param {Object} user - authenticated User
88
- * @param {Object} request - { method, path, query?, headers?, body? }
89
- * @returns {Promise<{ status: number, body: { success: boolean, status: number, headers: Object, data: unknown, dataEncoding?: string } }>}
90
- */
91
- async execute(entityId, user, request) {
92
- const started = this.now();
93
- const audit = {
94
- userId: user?.getId?.(),
95
- organizationId: user?.getOrganizationUser?.()?.id,
96
- entityId: String(entityId),
97
- method: typeof request?.method === 'string' ? request.method.toUpperCase() : undefined,
98
- requestBytes: 0,
99
- };
100
-
101
- try {
102
- const validated = validateProxyRequest(request, this.proxyConfig);
103
- audit.method = validated.method;
104
- audit.requestBytes = validated.requestBytes;
105
- const path = normalizeProxyPath(validated.path);
106
-
107
- // Visible in the active organization, or 404 (ADR-056 §3.6): the
108
- // same clauses the v2 lists use, so a missing entity, another
109
- // person's and one paired with another organization look alike.
110
- const { entity, definition, module } =
111
- await this.getEntityModuleForUser.execute(entityId, user);
112
- audit.module = definition.moduleName;
113
- this.assertProxyable(entityId, entity, definition, user);
114
-
115
- // The base is a name the module declared; the host comes from the
116
- // module's declaration, never from the request.
117
- const base = validated.base ?? DEFAULT_BASE;
118
- audit.base = base;
119
- const bases = this.basesOf(definition);
120
- if (!bases.names.includes(base)) {
121
- throw new ProxyRequestError(
122
- 'PROXY_UNKNOWN_BASE',
123
- `Module ${definition.moduleName} has no base named ${base}; use one of: ${bases.names.join(', ')}`,
124
- { module: definition.moduleName, base, bases: bases.names }
125
- );
126
- }
127
-
128
- const credentialRef = entity.credential;
129
- const credentialId =
130
- credentialRef && typeof credentialRef === 'object'
131
- ? credentialRef.id ?? credentialRef._id
132
- : credentialRef;
133
- if (credentialId !== undefined && credentialId !== null) {
134
- audit.credentialId = String(credentialId);
135
- }
136
-
137
- const { rules } = resolveAllowRules(definition, this.proxyConfig.modules);
138
- const rule = matchAllowRule(rules, validated.method, path, base);
139
- if (!rule) {
140
- throw new ProxyRequestError(
141
- 'PROXY_NOT_ALLOWED',
142
- `Module ${definition.moduleName} does not allow ${validated.method} on this path` +
143
- // Single-base modules keep the message they always had.
144
- (bases.names.length > 1 ? ` of base ${base}` : ''),
145
- { module: definition.moduleName, method: validated.method, base }
146
- );
147
- }
148
- audit.pattern = rule.path;
149
-
150
- const credential = entity.credential;
151
- if (!credential || (typeof credential === 'object' && credential.authIsValid === false)) {
152
- throw new ProxyRequestError(
153
- 'INVALID_CREDENTIALS',
154
- 'The entity has no usable credential; re-authorize it'
155
- );
156
- }
157
-
158
- const authHeaderNames = authHeaderNamesOf(module.api);
159
- const headers = filterRequestHeaders(validated.headers, {
160
- moduleHeaders: Array.isArray(definition.proxy?.headers) ? definition.proxy.headers : [],
161
- authHeaderNames,
162
- });
163
- if (validated.body !== undefined && !headers['content-type']) {
164
- headers['content-type'] = validated.bodyIsJson ? 'application/json' : 'text/plain';
165
- }
166
-
167
- const upstream = await module.api._proxyRequest({
168
- method: validated.method,
169
- path,
170
- query: validated.query,
171
- headers,
172
- body: validated.body,
173
- baseUrl: this.resolveBaseUrl(bases, module.api, base),
174
- timeoutMs: this.proxyConfig.timeoutMs,
175
- maxResponseBytes: this.proxyConfig.maxResponseBytes,
176
- isAllowedRedirect: (nextPath, nextMethod) =>
177
- Boolean(matchAllowRule(rules, nextMethod, nextPath, base)),
178
- });
179
- audit.upstreamStatus = upstream.status;
180
- audit.responseBytes = upstream.body?.length ?? 0;
181
-
182
- if (upstream.status === 429) {
183
- throw Object.assign(
184
- new ProxyRequestError('RATE_LIMITED', 'The upstream API is rate limiting this account', {
185
- upstreamStatus: 429,
186
- }),
187
- { retryAfter: upstream.headers['retry-after'] }
188
- );
189
- }
190
- if (upstream.status >= 500) {
191
- throw new ProxyRequestError('UPSTREAM_ERROR', 'The upstream API returned an error', {
192
- upstreamStatus: upstream.status,
193
- });
194
- }
195
-
196
- const decoded = decodeResponseBody(upstream.body, upstream.contentType, {
197
- binary: definition.proxy?.binary === true,
198
- });
199
- const result = {
200
- status: upstream.status,
201
- body: {
202
- success: upstream.status < 400,
203
- status: upstream.status,
204
- headers: filterResponseHeaders(upstream.headers, {
205
- redirectBlocked: upstream.redirectBlocked,
206
- authHeaderNames,
207
- }),
208
- data: decoded.data,
209
- ...(decoded.encoding && { dataEncoding: decoded.encoding }),
210
- },
211
- };
212
- this.record(audit, upstream.status < 400 ? 'ok' : 'upstream_error', started);
213
- return result;
214
- } catch (error) {
215
- if (error instanceof ProxyRequestError) {
216
- const { outcome = 'upstream_error' } = ERROR_MAP[error.code] || {};
217
- this.record(audit, outcome, started, error.code);
218
- throw toBoom(error, error.retryAfter ? { retryAfter: error.retryAfter } : {});
219
- }
220
- const notFound = error?.isBoom && error.output.statusCode === 404;
221
- this.record(
222
- audit,
223
- notFound ? 'denied' : 'error',
224
- started,
225
- error?.data?.code || (error?.isBoom ? undefined : 'INTERNAL_ERROR')
226
- );
227
- throw error;
228
- }
229
- }
230
-
231
- /**
232
- * ADR-056 §3.5: the caller needs `use` access to the entity in the active
233
- * organization, and the entity's scope must be one the app lets the proxy
234
- * accept. `access` and `scope` come from the description the v2 lists
235
- * return (GetEntityModuleForUser.describe), never from a second rule.
236
- *
237
- * Both refusals are 403 PROXY_NOT_ALLOWED: the caller can already see the
238
- * entity, so saying it exists leaks nothing. `details.reason` tells them
239
- * apart ('access' or 'scope').
240
- */
241
- assertProxyable(entityId, entity, definition, user) {
242
- const tenancy = this.getEntityModuleForUser.describe(entity, user);
243
- if (!tenancy) {
244
- // Not visible after all: the same 404 as a missing entity.
245
- throw Boom.notFound(`Entity ${entityId} not found`, {
246
- code: 'ENTITY_NOT_FOUND',
247
- });
248
- }
249
- if (!grantsUse(tenancy.access)) {
250
- throw new ProxyRequestError(
251
- 'PROXY_NOT_ALLOWED',
252
- 'Proxying through an entity needs use access to it; you can only view this one',
253
- { module: definition.moduleName, reason: 'access' }
254
- );
255
- }
256
- // Narrowest by default (§3.5): with no setting only individual
257
- // entities are proxied, because proxying through a shared credential
258
- // lets any member act on the organization's account.
259
- const acceptedScopes = this.proxyConfig.scopes ?? [SCOPES.INDIVIDUAL];
260
- if (!acceptedScopes.includes(tenancy.scope)) {
261
- throw new ProxyRequestError(
262
- 'PROXY_NOT_ALLOWED',
263
- `The entity proxy does not accept ${tenancy.scope} entities in this app`,
264
- { module: definition.moduleName, reason: 'scope', scope: tenancy.scope }
265
- );
266
- }
267
- }
268
-
269
- /**
270
- * The module's bases. Load already validated the definition
271
- * (assertProxyConfig), so this only fails for a definition that skipped
272
- * load; that is a configuration fault (501), never an unmapped 500.
273
- */
274
- basesOf(definition) {
275
- try {
276
- return resolveModuleBases(definition);
277
- } catch (error) {
278
- if (!(error instanceof ProxyConfigError)) throw error;
279
- throw new ProxyRequestError(
280
- 'PROXY_NOT_CONFIGURED',
281
- `Module ${definition.moduleName} has invalid proxy settings`
282
- );
283
- }
284
- }
285
-
286
- /**
287
- * The URL of the named base. A base the module declared never falls back
288
- * to the requester's own host (that would send the module's token to a
289
- * host the module did not name); only a module that declares nothing at
290
- * all uses the requester's base URL, signalled by undefined.
291
- */
292
- resolveBaseUrl(bases, api, name) {
293
- const declared = bases.values.get(name);
294
- if (!bases.declared) return undefined;
295
- const url = typeof declared === 'function' ? declared(api) : declared;
296
- if (typeof url !== 'string' || url === '') {
297
- throw new ProxyRequestError(
298
- 'PROXY_NOT_CONFIGURED',
299
- `The module declares no usable URL for base ${name}`
300
- );
301
- }
302
- return url;
303
- }
304
-
305
- /** One audit record (ADR-048) and one usage count (ADR-011) per call. */
306
- record(audit, outcome, started, reason) {
307
- const fields = {
308
- eventName: 'frigg.api.proxy.request',
309
- ...audit,
310
- outcome,
311
- durationMs: this.now() - started,
312
- ...(reason && { reason }),
313
- };
314
- if (outcome === 'ok' || outcome === 'upstream_error') {
315
- log.info('Entity proxy request', fields);
316
- } else {
317
- log.warn('Entity proxy request refused', fields);
318
- }
319
- try {
320
- (this.telemetry || getTelemetry()).count('frigg.api.proxy.requests', 1, {
321
- module: audit.module || 'unknown',
322
- method: audit.method || 'unknown',
323
- outcome,
324
- });
325
- } catch {
326
- // Telemetry must never break a request.
327
- }
328
- }
329
- }
330
-
331
- module.exports = { ExecuteEntityProxyRequest, ERROR_MAP };
@@ -1,100 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { findDefinition, getAuthStepCount } = require('./entity-types');
3
-
4
- const defaultCreateModule = (params) => {
5
- const { Module } = require('../module');
6
- return Module.create(params);
7
- };
8
-
9
- /**
10
- * Normalises what a module returns for a step into the v2 requirements
11
- * shape: `{ type, data }`. Modules either return `{ type, data }` already
12
- * (multi-step, JSON-schema forms) or a flat object such as the OAuth2
13
- * requester's `{ type: 'oauth2', url }`.
14
- */
15
- function normalizeRequirements(raw) {
16
- if (!raw || typeof raw !== 'object') return { type: 'custom', data: {} };
17
- if (raw.data && typeof raw.data === 'object') {
18
- return { ...raw, type: raw.type || 'custom' };
19
- }
20
- const { type, ...data } = raw;
21
- return { type: type || 'custom', data };
22
- }
23
-
24
- /**
25
- * Single-step requirements, from wherever the module declares them: a
26
- * requiredAuthMethods.getAuthorizationRequirements(api) (the form-based
27
- * API-key convention `frigg auth` uses), or the Api's own
28
- * getAuthorizationRequirements() (OAuth2Requester builds the authorize URL).
29
- */
30
- async function singleStepRequirements(definition, module) {
31
- const declared = definition.requiredAuthMethods?.getAuthorizationRequirements;
32
- if (typeof declared === 'function') return declared(module.api);
33
- if (typeof module.api?.getAuthorizationRequirements === 'function') {
34
- return module.api.getAuthorizationRequirements();
35
- }
36
- throw Boom.notImplemented(
37
- `Module ${definition.moduleName} declares no authorization requirements (add requiredAuthMethods.getAuthorizationRequirements)`,
38
- { code: 'REQUIREMENTS_NOT_DECLARED', expose: true }
39
- );
40
- }
41
-
42
- /**
43
- * What the caller must submit for one authorization step of an entity type
44
- * (Management API v2). Single-step modules use their Api's
45
- * getAuthorizationRequirements(); multi-step modules declare
46
- * getAuthStepCount() and getAuthRequirementsForStep(step) on their
47
- * definition (docs/MULTI_STEP_AUTH_MIGRATION_GUIDE.md).
48
- */
49
- class GetAuthorizationRequirements {
50
- /**
51
- * @param {Object} params
52
- * @param {Array<Object>} params.moduleDefinitions
53
- * @param {Function} [params.createModule]
54
- */
55
- constructor({ moduleDefinitions, createModule }) {
56
- this.moduleDefinitions = moduleDefinitions || [];
57
- this.createModule = createModule || defaultCreateModule;
58
- }
59
-
60
- /**
61
- * @param {Object} params
62
- * @param {string} params.entityType
63
- * @param {number} [params.step=1]
64
- * @param {string} [params.userId]
65
- * @param {string} [params.state] - OAuth state forwarded to the Api.
66
- * @returns {Promise<{type: string, data: Object, step: number, totalSteps: number, isMultiStep: boolean}>}
67
- */
68
- async execute({ entityType, step = 1, userId = null, state } = {}) {
69
- const definition = findDefinition(this.moduleDefinitions, entityType);
70
- const totalSteps = getAuthStepCount(definition);
71
-
72
- if (!Number.isInteger(step) || step < 1 || step > totalSteps) {
73
- throw Boom.badRequest(
74
- `step must be between 1 and ${totalSteps} for ${entityType}`,
75
- { code: 'INVALID_STEP' }
76
- );
77
- }
78
-
79
- let raw;
80
- if (typeof definition.getAuthRequirementsForStep === 'function') {
81
- raw = await definition.getAuthRequirementsForStep(step);
82
- } else {
83
- const module = await this.createModule({
84
- userId,
85
- definition,
86
- ...(state ? { state } : {}),
87
- });
88
- raw = await singleStepRequirements(definition, module);
89
- }
90
-
91
- return {
92
- ...normalizeRequirements(raw),
93
- step,
94
- totalSteps,
95
- isMultiStep: totalSteps > 1,
96
- };
97
- }
98
- }
99
-
100
- module.exports = { GetAuthorizationRequirements, normalizeRequirements };
@@ -1,113 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { describeRecord } = require('../scope-visibility');
3
- const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
4
- const { isEntityNotFound } = require('../repositories/entity-not-found');
5
-
6
- /**
7
- * True when `user` may act on records owned by `ownerId`. Uses the User
8
- * domain object's rule (individual or organisation user model) when present.
9
- * This is the owner-only rule: it ignores the organization a record belongs
10
- * to, so it no longer decides access to an entity or an integration
11
- * (modules/scope-visibility.js does). Credentials still use it.
12
- * @param {Object} user - authenticated User (or a plain { getId } double)
13
- * @param {string|number} ownerId
14
- */
15
- function userOwns(user, ownerId) {
16
- if (ownerId === undefined || ownerId === null || !user) return false;
17
- if (typeof user.ownsUserId === 'function') return user.ownsUserId(ownerId);
18
- return String(user.getId()) === String(ownerId);
19
- }
20
-
21
- const defaultCreateModule = (params) => {
22
- const { Module } = require('../module');
23
- return Module.create(params);
24
- };
25
-
26
- /**
27
- * Loads an entity the caller owns and builds its Module (API client
28
- * hydrated from the persisted entity and credential). Management API v2.
29
- *
30
- * A missing entity and one owned by someone else are the same 404, so ids
31
- * cannot be probed (ADR-052 §3).
32
- */
33
- class GetEntityModuleForUser {
34
- /**
35
- * @param {Object} params
36
- * @param {import('../repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
37
- * @param {Array<Object>} params.moduleDefinitions
38
- * @param {Function} [params.createModule] - builds a Module; injectable for tests.
39
- * @param {Object} [params.scopePolicy] - from createScopePolicy; with none the
40
- * app configures nothing (ADR-056 §3.1).
41
- */
42
- constructor({ moduleRepository, moduleDefinitions, createModule, scopePolicy }) {
43
- this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
44
- this.moduleRepository = moduleRepository;
45
- this.moduleDefinitions = moduleDefinitions || [];
46
- this.createModule = createModule || defaultCreateModule;
47
- }
48
-
49
- /**
50
- * @param {string} entityId
51
- * @param {Object} user - authenticated User
52
- * @returns {Promise<{ entity: Object, definition: Object, module: Object }>}
53
- */
54
- async execute(entityId, user) {
55
- const entity = await this.loadOwnedEntity(entityId, user);
56
- const definition = this.moduleDefinitions.find(
57
- (def) => def.moduleName === entity.moduleName
58
- );
59
- if (!definition) {
60
- throw Boom.notFound(
61
- `Entity type ${entity.moduleName} is not configured in this app`,
62
- { code: 'ENTITY_TYPE_NOT_FOUND' }
63
- );
64
- }
65
- const module = await this.createModule({
66
- userId: entity.userId,
67
- entity,
68
- definition,
69
- });
70
- return { entity, definition, module };
71
- }
72
-
73
- /**
74
- * The entity record, or a 404 when it is missing or the caller may not use
75
- * it: not theirs, or paired with another organization (ADR-056 §3.6). A
76
- * `global` entity is never the tenant's to load.
77
- */
78
- async loadOwnedEntity(entityId, user) {
79
- let entity = null;
80
- try {
81
- entity = await this.moduleRepository.findEntityById(entityId);
82
- } catch (error) {
83
- // Every adapter throws for a missing row, and a malformed id (e.g.
84
- // not an integer on Postgres) is "not found" too. Both must be the
85
- // 404 another tenant's entity gets, or the status says which ids
86
- // exist. Anything else is a real failure.
87
- if (
88
- isEntityNotFound(error) ||
89
- /Invalid ID|ObjectId|Malformed/i.test(error?.message || '')
90
- ) {
91
- entity = null;
92
- } else {
93
- throw error;
94
- }
95
- }
96
- if (!entity || !this.describe(entity, user)) {
97
- throw Boom.notFound(`Entity ${entityId} not found`, {
98
- code: 'ENTITY_NOT_FOUND',
99
- });
100
- }
101
- return entity;
102
- }
103
-
104
- /**
105
- * `{ scope, owner, organizationId, access }` of an entity to this caller,
106
- * or null when they may not see it (§3.4).
107
- */
108
- describe(entity, user) {
109
- return describeRecord(entity, this.scopePolicy.accessContext(user), 'entity');
110
- }
111
- }
112
-
113
- module.exports = { GetEntityModuleForUser, userOwns };