@friggframework/core 2.0.0--canary.686.3e27a92.0 → 2.0.0--canary.663.8de669c.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 (184) 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 +1 -6
  15. package/database/encryption/encryption-config.js +31 -10
  16. package/database/encryption/encryption-schema-registry.js +12 -93
  17. package/database/prisma.js +2 -4
  18. package/generated/prisma-mongodb/edge.js +4 -43
  19. package/generated/prisma-mongodb/index-browser.js +1 -40
  20. package/generated/prisma-mongodb/index.d.ts +111 -4739
  21. package/generated/prisma-mongodb/index.js +4 -43
  22. package/generated/prisma-mongodb/package.json +1 -1
  23. package/generated/prisma-mongodb/schema.prisma +0 -92
  24. package/generated/prisma-mongodb/wasm.js +4 -43
  25. package/generated/prisma-postgresql/edge.js +4 -43
  26. package/generated/prisma-postgresql/index-browser.js +1 -40
  27. package/generated/prisma-postgresql/index.d.ts +33 -4966
  28. package/generated/prisma-postgresql/index.js +4 -43
  29. package/generated/prisma-postgresql/package.json +1 -1
  30. package/generated/prisma-postgresql/schema.prisma +0 -87
  31. package/generated/prisma-postgresql/wasm.js +4 -43
  32. package/handlers/app-definition-loader.js +19 -121
  33. package/handlers/app-handler-helpers.js +3 -25
  34. package/handlers/backend-utils.js +25 -31
  35. package/handlers/middleware/admin-auth.js +1 -14
  36. package/handlers/routers/auth.js +10 -46
  37. package/handlers/routers/health.js +4 -13
  38. package/handlers/routers/integration-defined-routers.js +92 -144
  39. package/handlers/routers/integration-webhook-routers.js +64 -102
  40. package/handlers/workers/integration-defined-workers.js +2 -5
  41. package/index.js +1 -65
  42. package/infrastructure/scheduler/index.js +6 -19
  43. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  44. package/integrations/index.js +0 -1
  45. package/integrations/integration-router.js +96 -131
  46. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  47. package/integrations/repositories/integration-repository-interface.js +1 -17
  48. package/integrations/repositories/integration-repository-mongo.js +76 -59
  49. package/integrations/repositories/integration-repository-postgres.js +76 -62
  50. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  51. package/integrations/use-cases/create-integration.js +20 -55
  52. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  53. package/integrations/use-cases/index.js +1 -7
  54. package/integrations/utils/map-integration-dto.js +14 -43
  55. package/modules/index.js +0 -14
  56. package/modules/module-factory.js +0 -12
  57. package/modules/module.js +2 -53
  58. package/modules/repositories/module-repository-documentdb.js +6 -51
  59. package/modules/repositories/module-repository-interface.js +2 -19
  60. package/modules/repositories/module-repository-mongo.js +74 -67
  61. package/modules/repositories/module-repository-postgres.js +74 -66
  62. package/modules/repositories/module-repository.js +1 -2
  63. package/modules/requester/requester.js +2 -27
  64. package/modules/use-cases/get-entities-for-user.js +13 -26
  65. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  66. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  67. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  68. package/modules/use-cases/get-module.js +4 -10
  69. package/modules/use-cases/process-authorization-callback.js +19 -53
  70. package/modules/use-cases/refresh-entity-options.js +4 -10
  71. package/modules/use-cases/test-module-auth.js +4 -10
  72. package/package.json +5 -6
  73. package/prisma-mongodb/schema.prisma +0 -92
  74. package/prisma-postgresql/schema.prisma +0 -87
  75. package/queues/index.js +0 -25
  76. package/types/index.d.ts +0 -2
  77. package/user/repositories/user-repository-documentdb.js +2 -27
  78. package/user/repositories/user-repository-interface.js +0 -18
  79. package/user/repositories/user-repository-mongo.js +0 -24
  80. package/user/repositories/user-repository-postgres.js +0 -18
  81. package/user/use-cases/authenticate-user.js +1 -7
  82. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  83. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  84. package/user/user.js +1 -64
  85. package/credential/use-cases/credential-operations-for-user.js +0 -202
  86. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  87. package/database/organization-where.js +0 -18
  88. package/database/unique-violation.js +0 -25
  89. package/extensions/bootstrap-runner.js +0 -43
  90. package/extensions/extension-loader.js +0 -187
  91. package/extensions/index.js +0 -41
  92. package/extensions/initialize-app.js +0 -123
  93. package/extensions/route-mounter.js +0 -103
  94. package/extensions/schema-composer.js +0 -188
  95. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  96. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  97. package/integrations/repositories/integration-not-found.js +0 -14
  98. package/integrations/schedule-config.js +0 -80
  99. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  100. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  101. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  102. package/management-api/create-management-api-router.js +0 -49
  103. package/management-api/error-response.js +0 -116
  104. package/management-api/management-api-config.js +0 -151
  105. package/management-api/meta-router.js +0 -76
  106. package/management-api/openapi/build-openapi.js +0 -144
  107. package/management-api/openapi/components.js +0 -408
  108. package/management-api/openapi/v1-operations.js +0 -83
  109. package/management-api/openapi/v2-operations.js +0 -264
  110. package/management-api/route-registry.js +0 -411
  111. package/management-api/test-support/http-client.js +0 -39
  112. package/management-api/use-cases/get-management-api-meta.js +0 -57
  113. package/management-api/v1-deprecation.js +0 -120
  114. package/management-api/v2/create-v2-router.js +0 -85
  115. package/management-api/v2/dependencies.js +0 -281
  116. package/management-api/v2/dto.js +0 -143
  117. package/management-api/v2/handlers/authorization.js +0 -94
  118. package/management-api/v2/handlers/credentials.js +0 -33
  119. package/management-api/v2/handlers/entities.js +0 -77
  120. package/management-api/v2/handlers/integrations.js +0 -128
  121. package/management-api/v2/handlers/proxy.js +0 -34
  122. package/management-api/v2/request.js +0 -85
  123. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  124. package/modules/domain/authorization-session.js +0 -144
  125. package/modules/entity-scope.js +0 -15
  126. package/modules/entity-type-registry.js +0 -311
  127. package/modules/proxy/proxy-errors.js +0 -20
  128. package/modules/proxy/proxy-path.js +0 -179
  129. package/modules/proxy/proxy-policy.js +0 -248
  130. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  131. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  132. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  133. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  134. package/modules/repositories/entity-not-found.js +0 -18
  135. package/modules/requester/proxy-request.js +0 -244
  136. package/modules/scope-config.js +0 -192
  137. package/modules/scope-policy.js +0 -112
  138. package/modules/scope-resolution.js +0 -288
  139. package/modules/scope-visibility.js +0 -247
  140. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  141. package/modules/tests/doubles/ownership-matching.js +0 -18
  142. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  143. package/modules/use-cases/create-entity-from-credential.js +0 -113
  144. package/modules/use-cases/entity-operations-for-user.js +0 -162
  145. package/modules/use-cases/entity-types.js +0 -139
  146. package/modules/use-cases/execute-entity-proxy-request.js +0 -286
  147. package/modules/use-cases/get-authorization-requirements.js +0 -100
  148. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  149. package/modules/use-cases/manage-access.js +0 -21
  150. package/modules/use-cases/multi-step-authorization.js +0 -329
  151. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  152. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  153. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  154. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  155. package/providers/index.js +0 -23
  156. package/providers/resolve-provider.js +0 -93
  157. package/providers/runtime-detection.js +0 -124
  158. package/queues/providers/index.js +0 -11
  159. package/queues/providers/netlify-background-provider.js +0 -150
  160. package/queues/providers/qstash-queue-provider.js +0 -219
  161. package/queues/providers/sqs-queue-provider.js +0 -70
  162. package/queues/queue-client-interface.js +0 -55
  163. package/queues/queue-message-auth.js +0 -193
  164. package/queues/queue-provider-factory.js +0 -101
  165. package/queues/queue-provider.js +0 -65
  166. package/queues/sqs-queue-client.js +0 -81
  167. package/types/modules/index.d.ts +0 -197
  168. package/types/user/index.d.ts +0 -134
  169. package/user/adopter-jwt-config.js +0 -177
  170. package/user/adopter-jwt-errors.js +0 -59
  171. package/user/adopter-token-verifier.js +0 -122
  172. package/user/app-user-helpers.js +0 -97
  173. package/user/create-user-authentication.js +0 -64
  174. package/user/membership.js +0 -51
  175. package/user/repositories/organization-membership-record.js +0 -19
  176. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  177. package/user/repositories/organization-membership-repository-factory.js +0 -42
  178. package/user/repositories/organization-membership-repository-interface.js +0 -57
  179. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  180. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  181. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  182. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  183. package/user/use-cases/ensure-organization-membership.js +0 -92
  184. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,177 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { SCOPES, resolveScope } = require('../../modules/scope-resolution');
