@friggframework/core 2.0.0--canary.663.8de669c.0 → 2.0.0--canary.686.821f8a3.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 (188) 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-utils.js +6 -1
  18. package/database/encryption/README.md +2 -0
  19. package/database/encryption/encryption-config.js +10 -31
  20. package/database/encryption/encryption-schema-registry.js +121 -24
  21. package/database/encryption/field-encryption-service.js +69 -5
  22. package/database/encryption/test-support/fake-ciphertext.js +15 -0
  23. package/database/organization-where.js +18 -0
  24. package/database/prisma.js +4 -2
  25. package/database/unique-violation.js +25 -0
  26. package/extensions/bootstrap-runner.js +43 -0
  27. package/extensions/extension-loader.js +187 -0
  28. package/extensions/index.js +41 -0
  29. package/extensions/initialize-app.js +123 -0
  30. package/extensions/route-mounter.js +103 -0
  31. package/extensions/schema-composer.js +188 -0
  32. package/generated/prisma-mongodb/edge.js +43 -4
  33. package/generated/prisma-mongodb/index-browser.js +40 -1
  34. package/generated/prisma-mongodb/index.d.ts +4761 -133
  35. package/generated/prisma-mongodb/index.js +43 -4
  36. package/generated/prisma-mongodb/package.json +1 -1
  37. package/generated/prisma-mongodb/schema.prisma +92 -0
  38. package/generated/prisma-mongodb/wasm.js +43 -4
  39. package/generated/prisma-postgresql/edge.js +43 -4
  40. package/generated/prisma-postgresql/index-browser.js +40 -1
  41. package/generated/prisma-postgresql/index.d.ts +4961 -28
  42. package/generated/prisma-postgresql/index.js +43 -4
  43. package/generated/prisma-postgresql/package.json +1 -1
  44. package/generated/prisma-postgresql/schema.prisma +87 -0
  45. package/generated/prisma-postgresql/wasm.js +43 -4
  46. package/handlers/app-definition-loader.js +121 -19
  47. package/handlers/app-handler-helpers.js +25 -3
  48. package/handlers/backend-utils.js +31 -25
  49. package/handlers/middleware/admin-auth.js +14 -1
  50. package/handlers/routers/auth.js +46 -10
  51. package/handlers/routers/health.js +13 -4
  52. package/handlers/routers/integration-defined-routers.js +144 -92
  53. package/handlers/routers/integration-webhook-routers.js +102 -64
  54. package/handlers/workers/integration-defined-workers.js +5 -2
  55. package/index.js +65 -1
  56. package/infrastructure/scheduler/index.js +19 -6
  57. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  58. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  59. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  60. package/integrations/index.js +1 -0
  61. package/integrations/integration-router.js +131 -96
  62. package/integrations/repositories/integration-not-found.js +14 -0
  63. package/integrations/repositories/integration-repository-documentdb.js +35 -3
  64. package/integrations/repositories/integration-repository-interface.js +17 -1
  65. package/integrations/repositories/integration-repository-mongo.js +59 -76
  66. package/integrations/repositories/integration-repository-postgres.js +62 -76
  67. package/integrations/schedule-config.js +80 -0
  68. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  69. package/integrations/use-cases/create-integration.js +55 -20
  70. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  71. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  72. package/integrations/use-cases/index.js +7 -1
  73. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  74. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  75. package/integrations/utils/map-integration-dto.js +43 -14
  76. package/management-api/create-management-api-router.js +49 -0
  77. package/management-api/error-response.js +116 -0
  78. package/management-api/management-api-config.js +151 -0
  79. package/management-api/meta-router.js +76 -0
  80. package/management-api/openapi/build-openapi.js +144 -0
  81. package/management-api/openapi/components.js +408 -0
  82. package/management-api/openapi/v1-operations.js +83 -0
  83. package/management-api/openapi/v2-operations.js +264 -0
  84. package/management-api/route-registry.js +411 -0
  85. package/management-api/test-support/http-client.js +39 -0
  86. package/management-api/use-cases/get-management-api-meta.js +57 -0
  87. package/management-api/v1-deprecation.js +120 -0
  88. package/management-api/v2/create-v2-router.js +85 -0
  89. package/management-api/v2/dependencies.js +281 -0
  90. package/management-api/v2/dto.js +143 -0
  91. package/management-api/v2/handlers/authorization.js +94 -0
  92. package/management-api/v2/handlers/credentials.js +33 -0
  93. package/management-api/v2/handlers/entities.js +77 -0
  94. package/management-api/v2/handlers/integrations.js +128 -0
  95. package/management-api/v2/handlers/proxy.js +34 -0
  96. package/management-api/v2/request.js +85 -0
  97. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  98. package/modules/domain/authorization-session.js +144 -0
  99. package/modules/entity-scope.js +15 -0
  100. package/modules/entity-type-registry.js +311 -0
  101. package/modules/index.js +14 -0
  102. package/modules/module-factory.js +12 -0
  103. package/modules/module.js +53 -2
  104. package/modules/proxy/proxy-errors.js +20 -0
  105. package/modules/proxy/proxy-path.js +179 -0
  106. package/modules/proxy/proxy-policy.js +248 -0
  107. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  108. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  109. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  110. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  111. package/modules/repositories/entity-not-found.js +18 -0
  112. package/modules/repositories/module-repository-documentdb.js +51 -6
  113. package/modules/repositories/module-repository-interface.js +19 -2
  114. package/modules/repositories/module-repository-mongo.js +67 -74
  115. package/modules/repositories/module-repository-postgres.js +66 -74
  116. package/modules/repositories/module-repository.js +2 -1
  117. package/modules/requester/proxy-request.js +244 -0
  118. package/modules/requester/requester.js +27 -2
  119. package/modules/scope-config.js +192 -0
  120. package/modules/scope-policy.js +112 -0
  121. package/modules/scope-resolution.js +288 -0
  122. package/modules/scope-visibility.js +247 -0
  123. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  124. package/modules/tests/doubles/ownership-matching.js +18 -0
  125. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  126. package/modules/use-cases/create-entity-from-credential.js +113 -0
  127. package/modules/use-cases/entity-operations-for-user.js +162 -0
  128. package/modules/use-cases/entity-types.js +139 -0
  129. package/modules/use-cases/execute-entity-proxy-request.js +286 -0
  130. package/modules/use-cases/get-authorization-requirements.js +100 -0
  131. package/modules/use-cases/get-entities-for-user.js +26 -13
  132. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  133. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  134. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  135. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  136. package/modules/use-cases/get-module.js +10 -4
  137. package/modules/use-cases/manage-access.js +21 -0
  138. package/modules/use-cases/multi-step-authorization.js +329 -0
  139. package/modules/use-cases/process-authorization-callback.js +53 -19
  140. package/modules/use-cases/refresh-entity-options.js +10 -4
  141. package/modules/use-cases/test-module-auth.js +10 -4
  142. package/package.json +6 -5
  143. package/prisma-mongodb/schema.prisma +92 -0
  144. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  145. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  146. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  147. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  148. package/prisma-postgresql/schema.prisma +87 -0
  149. package/providers/index.js +23 -0
  150. package/providers/resolve-provider.js +93 -0
  151. package/providers/runtime-detection.js +124 -0
  152. package/queues/index.js +25 -0
  153. package/queues/providers/index.js +11 -0
  154. package/queues/providers/netlify-background-provider.js +150 -0
  155. package/queues/providers/qstash-queue-provider.js +219 -0
  156. package/queues/providers/sqs-queue-provider.js +70 -0
  157. package/queues/queue-client-interface.js +55 -0
  158. package/queues/queue-message-auth.js +193 -0
  159. package/queues/queue-provider-factory.js +101 -0
  160. package/queues/queue-provider.js +65 -0
  161. package/queues/sqs-queue-client.js +81 -0
  162. package/types/index.d.ts +2 -0
  163. package/types/modules/index.d.ts +197 -0
  164. package/types/user/index.d.ts +134 -0
  165. package/user/adopter-jwt-config.js +177 -0
  166. package/user/adopter-jwt-errors.js +59 -0
  167. package/user/adopter-token-verifier.js +122 -0
  168. package/user/app-user-helpers.js +97 -0
  169. package/user/create-user-authentication.js +64 -0
  170. package/user/membership.js +51 -0
  171. package/user/repositories/organization-membership-record.js +19 -0
  172. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  173. package/user/repositories/organization-membership-repository-factory.js +42 -0
  174. package/user/repositories/organization-membership-repository-interface.js +57 -0
  175. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  176. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  177. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  178. package/user/repositories/user-repository-documentdb.js +27 -2
  179. package/user/repositories/user-repository-interface.js +18 -0
  180. package/user/repositories/user-repository-mongo.js +24 -0
  181. package/user/repositories/user-repository-postgres.js +18 -0
  182. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  183. package/user/use-cases/authenticate-user.js +7 -1
  184. package/user/use-cases/ensure-organization-membership.js +92 -0
  185. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  186. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  187. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  188. package/user/user.js +64 -1
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Resolves `appDefinition.managementApi` (ADR-052, ADR-053).
3
+ *
4
+ * ```js
5
+ * managementApi: {
6
+ * v1: false, // drop the deprecated v1 routes (default true)
7
+ * proxy: {
8
+ * enable: true, // entity proxy, off by default (ADR-052)
9
+ * timeoutMs: 25000, // whole upstream call, lower only
10
+ * maxRequestBytes: 1048576,
11
+ * maxResponseBytes: 5242880, // lower only
12
+ * scopes: ['individual'], // entity scopes the proxy accepts (ADR-056 §3.5)
13
+ * modules: { // per-module allow-list override
14
+ * hubspot: { allow: 'module' },
15
+ * erp: { allow: [{ method: 'GET', path: '/v1/customers/**' }] },
16
+ * },
17
+ * },
18
+ * }
19
+ * ```
20
+ *
21
+ * Limits that are out of range fall back to their defaults. `proxy.scopes` is a
22
+ * security setting, so it does not fall back: a value that is not a list of
23
+ * known tenant scopes throws ManagementApiConfigError, and loadAppDefinition
24
+ * calls this so the app stops at load.
25
+ */
26
+
27
+ const { BaseError } = require('../errors/base-error');
28
+ const { SCOPES } = require('../modules/scope-resolution');
29
+
30
+ /** Raised for an invalid `appDefinition.managementApi` setting. */
31
+ class ManagementApiConfigError extends BaseError {
32
+ constructor(message, options) {
33
+ super(message, options);
34
+ this.code = 'MANAGEMENT_API_CONFIG_INVALID';
35
+ }
36
+ }
37
+
38
+ /**
39
+ * The scopes `managementApi.proxy.scopes` may list. `global` is not one of
40
+ * them: a global entity is the app owner's and is never a tenant's to proxy.
41
+ */
42
+ const PROXY_SCOPE_VALUES = Object.freeze([SCOPES.INDIVIDUAL, SCOPES.ORGANIZATION]);
43
+
44
+ /**
45
+ * Narrowest by default (ADR-056 §3.5): proxying through a shared credential
46
+ * lets any member act on the organization's account, so an app opts in to
47
+ * `organization` explicitly.
48
+ */
49
+ const DEFAULT_PROXY_SCOPES = Object.freeze([SCOPES.INDIVIDUAL]);
50
+
51
+ // ADR-052 §7. API Gateway HTTP APIs time out at 30s and Lambda responses cap
52
+ // at 6MB, so the timeout and response defaults are also ceilings: an app may
53
+ // lower them, not raise them.
54
+ const DEFAULT_PROXY_LIMITS = Object.freeze({
55
+ timeoutMs: 25_000,
56
+ maxRequestBytes: 1024 * 1024,
57
+ maxResponseBytes: 5 * 1024 * 1024,
58
+ });
59
+
60
+ // The request-body limit has no platform ceiling below API Gateway's 10MB, so
61
+ // an app may raise it up to that.
62
+ const MAX_REQUEST_BYTES_CEILING = 10 * 1024 * 1024;
63
+
64
+ const isPositiveNumber = (value) =>
65
+ typeof value === 'number' && Number.isFinite(value) && value > 0;
66
+
67
+ /** The value, capped at `ceiling`; the ceiling when the value is unusable. */
68
+ function positiveCapped(value, ceiling) {
69
+ return isPositiveNumber(value) ? Math.min(value, ceiling) : ceiling;
70
+ }
71
+
72
+ /** The value, capped at `ceiling`; `fallback` when the value is unusable. */
73
+ function positiveOr(value, fallback, ceiling) {
74
+ return isPositiveNumber(value) ? Math.min(value, ceiling) : fallback;
75
+ }
76
+
77
+ const proxyScopesError = (message) =>
78
+ new ManagementApiConfigError(`managementApi.proxy.scopes: ${message}`);
79
+
80
+ /**
81
+ * `managementApi.proxy.scopes`: absent means the default; anything else must be
82
+ * a non-empty list of PROXY_SCOPE_VALUES, each once.
83
+ * @returns {ReadonlyArray<string>}
84
+ * @throws {ManagementApiConfigError}
85
+ */
86
+ function resolveProxyScopes(scopes) {
87
+ if (scopes === undefined) return DEFAULT_PROXY_SCOPES;
88
+ const allowed = PROXY_SCOPE_VALUES.join(', ');
89
+ if (!Array.isArray(scopes) || scopes.length === 0) {
90
+ throw proxyScopesError(`must be a non-empty list drawn from ${allowed}`);
91
+ }
92
+ for (const scope of scopes) {
93
+ if (scope === SCOPES.GLOBAL) {
94
+ throw proxyScopesError(
95
+ "'global' is not allowed: global entities belong to the app owner and are never proxied for a tenant"
96
+ );
97
+ }
98
+ if (!PROXY_SCOPE_VALUES.includes(scope)) {
99
+ throw proxyScopesError(`unknown scope ${JSON.stringify(scope)}; use ${allowed}`);
100
+ }
101
+ }
102
+ if (new Set(scopes).size !== scopes.length) {
103
+ throw proxyScopesError('lists a scope more than once');
104
+ }
105
+ return Object.freeze([...scopes]);
106
+ }
107
+
108
+ /**
109
+ * @param {Object} [managementApi] - `appDefinition.managementApi`
110
+ * @returns {{ v1: boolean, proxy: { enable: boolean, timeoutMs: number, maxRequestBytes: number, maxResponseBytes: number, scopes: ReadonlyArray<string>, modules: Object } }}
111
+ * @throws {ManagementApiConfigError} for an invalid `proxy.scopes`
112
+ */
113
+ function resolveManagementApiConfig(managementApi) {
114
+ const config =
115
+ managementApi && typeof managementApi === 'object' ? managementApi : {};
116
+ const proxy =
117
+ config.proxy && typeof config.proxy === 'object' ? config.proxy : {};
118
+
119
+ return {
120
+ v1: config.v1 !== false,
121
+ proxy: {
122
+ enable: proxy.enable === true,
123
+ timeoutMs: positiveCapped(
124
+ proxy.timeoutMs,
125
+ DEFAULT_PROXY_LIMITS.timeoutMs
126
+ ),
127
+ maxRequestBytes: positiveOr(
128
+ proxy.maxRequestBytes,
129
+ DEFAULT_PROXY_LIMITS.maxRequestBytes,
130
+ MAX_REQUEST_BYTES_CEILING
131
+ ),
132
+ maxResponseBytes: positiveCapped(
133
+ proxy.maxResponseBytes,
134
+ DEFAULT_PROXY_LIMITS.maxResponseBytes
135
+ ),
136
+ scopes: resolveProxyScopes(proxy.scopes),
137
+ modules:
138
+ proxy.modules && typeof proxy.modules === 'object'
139
+ ? proxy.modules
140
+ : {},
141
+ },
142
+ };
143
+ }
144
+
145
+ module.exports = {
146
+ resolveManagementApiConfig,
147
+ DEFAULT_PROXY_LIMITS,
148
+ DEFAULT_PROXY_SCOPES,
149
+ PROXY_SCOPE_VALUES,
150
+ ManagementApiConfigError,
151
+ };
@@ -0,0 +1,76 @@
1
+ const { Router } = require('express');
2
+ const Boom = require('@hapi/boom');
3
+ const { resolveManagementApiConfig } = require('./management-api-config');
4
+ const {
5
+ GetManagementApiMeta,
6
+ } = require('./use-cases/get-management-api-meta');
7
+ const { isValidAdminApiKey } = require('../handlers/middleware/admin-auth');
8
+ const { version: coreVersion } = require('../package.json');
9
+ const {
10
+ MANAGEMENT_API_VERSIONS,
11
+ PREFERRED_API_VERSION,
12
+ DOCS_ROUTES,
13
+ } = require('./route-registry');
14
+ const { buildOpenApiDocument } = require('./openapi/build-openapi');
15
+
16
+ /**
17
+ * Version-neutral discovery routes (ADR-053 §4, §5), mounted on the DB-free
18
+ * health Lambda. No authentication: the admin API key only adds the core
19
+ * version to the response.
20
+ *
21
+ * @param {Object} params
22
+ * @param {Object} params.appDefinition - the loaded app definition
23
+ * (`managementApi` is read).
24
+ */
25
+ function createManagementApiMetaRouter({ appDefinition = {} } = {}) {
26
+ const config = resolveManagementApiConfig(appDefinition.managementApi);
27
+ const getMeta = new GetManagementApiMeta({ config, coreVersion });
28
+
29
+ const router = Router();
30
+
31
+ router.get('/api/meta', (req, res) => {
32
+ const isAdmin = isValidAdminApiKey(req.get('x-frigg-admin-api-key'));
33
+ res.set('Cache-Control', 'no-store');
34
+ res.json(getMeta.execute({ isAdmin }));
35
+ });
36
+
37
+ // One OpenAPI document per major (ADR-053 §5). Static per deployment, so
38
+ // built once; the entity proxy appears only when the app enables it.
39
+ const documents = new Map();
40
+ for (const major of Object.keys(MANAGEMENT_API_VERSIONS)) {
41
+ router.get(`/api/meta/openapi/v${major}.json`, (_req, res) => {
42
+ if (!documents.has(major)) {
43
+ documents.set(
44
+ major,
45
+ buildOpenApiDocument(major, { proxy: config.proxy.enable })
46
+ );
47
+ }
48
+ res.set('Cache-Control', 'public, max-age=300');
49
+ res.json(documents.get(major));
50
+ });
51
+ }
52
+
53
+ // Anything else under /api/meta is a v2-shaped 404, not Express's page.
54
+ router.use('/api/meta', (req, _res, next) => {
55
+ next(Boom.notFound(`No Management API meta route for ${req.method} ${req.originalUrl.split('?')[0]}`));
56
+ });
57
+
58
+ return router;
59
+ }
60
+
61
+ /**
62
+ * GET /api/docs (DOCS_ROUTES): a redirect to the current OpenAPI document,
63
+ * mounted next to the meta router on the DB-free health Lambda. Public, like
64
+ * /api/meta.
65
+ */
66
+ function createDocsRedirectRouter() {
67
+ const router = Router();
68
+ for (const { path } of DOCS_ROUTES) {
69
+ router.get(path, (_req, res) =>
70
+ res.redirect(302, `/api/meta/openapi/v${PREFERRED_API_VERSION}.json`)
71
+ );
72
+ }
73
+ return router;
74
+ }
75
+
76
+ module.exports = { createManagementApiMetaRouter, createDocsRedirectRouter };
@@ -0,0 +1,144 @@
1
+ const {
2
+ MANAGEMENT_API_VERSIONS,
3
+ V1_ROUTES,
4
+ META_ROUTES,
5
+ V1_MIGRATION_DOCS_URL,
6
+ listV2Routes,
7
+ toGatewayPath,
8
+ } = require('../route-registry');
9
+ const { SCHEMAS, RESPONSES, SECURITY_SCHEMES } = require('./components');
10
+ const { V2_OPERATIONS, META_OPERATIONS } = require('./v2-operations');
11
+ const { V1_OPERATIONS } = require('./v1-operations');
12
+ const { version: coreVersion } = require('../../package.json');
13
+
14
+ const COMPATIBILITY_RULES = `
15
+ Compatibility (ADR-053): within a major, changes are additive only. Clients
16
+ must ignore response fields and enum values they do not know. Operations
17
+ marked \`x-frigg-stability: beta\` may still change in a core minor.
18
+
19
+ Errors: \`{ "error": { "code", "message", "details"? } }\`. Every response
20
+ carries \`Frigg-API-Version: 2\`.`;
21
+
22
+ const DESCRIPTIONS = {
23
+ 1: `The original, unprefixed Management API. Deprecated (ADR-053): every response carries
24
+ \`Deprecation\` and \`Link: <${V1_MIGRATION_DOCS_URL}>; rel="deprecation"\`. It is frozen,
25
+ and \`managementApi.v1: false\` turns it off. Use v2.`,
26
+ 2: `Integrations, entities, credentials, multi-step authorization and the entity proxy, under
27
+ \`/api/v2\`. Discover what a deployment serves with \`GET /api/meta\`.
28
+ ${COMPATIBILITY_RULES}`,
29
+ };
30
+
31
+ const TAGS = [
32
+ { name: 'Integrations', description: 'Integrations between connected entities' },
33
+ { name: 'Entities', description: 'Connected accounts, entity types and the entity proxy' },
34
+ { name: 'Credentials', description: 'Stored credentials (always masked) and re-authorization' },
35
+ { name: 'Authorization', description: 'Single- and multi-step authorization' },
36
+ { name: 'Meta', description: 'Version-neutral discovery' },
37
+ ];
38
+
39
+ /** Path parameters, derived from the Express path. */
40
+ function pathParameters(expressPath) {
41
+ return [...expressPath.matchAll(/:([A-Za-z0-9_]+)/g)].map(([, name]) => ({
42
+ name,
43
+ in: 'path',
44
+ required: true,
45
+ schema: { type: 'string' },
46
+ }));
47
+ }
48
+
49
+ function operationFor(route, docs, extra = {}) {
50
+ if (!docs) {
51
+ throw new Error(
52
+ `No OpenAPI documentation for ${route.method} ${route.path} (${route.operationId || 'v1'})`
53
+ );
54
+ }
55
+ const parameters = [...pathParameters(route.path), ...(docs.parameters || [])];
56
+ return {
57
+ operationId: route.operationId,
58
+ ...(route.tag && { tags: [route.tag] }),
59
+ ...docs,
60
+ ...(parameters.length && { parameters }),
61
+ ...(route.stability && { 'x-frigg-stability': route.stability }),
62
+ ...extra,
63
+ };
64
+ }
65
+
66
+ function addOperation(paths, route, operation) {
67
+ const path = toGatewayPath(route.path);
68
+ paths[path] = paths[path] || {};
69
+ // An ANY route (router.all in v1) is documented as GET and POST, the two
70
+ // methods callers use.
71
+ const methods = route.method === 'ANY' ? ['get', 'post'] : [route.method.toLowerCase()];
72
+ for (const method of methods) {
73
+ paths[path][method] = {
74
+ ...operation,
75
+ ...(methods.length > 1 && {
76
+ operationId: `${operation.operationId}${method === 'get' ? '' : 'Post'}`,
77
+ }),
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Builds the OpenAPI 3 document for one Management API major from the route
84
+ * registry (ADR-053 §5).
85
+ *
86
+ * @param {string} major - '1' or '2'
87
+ * @param {Object} [options]
88
+ * @param {boolean} [options.proxy=true] - include the entity proxy (served
89
+ * documents follow managementApi.proxy.enable)
90
+ * @returns {Object}
91
+ */
92
+ function buildOpenApiDocument(major, { proxy = true } = {}) {
93
+ const info = MANAGEMENT_API_VERSIONS[major];
94
+ if (!info) throw new Error(`Unknown Management API version: ${major}`);
95
+
96
+ const paths = {};
97
+ if (major === '1') {
98
+ for (const route of V1_ROUTES) {
99
+ const key = `${route.method} ${route.path}`;
100
+ addOperation(
101
+ paths,
102
+ route,
103
+ operationFor({ ...route, operationId: V1_OPERATIONS[key]?.operationId }, V1_OPERATIONS[key], {
104
+ deprecated: true,
105
+ })
106
+ );
107
+ }
108
+ } else {
109
+ for (const route of listV2Routes({ proxy })) {
110
+ addOperation(paths, route, operationFor(route, V2_OPERATIONS[route.operationId]));
111
+ }
112
+ }
113
+ for (const route of META_ROUTES) {
114
+ addOperation(
115
+ paths,
116
+ route,
117
+ operationFor({ ...route, tag: 'Meta' }, META_OPERATIONS[route.operationId])
118
+ );
119
+ }
120
+
121
+ return {
122
+ openapi: '3.0.3',
123
+ info: {
124
+ title: `Frigg Management API v${major}`,
125
+ version: coreVersion,
126
+ description: DESCRIPTIONS[major],
127
+ license: { name: 'MIT' },
128
+ },
129
+ 'x-frigg-api-version': major,
130
+ 'x-frigg-api-status': info.status,
131
+ ...(info.deprecatedAt && { 'x-frigg-deprecated-at': info.deprecatedAt }),
132
+ servers: [{ url: '/', description: 'This Frigg deployment' }],
133
+ security: [{ bearerAuth: [] }, { friggApiKey: [] }],
134
+ tags: TAGS,
135
+ paths,
136
+ components: {
137
+ schemas: SCHEMAS,
138
+ responses: RESPONSES,
139
+ securitySchemes: SECURITY_SCHEMES,
140
+ },
141
+ };
142
+ }
143
+
144
+ module.exports = { buildOpenApiDocument };