@friggframework/core 2.0.0--canary.680.6bacd26.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 (86) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/application/commands/integration-commands.js +8 -18
  3. package/application/index.js +2 -6
  4. package/credential/repositories/credential-repository-documentdb.js +0 -34
  5. package/credential/repositories/credential-repository-interface.js +0 -15
  6. package/credential/repositories/credential-repository-mongo.js +0 -23
  7. package/credential/repositories/credential-repository-postgres.js +0 -28
  8. package/database/encryption/README.md +0 -2
  9. package/database/encryption/encryption-schema-registry.js +12 -41
  10. package/database/encryption/field-encryption-service.js +5 -69
  11. package/generated/prisma-mongodb/edge.js +4 -20
  12. package/generated/prisma-mongodb/index-browser.js +1 -17
  13. package/generated/prisma-mongodb/index.d.ts +95 -1543
  14. package/generated/prisma-mongodb/index.js +4 -20
  15. package/generated/prisma-mongodb/package.json +1 -1
  16. package/generated/prisma-mongodb/schema.prisma +0 -27
  17. package/generated/prisma-mongodb/wasm.js +4 -20
  18. package/generated/prisma-postgresql/edge.js +4 -20
  19. package/generated/prisma-postgresql/index-browser.js +1 -17
  20. package/generated/prisma-postgresql/index.d.ts +84 -1565
  21. package/generated/prisma-postgresql/index.js +4 -20
  22. package/generated/prisma-postgresql/package.json +1 -1
  23. package/generated/prisma-postgresql/schema.prisma +0 -26
  24. package/generated/prisma-postgresql/wasm.js +4 -20
  25. package/handlers/app-definition-loader.js +8 -19
  26. package/handlers/app-handler-helpers.js +3 -25
  27. package/handlers/backend-utils.js +25 -31
  28. package/handlers/middleware/admin-auth.js +1 -14
  29. package/handlers/routers/auth.js +3 -11
  30. package/handlers/routers/health.js +4 -11
  31. package/handlers/workers/integration-defined-workers.js +2 -5
  32. package/index.js +2 -4
  33. package/integrations/index.js +4 -0
  34. package/integrations/integration-router.js +23 -16
  35. package/integrations/utils/map-integration-dto.js +15 -1
  36. package/modules/index.js +0 -6
  37. package/modules/requester/requester.js +2 -27
  38. package/package.json +5 -5
  39. package/prisma-mongodb/schema.prisma +0 -27
  40. package/prisma-postgresql/schema.prisma +0 -26
  41. package/types/index.d.ts +0 -1
  42. package/credential/use-cases/credential-operations-for-user.js +0 -117
  43. package/credential/use-cases/reauthorize-credential-for-user.js +0 -66
  44. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  45. package/integrations/use-cases/integration-operations-for-user.js +0 -225
  46. package/management-api/create-management-api-router.js +0 -49
  47. package/management-api/error-response.js +0 -116
  48. package/management-api/management-api-config.js +0 -82
  49. package/management-api/meta-router.js +0 -57
  50. package/management-api/openapi/build-openapi.js +0 -144
  51. package/management-api/openapi/components.js +0 -368
  52. package/management-api/openapi/v1-operations.js +0 -83
  53. package/management-api/openapi/v2-operations.js +0 -252
  54. package/management-api/route-registry.js +0 -294
  55. package/management-api/test-support/http-client.js +0 -39
  56. package/management-api/use-cases/get-management-api-meta.js +0 -57
  57. package/management-api/v1-deprecation.js +0 -120
  58. package/management-api/v2/create-v2-router.js +0 -85
  59. package/management-api/v2/dependencies.js +0 -256
  60. package/management-api/v2/dto.js +0 -123
  61. package/management-api/v2/handlers/authorization.js +0 -93
  62. package/management-api/v2/handlers/credentials.js +0 -29
  63. package/management-api/v2/handlers/entities.js +0 -74
  64. package/management-api/v2/handlers/integrations.js +0 -125
  65. package/management-api/v2/handlers/proxy.js +0 -34
  66. package/management-api/v2/request.js +0 -54
  67. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  68. package/modules/domain/authorization-session.js +0 -144
  69. package/modules/entity-type-registry.js +0 -311
  70. package/modules/proxy/proxy-errors.js +0 -20
  71. package/modules/proxy/proxy-path.js +0 -179
  72. package/modules/proxy/proxy-policy.js +0 -248
  73. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  74. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  75. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  76. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  77. package/modules/requester/proxy-request.js +0 -244
  78. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  79. package/modules/use-cases/entity-operations-for-user.js +0 -125
  80. package/modules/use-cases/entity-types.js +0 -115
  81. package/modules/use-cases/execute-entity-proxy-request.js +0 -239
  82. package/modules/use-cases/get-authorization-requirements.js +0 -100
  83. package/modules/use-cases/get-entity-module-for-user.js +0 -86
  84. package/modules/use-cases/multi-step-authorization.js +0 -302
  85. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  86. package/types/modules/index.d.ts +0 -58