3
- const {
4
- integrationOwnerKind,
5
- resolveIntegrationOwner,
6
- } = require('../../modules/scope-resolution');
7
- const {
8
- describeRecord,
9
- unscopedRowsBelongHere,
10
- } = require('../../modules/scope-visibility');
11
- const {
12
- bindingScopeSetting,
13
- readIntegrationOwnership,
14
- } = require('../../modules/scope-config');
15
- const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
16
- const { entityTypeNameOf } = require('../../modules/entity-type-registry');
17
-
18
- const notFound = (entityId) =>
19
- Boom.notFound(`Entity ${entityId} not found`, { code: 'ENTITY_NOT_FOUND' });
20
-
21
- /**
22
- * Who owns an integration about to be created, and in which organization
23
- * (ADR-056 §3, §3.3, §3.6). Shared by the v1 and v2 create paths. Nothing is
24
- * persisted before this has answered, so a refusal leaves no row behind.
25
- *
26
- * Every entity must be one the caller may use in the active organization. A
27
- * missing one, another organization's and a `global` one are the same 404, so
28
- * ids cannot be probed. When the integration declares a scope (a binding's
29
- * `scope` or `Definition.entities[key].scope`, or `Definition.ownership.scope`)
30
- * the entity's scope must be in it. Levels below the integration (entity type,
31
- * app, default) decide where a connection is CREATED and do not restrict what
32
- * an integration may bind, so an app that adds a registry scope later does not
33
- * break integrations that already use older connections.
34
- *
35
- * The owner follows the entities (all organization: the organization;
36
- * otherwise the person) unless `Definition.ownership.owner` says. Every entity
37
- * must then be of the owner's own scope: an integration is loaded by its owner,
38
- * who must own what it uses, so an integration that mixes an organization
39
- * entity with a person's own, or that names an owner that does not match, is
40
- * refused with a 400 here, before anything is written.
41
- */
42
- class ResolveIntegrationOwnership {
43
- /**
44
- * @param {Object} params
45
- * @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
46
- * @param {Array<Function>} params.integrationClasses
47
- * @param {Object} [params.scopePolicy] - from createScopePolicy
48
- */
49
- constructor({ moduleRepository, integrationClasses, scopePolicy }) {
50
- this.moduleRepository = moduleRepository;
51
- this.integrationClasses = integrationClasses || [];
52
- this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
53
- }
54
-
55
- /**
56
- * @param {Object} params
57
- * @param {Array<string|number>} params.entityIds
58
- * @param {Object} params.config - `{ type }`
59
- * @param {Object} params.user - authenticated User
60
- * @returns {Promise<{ownerId: string, organizationId: (string|null), ownerKind: string, adoptUnscoped: boolean}>}
61
- */
62
- async execute({ entityIds, config, user }) {
63
- const ctx = this.scopePolicy.accessContext(user);
64
- const integrationClass = this.integrationClasses.find(
65
- (IntegrationClass) => IntegrationClass.Definition?.name === config?.type
66
- );
67
- const ownership = readIntegrationOwnership(integrationClass);
68
-
69
- const entities = await this.loadEntities(entityIds);
70
- const scoped = entities.map((entity) => ({
71
- entity,
72
- scope: this.acceptedScope({ entity, ctx, integrationClass, ownership }),
73
- }));
74
-
75
- const ownerKind = integrationOwnerKind(
76
- scoped.map(({ scope }) => scope),
77
- ownership.owner
78
- );
79
- this.assertOneScope(scoped, ownerKind, config?.type);
80
- return {
81
- ...resolveIntegrationOwner(ownerKind, ctx),
82
- ownerKind,
83
- // Whether an existing integration with no organization (written
84
- // before scoping) is this caller's to reuse. An organization-owned
85
- // one is unambiguous: its owner is the organization, whoever asks
86
- // and whatever their own legacy pointer says. A person's is theirs
87
- // only where scope-visibility says (their legacy organization).
88
- adoptUnscoped:
89
- ownerKind === SCOPES.ORGANIZATION ||
90
- unscopedRowsBelongHere(ctx, 'integration'),
91
- };
92
- }
93
-
94
- /** Every entity must be of the integration owner's own scope (see the class comment). */
95
- assertOneScope(scoped, ownerKind, type) {
96
- for (const { entity, scope } of scoped) {
97
- if (scope === ownerKind) continue;
98
- // The load-time check (resolveOwnershipConfig) only sees declared
99
- // scopes; this one sees the entities actually named.
100
- if (ownerKind === SCOPES.ORGANIZATION) {
101
- throw Boom.badRequest(
102
- "An organization-owned integration cannot use an 'individual' entity: a shared integration must not run on one person's credential",
103
- {
104
- code: 'ORG_INTEGRATION_INDIVIDUAL_ENTITY',
105
- details: { entityId: String(entity.id) },
106
- }
107
- );
108
- }
109
- throw Boom.badRequest(
110
- `Integration '${type}' would be owned by the person but uses the organization entity ${entity.id}. An integration must use entities of one scope: all of the person's, or all of the organization's.`,
111
- {
112
- code: 'MIXED_ENTITY_SCOPES',
113
- details: { entityId: String(entity.id), scope },
114
- }
115
- );
116
- }
117
- }
118
-
119
- async loadEntities(entityIds) {
120
- const ids = (entityIds || []).map(String);
121
- if (ids.length === 0) return [];
122
- let found;
123
- try {
124
- found = await this.moduleRepository.findEntitiesByIds(ids);
125
- } catch (error) {
126
- // A malformed id (e.g. not an integer on PostgreSQL) is "not found".
127
- if (/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
128
- throw notFound(ids[0]);
129
- }
130
- throw error;
131
- }
132
- const byId = new Map((found || []).filter(Boolean).map((e) => [String(e.id), e]));
133
- return ids.map((id) => {
134
- if (!byId.has(id)) throw notFound(id);
135
- return byId.get(id);
136
- });
137
- }
138
-
139
- /** The entity's scope for this caller, or a refusal. */
140
- acceptedScope({ entity, ctx, integrationClass, ownership }) {
141
- const described = describeRecord(entity, ctx, 'entity');
142
- if (!described) throw notFound(entity.id);
143
- const { scope } = described;
144
-
145
- const declared = this.declaredScope({ entity, integrationClass, ownership });
146
- if (declared && !declared.allow.includes(scope)) {
147
- throw Boom.badRequest(
148
- `Entity ${entity.id} is ${scope}-scoped, but this integration binds ${entity.moduleName} at ${declared.allow.join(' or ')} scope`,
149
- {
150
- code: 'ENTITY_SCOPE_MISMATCH',
151
- details: { entityId: String(entity.id), scope, allowed: declared.allow },
152
- }
153
- );
154
- }
155
- return scope;
156
- }
157
-
158
- /**
159
- * What the integration itself declares for this entity's binding (levels 2
160
- * and 3 only), or null when it declares nothing.
161
- */
162
- declaredScope({ entity, integrationClass, ownership }) {
163
- const Definition = integrationClass?.Definition;
164
- const key = Object.keys(Definition?.modules || {}).find(
165
- (candidate) =>
166
- entityTypeNameOf(Definition.modules[candidate]?.definition) ===
167
- entity.moduleName
168
- );
169
- const resolved = resolveScope({
170
- binding: key === undefined ? undefined : bindingScopeSetting(Definition, key),
171
- integration: ownership.scope,
172
- });
173
- return resolved.decidedBy === 'default' ? null : resolved;
174
- }
175
- }
176
-
177
- module.exports = { ResolveIntegrationOwnership };
@@ -1,49 +0,0 @@
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 };
@@ -1,116 +0,0 @@
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
- };
@@ -1,151 +0,0 @@
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
- };
@@ -1,76 +0,0 @@
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 };