@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.
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +6 -1
- package/database/encryption/README.md +2 -0
- package/database/encryption/encryption-config.js +10 -31
- package/database/encryption/encryption-schema-registry.js +121 -24
- package/database/encryption/field-encryption-service.js +69 -5
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- package/database/organization-where.js +18 -0
- package/database/prisma.js +4 -2
- package/database/unique-violation.js +25 -0
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +187 -0
- package/extensions/index.js +41 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +43 -4
- package/generated/prisma-mongodb/index-browser.js +40 -1
- package/generated/prisma-mongodb/index.d.ts +4761 -133
- package/generated/prisma-mongodb/index.js +43 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +92 -0
- package/generated/prisma-mongodb/wasm.js +43 -4
- package/generated/prisma-postgresql/edge.js +43 -4
- package/generated/prisma-postgresql/index-browser.js +40 -1
- package/generated/prisma-postgresql/index.d.ts +4961 -28
- package/generated/prisma-postgresql/index.js +43 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +87 -0
- package/generated/prisma-postgresql/wasm.js +43 -4
- package/handlers/app-definition-loader.js +121 -19
- package/handlers/app-handler-helpers.js +25 -3
- package/handlers/backend-utils.js +31 -25
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +13 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +65 -1
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +1 -0
- package/integrations/integration-router.js +131 -96
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -3
- package/integrations/repositories/integration-repository-interface.js +17 -1
- package/integrations/repositories/integration-repository-mongo.js +59 -76
- package/integrations/repositories/integration-repository-postgres.js +62 -76
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +7 -1
- package/integrations/use-cases/integration-operations-for-user.js +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +76 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +408 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +411 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-errors.js +20 -0
- package/modules/proxy/proxy-path.js +179 -0
- package/modules/proxy/proxy-policy.js +248 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/requester/proxy-request.js +244 -0
- package/modules/requester/requester.js +27 -2
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +286 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +329 -0
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +92 -0
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +87 -0
- package/providers/index.js +23 -0
- package/providers/resolve-provider.js +93 -0
- package/providers/runtime-detection.js +124 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +219 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +193 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +2 -0
- package/types/modules/index.d.ts +197 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- 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 };
|