@@ -1,225 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { userOwns } = require('../../modules/use-cases/get-entity-module-for-user');
3
-
4
- /**
5
- * Management API v2 integration operations. They put an ownership check
6
- * (missing or foreign integration = 404) in front of the existing
7
- * integration use cases, and pass the record's owner id on, so the
8
- * organisation user model works the same as for entities.
9
- */
10
-
11
- const notFound = (integrationId) =>
12
- Boom.notFound(`Integration ${integrationId} not found`, {
13
- code: 'INTEGRATION_NOT_FOUND',
14
- });
15
-
16
- /** The integration record, or 404 when it is missing or not the caller's. */
17
- class GetOwnedIntegrationRecord {
18
- constructor({ integrationRepository }) {
19
- this.integrationRepository = integrationRepository;
20
- }
21
-
22
- async execute(integrationId, user) {
23
- let record = null;
24
- try {
25
- record = await this.integrationRepository.findIntegrationById(
26
- integrationId
27
- );
28
- } catch (error) {
29
- if (/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
30
- throw notFound(integrationId);
31
- }
32
- throw error;
33
- }
34
- if (!record || !userOwns(user, record.userId)) {
35
- throw notFound(integrationId);
36
- }
37
- return record;
38
- }
39
- }
40
-
41
- /**
42
- * Creates (or reuses) an integration between entities the caller owns.
43
- * Unlike v1, the entities' ownership and the integration type are checked
44
- * before anything is written.
45
- */
46
- class CreateIntegrationForUser {
47
- constructor({ createIntegration, getEntityModuleForUser, integrationClasses }) {
48
- this.createIntegration = createIntegration;
49
- this.getEntityModuleForUser = getEntityModuleForUser;
50
- this.integrationClasses = integrationClasses || [];
51
- }
52
-
53
- async execute({ entities, config }, user) {
54
- if (!config || typeof config !== 'object' || typeof config.type !== 'string') {
55
- throw Boom.badRequest('config.type is required', {
56
- code: 'VALIDATION_ERROR',
57
- details: { field: 'config.type' },
58
- });
59
- }
60
- const known = this.integrationClasses.some(
61
- (IntegrationClass) => IntegrationClass.Definition?.name === config.type
62
- );
63
- if (!known) {
64
- throw Boom.badRequest(`Unknown integration type '${config.type}'`, {
65
- code: 'UNKNOWN_INTEGRATION_TYPE',
66
- });
67
- }
68
- if (
69
- !Array.isArray(entities) ||
70
- entities.some((id) => typeof id !== 'string' && typeof id !== 'number')
71
- ) {
72
- throw Boom.badRequest('entities must be an array of entity ids', {
73
- code: 'VALIDATION_ERROR',
74
- details: { field: 'entities' },
75
- });
76
- }
77
- for (const entityId of entities) {
78
- await this.getEntityModuleForUser.loadOwnedEntity(String(entityId), user);
79
- }
80
- return this.createIntegration.execute(
81
- entities.map(String),
82
- user.getId(),
83
- config
84
- );
85
- }
86
- }
87
-
88
- /** The hydrated integration instance (modules loaded) for an owned integration. */
89
- class GetIntegrationInstanceForUser {
90
- constructor({ getOwnedIntegrationRecord, getIntegrationInstance }) {
91
- this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
92
- this.getIntegrationInstance = getIntegrationInstance;
93
- }
94
-
95
- async execute(integrationId, user) {
96
- const record = await this.getOwnedIntegrationRecord.execute(
97
- integrationId,
98
- user
99
- );
100
- return this.getIntegrationInstance.execute(record.id, record.userId);
101
- }
102
- }
103
-
104
- /** Replaces an owned integration's config. */
105
- class UpdateIntegrationForUser {
106
- constructor({ getOwnedIntegrationRecord, updateIntegration }) {
107
- this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
108
- this.updateIntegration = updateIntegration;
109
- }
110
-
111
- async execute(integrationId, user, config) {
112
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
113
- throw Boom.badRequest('config must be an object', {
114
- code: 'VALIDATION_ERROR',
115
- details: { field: 'config' },
116
- });
117
- }
118
- const record = await this.getOwnedIntegrationRecord.execute(
119
- integrationId,
120
- user
121
- );
122
- return this.updateIntegration.execute(record.id, record.userId, config);
123
- }
124
- }
125
-
126
- /** Deletes an owned integration (runs its onDelete hooks). */
127
- class RemoveIntegrationForUser {
128
- constructor({ getOwnedIntegrationRecord, deleteIntegrationForUser }) {
129
- this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
130
- this.deleteIntegrationForUser = deleteIntegrationForUser;
131
- }
132
-
133
- async execute(integrationId, user) {
134
- const record = await this.getOwnedIntegrationRecord.execute(
135
- integrationId,
136
- user
137
- );
138
- await this.deleteIntegrationForUser.execute(record.id, record.userId);
139
- }
140
- }
141
-
142
- /**
143
- * Sends one of the framework's user-facing events (GET_CONFIG_OPTIONS,
144
- * GET_USER_ACTIONS, ...) to an owned integration. Handlers pass fixed event
145
- * names only; caller-chosen action ids go through RunIntegrationActionForUser.
146
- */
147
- class SendIntegrationEventForUser {
148
- constructor({ getIntegrationInstanceForUser }) {
149
- this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
150
- }
151
-
152
- async execute(integrationId, user, event, payload) {
153
- const instance = await this.getIntegrationInstanceForUser.execute(
154
- integrationId,
155
- user
156
- );
157
- return instance.send(event, payload);
158
- }
159
- }
160
-
161
- /**
162
- * Runs a user action on an owned integration. Unlike v1, the action id must
163
- * be one the integration lists as a user action (static USER_ACTION events
164
- * or its dynamic user actions), so a caller cannot fire internal events such
165
- * as lifecycle hooks.
166
- */
167
- class RunIntegrationActionForUser {
168
- constructor({ getIntegrationInstanceForUser }) {
169
- this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
170
- }
171
-
172
- async execute(integrationId, user, actionId, payload) {
173
- const instance = await this.getIntegrationInstanceForUser.execute(
174
- integrationId,
175
- user
176
- );
177
- const actions =
178
- typeof instance.loadUserActions === 'function'
179
- ? await instance.loadUserActions()
180
- : {};
181
- if (!actions || !Object.prototype.hasOwnProperty.call(actions, actionId)) {
182
- throw Boom.notFound(
183
- `Integration ${integrationId} has no user action '${actionId}'`,
184
- { code: 'ACTION_NOT_FOUND' }
185
- );
186
- }
187
- return instance.send(actionId, payload);
188
- }
189
- }
190
-
191
- /**
192
- * Runs the integration's auth test and reconciles its status. Returns the
193
- * errors recorded during the test.
194
- */
195
- class TestIntegrationAuthForUser {
196
- constructor({ getIntegrationInstanceForUser }) {
197
- this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
198
- }
199
-
200
- /** @returns {Promise<{ ok: boolean, errors: Array<Object> }>} */
201
- async execute(integrationId, user) {
202
- const instance = await this.getIntegrationInstanceForUser.execute(
203
- integrationId,
204
- user
205
- );
206
- const start = Date.now();
207
- const authPassed = await instance.testAuth();
208
- await instance.reconcileAuthStatus(authPassed);
209
- const errors = (instance.record?.messages?.errors || []).filter(
210
- ({ timestamp }) => timestamp >= start
211
- );
212
- return { ok: Boolean(authPassed) && errors.length === 0, errors };
213
- }
214
- }
215
-
216
- module.exports = {
217
- GetOwnedIntegrationRecord,
218
- CreateIntegrationForUser,
219
- GetIntegrationInstanceForUser,
220
- UpdateIntegrationForUser,
221
- RemoveIntegrationForUser,
222
- SendIntegrationEventForUser,
223
- RunIntegrationActionForUser,
224
- TestIntegrationAuthForUser,
225
- };
@@ -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,82 +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
- * modules: { // per-module allow-list override
13
- * hubspot: { allow: 'module' },
14
- * erp: { allow: [{ method: 'GET', path: '/v1/customers/**' }] },
15
- * },
16
- * },
17
- * }
18
- * ```
19
- */
20
-
21
- // ADR-052 §7. API Gateway HTTP APIs time out at 30s and Lambda responses cap
22
- // at 6MB, so the timeout and response defaults are also ceilings: an app may
23
- // lower them, not raise them.
24
- const DEFAULT_PROXY_LIMITS = Object.freeze({
25
- timeoutMs: 25_000,
26
- maxRequestBytes: 1024 * 1024,
27
- maxResponseBytes: 5 * 1024 * 1024,
28
- });
29
-
30
- // The request-body limit has no platform ceiling below API Gateway's 10MB, so
31
- // an app may raise it up to that.
32
- const MAX_REQUEST_BYTES_CEILING = 10 * 1024 * 1024;
33
-
34
- const isPositiveNumber = (value) =>
35
- typeof value === 'number' && Number.isFinite(value) && value > 0;
36
-
37
- /** The value, capped at `ceiling`; the ceiling when the value is unusable. */
38
- function positiveCapped(value, ceiling) {
39
- return isPositiveNumber(value) ? Math.min(value, ceiling) : ceiling;
40
- }
41
-
42
- /** The value, capped at `ceiling`; `fallback` when the value is unusable. */
43
- function positiveOr(value, fallback, ceiling) {
44
- return isPositiveNumber(value) ? Math.min(value, ceiling) : fallback;
45
- }
46
-
47
- /**
48
- * @param {Object} [managementApi] - `appDefinition.managementApi`
49
- * @returns {{ v1: boolean, proxy: { enable: boolean, timeoutMs: number, maxRequestBytes: number, maxResponseBytes: number, modules: Object } }}
50
- */
51
- function resolveManagementApiConfig(managementApi) {
52
- const config =
53
- managementApi && typeof managementApi === 'object' ? managementApi : {};
54
- const proxy =
55
- config.proxy && typeof config.proxy === 'object' ? config.proxy : {};
56
-
57
- return {
58
- v1: config.v1 !== false,
59
- proxy: {
60
- enable: proxy.enable === true,
61
- timeoutMs: positiveCapped(
62
- proxy.timeoutMs,
63
- DEFAULT_PROXY_LIMITS.timeoutMs
64
- ),
65
- maxRequestBytes: positiveOr(
66
- proxy.maxRequestBytes,
67
- DEFAULT_PROXY_LIMITS.maxRequestBytes,
68
- MAX_REQUEST_BYTES_CEILING
69
- ),
70
- maxResponseBytes: positiveCapped(
71
- proxy.maxResponseBytes,
72
- DEFAULT_PROXY_LIMITS.maxResponseBytes
73
- ),
74
- modules:
75
- proxy.modules && typeof proxy.modules === 'object'
76
- ? proxy.modules
77
- : {},
78
- },
79
- };
80
- }
81
-
82
- module.exports = { resolveManagementApiConfig, DEFAULT_PROXY_LIMITS };
@@ -1,57 +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 { MANAGEMENT_API_VERSIONS } = require('./route-registry');
10
- const { buildOpenApiDocument } = require('./openapi/build-openapi');
11
-
12
- /**
13
- * Version-neutral discovery routes (ADR-053 §4, §5), mounted on the DB-free
14
- * health Lambda. No authentication: the admin API key only adds the core
15
- * version to the response.
16
- *
17
- * @param {Object} params
18
- * @param {Object} params.appDefinition - the loaded app definition
19
- * (`managementApi` is read).
20
- */
21
- function createManagementApiMetaRouter({ appDefinition = {} } = {}) {
22
- const config = resolveManagementApiConfig(appDefinition.managementApi);
23
- const getMeta = new GetManagementApiMeta({ config, coreVersion });
24
-
25
- const router = Router();
26
-
27
- router.get('/api/meta', (req, res) => {
28
- const isAdmin = isValidAdminApiKey(req.get('x-frigg-admin-api-key'));
29
- res.set('Cache-Control', 'no-store');
30
- res.json(getMeta.execute({ isAdmin }));
31
- });
32
-
33
- // One OpenAPI document per major (ADR-053 §5). Static per deployment, so
34
- // built once; the entity proxy appears only when the app enables it.
35
- const documents = new Map();
36
- for (const major of Object.keys(MANAGEMENT_API_VERSIONS)) {
37
- router.get(`/api/meta/openapi/v${major}.json`, (_req, res) => {
38
- if (!documents.has(major)) {
39
- documents.set(
40
- major,
41
- buildOpenApiDocument(major, { proxy: config.proxy.enable })
42
- );
43
- }
44
- res.set('Cache-Control', 'public, max-age=300');
45
- res.json(documents.get(major));
46
- });
47
- }
48
-
49
- // Anything else under /api/meta is a v2-shaped 404, not Express's page.
50
- router.use('/api/meta', (req, _res, next) => {
51
- next(Boom.notFound(`No Management API meta route for ${req.method} ${req.originalUrl.split('?')[0]}`));
52
- });
53
-
54
- return router;
55
- }
56
-
57
- module.exports = { createManagementApiMetaRouter };
@@ -1,144 +0,0 @@
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 };