@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.
- package/CHANGELOG.md +0 -11
- package/application/commands/integration-commands.js +8 -18
- package/application/index.js +2 -6
- package/credential/repositories/credential-repository-documentdb.js +0 -34
- package/credential/repositories/credential-repository-interface.js +0 -15
- package/credential/repositories/credential-repository-mongo.js +0 -23
- package/credential/repositories/credential-repository-postgres.js +0 -28
- package/database/encryption/README.md +0 -2
- package/database/encryption/encryption-schema-registry.js +12 -41
- package/database/encryption/field-encryption-service.js +5 -69
- package/generated/prisma-mongodb/edge.js +4 -20
- package/generated/prisma-mongodb/index-browser.js +1 -17
- package/generated/prisma-mongodb/index.d.ts +95 -1543
- package/generated/prisma-mongodb/index.js +4 -20
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -27
- package/generated/prisma-mongodb/wasm.js +4 -20
- package/generated/prisma-postgresql/edge.js +4 -20
- package/generated/prisma-postgresql/index-browser.js +1 -17
- package/generated/prisma-postgresql/index.d.ts +84 -1565
- package/generated/prisma-postgresql/index.js +4 -20
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -26
- package/generated/prisma-postgresql/wasm.js +4 -20
- package/handlers/app-definition-loader.js +8 -19
- package/handlers/app-handler-helpers.js +3 -25
- package/handlers/backend-utils.js +25 -31
- package/handlers/middleware/admin-auth.js +1 -14
- package/handlers/routers/auth.js +3 -11
- package/handlers/routers/health.js +4 -11
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +2 -4
- package/integrations/index.js +4 -0
- package/integrations/integration-router.js +23 -16
- package/integrations/utils/map-integration-dto.js +15 -1
- package/modules/index.js +0 -6
- package/modules/requester/requester.js +2 -27
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +0 -27
- package/prisma-postgresql/schema.prisma +0 -26
- package/types/index.d.ts +0 -1
- package/credential/use-cases/credential-operations-for-user.js +0 -117
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -66
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
- package/integrations/use-cases/integration-operations-for-user.js +0 -225
- package/management-api/create-management-api-router.js +0 -49
- package/management-api/error-response.js +0 -116
- package/management-api/management-api-config.js +0 -82
- package/management-api/meta-router.js +0 -57
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -368
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -252
- package/management-api/route-registry.js +0 -294
- package/management-api/test-support/http-client.js +0 -39
- package/management-api/use-cases/get-management-api-meta.js +0 -57
- package/management-api/v1-deprecation.js +0 -120
- package/management-api/v2/create-v2-router.js +0 -85
- package/management-api/v2/dependencies.js +0 -256
- package/management-api/v2/dto.js +0 -123
- package/management-api/v2/handlers/authorization.js +0 -93
- package/management-api/v2/handlers/credentials.js +0 -29
- package/management-api/v2/handlers/entities.js +0 -74
- package/management-api/v2/handlers/integrations.js +0 -125
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -54
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-errors.js +0 -20
- package/modules/proxy/proxy-path.js +0 -179
- package/modules/proxy/proxy-policy.js +0 -248
- package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
- package/modules/repositories/authorization-session-repository-factory.js +0 -31
- package/modules/repositories/authorization-session-repository-interface.js +0 -47
- package/modules/repositories/authorization-session-repository-prisma.js +0 -87
- package/modules/requester/proxy-request.js +0 -244
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/use-cases/entity-operations-for-user.js +0 -125
- package/modules/use-cases/entity-types.js +0 -115
- package/modules/use-cases/execute-entity-proxy-request.js +0 -239
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -86
- package/modules/use-cases/multi-step-authorization.js +0 -302
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/types/modules/index.d.ts +0 -58
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
const { ref } = require('./components');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Per-operation documentation for Management API v2, keyed by the route
|
|
5
|
-
* registry's operationId. Paths and methods come from the registry, never
|
|
6
|
-
* from here, so the document cannot list a route the router does not serve.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const json = (schema) => ({ content: { 'application/json': { schema } } });
|
|
10
|
-
const ok = (description, schema) => ({ description, ...(schema && json(schema)) });
|
|
11
|
-
const body = (schema, required = true) => ({ required, ...json(schema) });
|
|
12
|
-
const errors = (...names) =>
|
|
13
|
-
Object.fromEntries(
|
|
14
|
-
names.map((name) => {
|
|
15
|
-
const status = {
|
|
16
|
-
BadRequest: '400',
|
|
17
|
-
Unauthorized: '401',
|
|
18
|
-
Forbidden: '403',
|
|
19
|
-
NotFound: '404',
|
|
20
|
-
Conflict: '409',
|
|
21
|
-
BadGateway: '502',
|
|
22
|
-
}[name];
|
|
23
|
-
return [status, { $ref: `#/components/responses/${name}` }];
|
|
24
|
-
})
|
|
25
|
-
);
|
|
26
|
-
const query = (name, description, schema = { type: 'string' }, required = false) => ({
|
|
27
|
-
name,
|
|
28
|
-
in: 'query',
|
|
29
|
-
required,
|
|
30
|
-
description,
|
|
31
|
-
schema,
|
|
32
|
-
});
|
|
33
|
-
const stepQuery = [
|
|
34
|
-
query('step', 'Step number, 1-based (default 1)', { type: 'integer', minimum: 1 }),
|
|
35
|
-
query('sessionId', 'Session from step 1; required for later steps'),
|
|
36
|
-
];
|
|
37
|
-
const anyJson = { description: 'Defined by the integration or module' };
|
|
38
|
-
|
|
39
|
-
const V2_OPERATIONS = {
|
|
40
|
-
// Integrations
|
|
41
|
-
listIntegrations: {
|
|
42
|
-
summary: "List the caller's integrations",
|
|
43
|
-
description:
|
|
44
|
-
'Returns only `{ integrations }`. Integration types are at /api/v2/integrations/options and connected accounts at /api/v2/entities.',
|
|
45
|
-
responses: { 200: ok('Integrations', ref('ListIntegrationsResponse')), ...errors('Unauthorized') },
|
|
46
|
-
},
|
|
47
|
-
listIntegrationOptions: {
|
|
48
|
-
summary: 'List the integration types this app offers',
|
|
49
|
-
responses: { 200: ok('Integration types', ref('ListIntegrationOptionsResponse')), ...errors('Unauthorized') },
|
|
50
|
-
},
|
|
51
|
-
createIntegration: {
|
|
52
|
-
summary: 'Create an integration between entities the caller owns',
|
|
53
|
-
description: 'An identical existing integration (same user, type and entities) is reused.',
|
|
54
|
-
requestBody: body(ref('CreateIntegrationRequest')),
|
|
55
|
-
responses: {
|
|
56
|
-
201: ok('Created (or reused) integration', ref('Integration')),
|
|
57
|
-
...errors('BadRequest', 'Unauthorized', 'NotFound'),
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
getIntegration: {
|
|
61
|
-
summary: 'Get an integration',
|
|
62
|
-
responses: { 200: ok('Integration', ref('Integration')), ...errors('Unauthorized', 'NotFound') },
|
|
63
|
-
},
|
|
64
|
-
updateIntegration: {
|
|
65
|
-
summary: "Replace an integration's config",
|
|
66
|
-
requestBody: body(ref('UpdateIntegrationRequest')),
|
|
67
|
-
responses: { 200: ok('Updated integration', ref('Integration')), ...errors('BadRequest', 'Unauthorized', 'NotFound') },
|
|
68
|
-
},
|
|
69
|
-
deleteIntegration: {
|
|
70
|
-
summary: 'Delete an integration',
|
|
71
|
-
responses: { 204: { description: 'Deleted' }, ...errors('Unauthorized', 'NotFound') },
|
|
72
|
-
},
|
|
73
|
-
getIntegrationConfigOptions: {
|
|
74
|
-
summary: 'Get config options (form schema) for an integration',
|
|
75
|
-
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
76
|
-
},
|
|
77
|
-
refreshIntegrationConfigOptions: {
|
|
78
|
-
summary: 'Refresh config options with the current form values',
|
|
79
|
-
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
80
|
-
responses: { 200: ok('Options', anyJson), ...errors('BadRequest', 'Unauthorized', 'NotFound') },
|
|
81
|
-
},
|
|
82
|
-
listIntegrationActions: {
|
|
83
|
-
summary: 'List user actions an integration offers',
|
|
84
|
-
parameters: [query('actionType', 'Only actions of this userActionType')],
|
|
85
|
-
responses: { 200: ok('User actions', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
86
|
-
},
|
|
87
|
-
getIntegrationActionOptions: {
|
|
88
|
-
summary: 'Get the options form for a user action',
|
|
89
|
-
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
90
|
-
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
91
|
-
},
|
|
92
|
-
refreshIntegrationActionOptions: {
|
|
93
|
-
summary: 'Refresh a user action options form',
|
|
94
|
-
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
95
|
-
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
96
|
-
},
|
|
97
|
-
runIntegrationAction: {
|
|
98
|
-
summary: 'Run a user action',
|
|
99
|
-
description: 'Only actions the integration lists as user actions can run; anything else is 404 ACTION_NOT_FOUND.',
|
|
100
|
-
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
101
|
-
responses: { 200: ok('Action result', anyJson), ...errors('BadRequest', 'Unauthorized', 'NotFound') },
|
|
102
|
-
},
|
|
103
|
-
testIntegrationAuth: {
|
|
104
|
-
summary: "Test an integration's connections",
|
|
105
|
-
responses: { 200: ok('Result', ref('AuthTestResult')), ...errors('Unauthorized', 'NotFound') },
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
// Entities
|
|
109
|
-
listEntities: {
|
|
110
|
-
summary: "List the caller's connected entities",
|
|
111
|
-
responses: { 200: ok('Entities', ref('ListEntitiesResponse')), ...errors('Unauthorized') },
|
|
112
|
-
},
|
|
113
|
-
listEntityTypes: {
|
|
114
|
-
summary: 'List the entity types (API modules) this app can connect',
|
|
115
|
-
description:
|
|
116
|
-
'Every type registered in appDefinition.entities plus every module a declared integration binds. `usedBy` lists the integrations that bind each type; it is empty for a type registered without any integration.',
|
|
117
|
-
responses: { 200: ok('Entity types', ref('ListEntityTypesResponse')), ...errors('Unauthorized') },
|
|
118
|
-
},
|
|
119
|
-
getEntityType: {
|
|
120
|
-
summary: 'Describe an entity type',
|
|
121
|
-
description:
|
|
122
|
-
'Includes `usedBy`, the integrations that bind the type (empty for a registry-only type).',
|
|
123
|
-
responses: { 200: ok('Entity type', ref('EntityType')), ...errors('Unauthorized', 'NotFound') },
|
|
124
|
-
},
|
|
125
|
-
getEntityTypeRequirements: {
|
|
126
|
-
summary: 'Describe one authorization step of an entity type',
|
|
127
|
-
description: 'Read-only: does not start a session. Use GET /api/v2/authorize to start a flow.',
|
|
128
|
-
parameters: [stepQuery[0]],
|
|
129
|
-
responses: {
|
|
130
|
-
200: ok('Requirements', ref('AuthorizationRequirements')),
|
|
131
|
-
...errors('BadRequest', 'Unauthorized', 'NotFound'),
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
getEntity: {
|
|
135
|
-
summary: 'Get an entity',
|
|
136
|
-
responses: { 200: ok('Entity', ref('Entity')), ...errors('Unauthorized', 'NotFound') },
|
|
137
|
-
},
|
|
138
|
-
deleteEntity: {
|
|
139
|
-
summary: 'Delete an entity',
|
|
140
|
-
description: 'Refused with 409 ENTITY_IN_USE while integrations use it. The credential stays.',
|
|
141
|
-
responses: { 204: { description: 'Deleted' }, ...errors('Unauthorized', 'NotFound', 'Conflict') },
|
|
142
|
-
},
|
|
143
|
-
testEntityAuth: {
|
|
144
|
-
summary: "Test an entity's stored credential",
|
|
145
|
-
responses: { 200: ok('Result', ref('AuthTestResult')), ...errors('Unauthorized', 'NotFound') },
|
|
146
|
-
},
|
|
147
|
-
getEntityOptions: {
|
|
148
|
-
summary: "Get an entity's options",
|
|
149
|
-
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
150
|
-
},
|
|
151
|
-
refreshEntityOptions: {
|
|
152
|
-
summary: "Refresh an entity's options",
|
|
153
|
-
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
154
|
-
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
155
|
-
},
|
|
156
|
-
proxyEntityRequest: {
|
|
157
|
-
summary: "Call the entity's API through Frigg (beta)",
|
|
158
|
-
description:
|
|
159
|
-
"ADR-052. Off unless the app sets managementApi.proxy.enable. Path-only input relative to the module's base URL; deny by default, only operations in the module's (or the app's) allow-list. The HTTP status follows the upstream status; upstream 5xx is 502 UPSTREAM_ERROR, 429 keeps Retry-After.",
|
|
160
|
-
requestBody: body(ref('ProxyRequest')),
|
|
161
|
-
responses: {
|
|
162
|
-
200: ok('Upstream answer (the status mirrors the upstream status)', ref('ProxyResponse')),
|
|
163
|
-
400: ok('INVALID_PROXY_REQUEST', ref('ProxyError')),
|
|
164
|
-
401: ok('INVALID_CREDENTIALS or unauthenticated', ref('ProxyError')),
|
|
165
|
-
403: ok('PROXY_NOT_ALLOWED', ref('ProxyError')),
|
|
166
|
-
404: ok('Entity not found', ref('Error')),
|
|
167
|
-
413: ok('PAYLOAD_TOO_LARGE', ref('ProxyError')),
|
|
168
|
-
415: ok('UNSUPPORTED_MEDIA_TYPE', ref('ProxyError')),
|
|
169
|
-
429: ok('RATE_LIMITED (Retry-After passed through)', ref('ProxyError')),
|
|
170
|
-
502: ok('UPSTREAM_ERROR, NETWORK_ERROR or RESPONSE_TOO_LARGE', ref('ProxyError')),
|
|
171
|
-
504: ok('TIMEOUT', ref('ProxyError')),
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
// Credentials
|
|
176
|
-
listCredentials: {
|
|
177
|
-
summary: "List the caller's credentials (masked)",
|
|
178
|
-
responses: { 200: ok('Credentials', ref('ListCredentialsResponse')), ...errors('Unauthorized') },
|
|
179
|
-
},
|
|
180
|
-
getCredential: {
|
|
181
|
-
summary: 'Get a credential (masked)',
|
|
182
|
-
responses: { 200: ok('Credential', ref('Credential')), ...errors('Unauthorized', 'NotFound') },
|
|
183
|
-
},
|
|
184
|
-
deleteCredential: {
|
|
185
|
-
summary: 'Delete a credential',
|
|
186
|
-
description: 'Entities that used it stay, with no credential, until re-authorized.',
|
|
187
|
-
responses: { 204: { description: 'Deleted' }, ...errors('Unauthorized', 'NotFound') },
|
|
188
|
-
},
|
|
189
|
-
getCredentialReauthorizeRequirements: {
|
|
190
|
-
summary: 'Start or continue re-authorizing a credential',
|
|
191
|
-
parameters: stepQuery,
|
|
192
|
-
responses: {
|
|
193
|
-
200: ok('Requirements', ref('AuthorizationRequirements')),
|
|
194
|
-
...errors('BadRequest', 'Unauthorized', 'NotFound', 'Conflict'),
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
reauthorizeCredential: {
|
|
198
|
-
summary: 'Submit a re-authorization step',
|
|
199
|
-
requestBody: body(ref('ReauthorizeRequest')),
|
|
200
|
-
responses: {
|
|
201
|
-
200: ok('Complete or pending', ref('AuthorizeResponse')),
|
|
202
|
-
...errors('BadRequest', 'Unauthorized', 'NotFound', 'Conflict', 'BadGateway'),
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
// Authorization
|
|
207
|
-
getAuthorizationRequirements: {
|
|
208
|
-
summary: 'Start or continue an authorization',
|
|
209
|
-
description:
|
|
210
|
-
'Single-step modules return their requirements. Multi-step modules start a session on step 1 (returned as sessionId); later steps need it and must be the current step.',
|
|
211
|
-
parameters: [
|
|
212
|
-
query('entityType', 'Module name', { type: 'string' }, true),
|
|
213
|
-
...stepQuery,
|
|
214
|
-
query('state', 'OAuth state forwarded to the provider'),
|
|
215
|
-
],
|
|
216
|
-
responses: {
|
|
217
|
-
200: ok('Requirements', ref('AuthorizationRequirements')),
|
|
218
|
-
...errors('BadRequest', 'Unauthorized', 'NotFound', 'Conflict'),
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
authorize: {
|
|
222
|
-
summary: 'Submit an authorization step',
|
|
223
|
-
description:
|
|
224
|
-
'Answers { status: "complete", entity, credential } or { status: "pending", step, totalSteps, sessionId, requirements }.',
|
|
225
|
-
requestBody: body(ref('AuthorizeRequest')),
|
|
226
|
-
responses: {
|
|
227
|
-
200: ok('Complete or pending', ref('AuthorizeResponse')),
|
|
228
|
-
...errors('BadRequest', 'Unauthorized', 'NotFound', 'Conflict', 'BadGateway'),
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const META_OPERATIONS = {
|
|
234
|
-
getApiMeta: {
|
|
235
|
-
summary: 'Supported API versions and capabilities',
|
|
236
|
-
description: 'No authentication, no database. The core version is added for admin callers only.',
|
|
237
|
-
security: [],
|
|
238
|
-
responses: { 200: ok('Discovery document', ref('ApiMeta')) },
|
|
239
|
-
},
|
|
240
|
-
getOpenApiV1: {
|
|
241
|
-
summary: 'OpenAPI document for Management API v1 (deprecated)',
|
|
242
|
-
security: [],
|
|
243
|
-
responses: { 200: ok('OpenAPI 3 document', { type: 'object' }) },
|
|
244
|
-
},
|
|
245
|
-
getOpenApiV2: {
|
|
246
|
-
summary: 'OpenAPI document for Management API v2',
|
|
247
|
-
security: [],
|
|
248
|
-
responses: { 200: ok('OpenAPI 3 document', { type: 'object' }) },
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
module.exports = { V2_OPERATIONS, META_OPERATIONS };
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Management API route registry (ADR-053).
|
|
3
|
-
*
|
|
4
|
-
* The single source of truth for the Management API surface. Three consumers
|
|
5
|
-
* read it, so they cannot drift apart:
|
|
6
|
-
*
|
|
7
|
-
* - the Express routers (v2 mounts its handlers by `operationId`; v1 gets its
|
|
8
|
-
* deprecation headers from the v1 list),
|
|
9
|
-
* - the OpenAPI documents served at /api/meta/openapi/v{n}.json,
|
|
10
|
-
* - the API Gateway HTTP API routes that devtools generates
|
|
11
|
-
* (`getGatewayRoutes`, used by base-definition-factory).
|
|
12
|
-
*
|
|
13
|
-
* Rules (ADR-053):
|
|
14
|
-
* - v2 lives under `/api/v2/<resource>`. Gateway routes are declared per
|
|
15
|
-
* resource, never `ANY /api/v2/{proxy+}`: `/api/v2/reports` (ADR-010) is
|
|
16
|
-
* served by the admin-scripts Lambda and must not be shadowed.
|
|
17
|
-
* - v1 is the frozen unprefixed surface. Its handlers live in
|
|
18
|
-
* integrations/integration-router.js and do not change; this list mirrors
|
|
19
|
-
* them (a contract test keeps the two equal).
|
|
20
|
-
* - `/api/meta` is version-neutral and served by the DB-free `health` Lambda.
|
|
21
|
-
*
|
|
22
|
-
* This module must stay free of database, AWS and app-definition imports:
|
|
23
|
-
* devtools loads it at build time.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Supported Management API majors. `deprecatedAt` feeds the RFC 9745
|
|
28
|
-
* `Deprecation` header; `sunset` (RFC 8594) stays null until a removal release
|
|
29
|
-
* is scheduled.
|
|
30
|
-
*/
|
|
31
|
-
const MANAGEMENT_API_VERSIONS = Object.freeze({
|
|
32
|
-
1: Object.freeze({
|
|
33
|
-
status: 'deprecated',
|
|
34
|
-
deprecatedAt: '2026-11-01',
|
|
35
|
-
sunset: null,
|
|
36
|
-
}),
|
|
37
|
-
2: Object.freeze({ status: 'stable' }),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const PREFERRED_API_VERSION = '2';
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* `/api/v2/<segment>` values no v2 route may take. `reports` belongs to the
|
|
44
|
-
* admin-scripts Lambda (ADR-010); where it lives long term is open (ADR-053
|
|
45
|
-
* §7), so it is reserved, kept out of the v2 OpenAPI document and out of the
|
|
46
|
-
* /api/meta capabilities.
|
|
47
|
-
*/
|
|
48
|
-
const RESERVED_V2_SEGMENTS = Object.freeze(['reports']);
|
|
49
|
-
|
|
50
|
-
const V1_MIGRATION_DOCS_URL =
|
|
51
|
-
'https://docs.friggframework.org/api/migrate-v1-v2';
|
|
52
|
-
|
|
53
|
-
/** Lambda functions (devtools names) that serve Management API routes. */
|
|
54
|
-
const FUNCTIONS = Object.freeze({ AUTH: 'auth', HEALTH: 'health' });
|
|
55
|
-
|
|
56
|
-
const route = (version, fn) => (definition) =>
|
|
57
|
-
Object.freeze({ version, function: fn, ...definition });
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The v1 routes registered by createIntegrationRouter(). `ANY` marks routes
|
|
61
|
-
* registered with `router.all()`. Frozen: no behaviour change except the
|
|
62
|
-
* deprecation headers.
|
|
63
|
-
*/
|
|
64
|
-
const V1_ROUTES = Object.freeze(
|
|
65
|
-
[
|
|
66
|
-
{ method: 'GET', path: '/api/integrations' },
|
|
67
|
-
{ method: 'POST', path: '/api/integrations' },
|
|
68
|
-
{ method: 'PATCH', path: '/api/integrations/:integrationId' },
|
|
69
|
-
{ method: 'DELETE', path: '/api/integrations/:integrationId' },
|
|
70
|
-
{
|
|
71
|
-
method: 'GET',
|
|
72
|
-
path: '/api/integrations/:integrationId/config/options',
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
method: 'POST',
|
|
76
|
-
path: '/api/integrations/:integrationId/config/options/refresh',
|
|
77
|
-
},
|
|
78
|
-
{ method: 'ANY', path: '/api/integrations/:integrationId/actions' },
|
|
79
|
-
{
|
|
80
|
-
method: 'ANY',
|
|
81
|
-
path: '/api/integrations/:integrationId/actions/:actionId/options',
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
method: 'POST',
|
|
85
|
-
path: '/api/integrations/:integrationId/actions/:actionId/options/refresh',
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
method: 'POST',
|
|
89
|
-
path: '/api/integrations/:integrationId/actions/:actionId',
|
|
90
|
-
},
|
|
91
|
-
{ method: 'GET', path: '/api/integrations/:integrationId' },
|
|
92
|
-
{ method: 'GET', path: '/api/integrations/:integrationId/test-auth' },
|
|
93
|
-
{ method: 'GET', path: '/api/authorize' },
|
|
94
|
-
{ method: 'POST', path: '/api/authorize' },
|
|
95
|
-
{ method: 'POST', path: '/api/entity' },
|
|
96
|
-
{ method: 'GET', path: '/api/entity/options/:credentialId' },
|
|
97
|
-
{ method: 'GET', path: '/api/entities/:entityId/test-auth' },
|
|
98
|
-
{ method: 'GET', path: '/api/entities/:entityId' },
|
|
99
|
-
{ method: 'POST', path: '/api/entities/:entityId/options' },
|
|
100
|
-
{ method: 'POST', path: '/api/entities/:entityId/options/refresh' },
|
|
101
|
-
].map(route('1', FUNCTIONS.AUTH))
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* The v2 routes. Order matters: Express matches in this order, so literal
|
|
106
|
-
* segments (`/options`, `/types`) come before `/:id` siblings.
|
|
107
|
-
*
|
|
108
|
-
* - `operationId` keys the handler in management-api/v2 and the OpenAPI
|
|
109
|
-
* operation.
|
|
110
|
-
* - `stability: 'beta'` marks routes that may still change in a core minor
|
|
111
|
-
* (ADR-053 §2); it is published as `x-frigg-stability`.
|
|
112
|
-
* - `capability` names the /api/meta capability a route belongs to.
|
|
113
|
-
*/
|
|
114
|
-
const V2_ROUTES = Object.freeze(
|
|
115
|
-
[
|
|
116
|
-
// Integrations
|
|
117
|
-
{ operationId: 'listIntegrations', method: 'GET', path: '/api/v2/integrations', tag: 'Integrations' },
|
|
118
|
-
{ operationId: 'listIntegrationOptions', method: 'GET', path: '/api/v2/integrations/options', tag: 'Integrations' },
|
|
119
|
-
{ operationId: 'createIntegration', method: 'POST', path: '/api/v2/integrations', tag: 'Integrations' },
|
|
120
|
-
{ operationId: 'getIntegration', method: 'GET', path: '/api/v2/integrations/:integrationId', tag: 'Integrations' },
|
|
121
|
-
{ operationId: 'updateIntegration', method: 'PATCH', path: '/api/v2/integrations/:integrationId', tag: 'Integrations' },
|
|
122
|
-
{ operationId: 'deleteIntegration', method: 'DELETE', path: '/api/v2/integrations/:integrationId', tag: 'Integrations' },
|
|
123
|
-
{ operationId: 'getIntegrationConfigOptions', method: 'GET', path: '/api/v2/integrations/:integrationId/config/options', tag: 'Integrations' },
|
|
124
|
-
{ operationId: 'refreshIntegrationConfigOptions', method: 'POST', path: '/api/v2/integrations/:integrationId/config/options/refresh', tag: 'Integrations' },
|
|
125
|
-
{ operationId: 'listIntegrationActions', method: 'GET', path: '/api/v2/integrations/:integrationId/actions', tag: 'Integrations' },
|
|
126
|
-
{ operationId: 'getIntegrationActionOptions', method: 'POST', path: '/api/v2/integrations/:integrationId/actions/:actionId/options', tag: 'Integrations' },
|
|
127
|
-
{ operationId: 'refreshIntegrationActionOptions', method: 'POST', path: '/api/v2/integrations/:integrationId/actions/:actionId/options/refresh', tag: 'Integrations' },
|
|
128
|
-
{ operationId: 'runIntegrationAction', method: 'POST', path: '/api/v2/integrations/:integrationId/actions/:actionId', tag: 'Integrations' },
|
|
129
|
-
{ operationId: 'testIntegrationAuth', method: 'GET', path: '/api/v2/integrations/:integrationId/test-auth', tag: 'Integrations' },
|
|
130
|
-
|
|
131
|
-
// Entities
|
|
132
|
-
{ operationId: 'listEntities', method: 'GET', path: '/api/v2/entities', tag: 'Entities' },
|
|
133
|
-
{ operationId: 'listEntityTypes', method: 'GET', path: '/api/v2/entities/types', tag: 'Entities' },
|
|
134
|
-
{ operationId: 'getEntityType', method: 'GET', path: '/api/v2/entities/types/:entityType', tag: 'Entities' },
|
|
135
|
-
{ operationId: 'getEntityTypeRequirements', method: 'GET', path: '/api/v2/entities/types/:entityType/requirements', tag: 'Entities' },
|
|
136
|
-
{ operationId: 'getEntity', method: 'GET', path: '/api/v2/entities/:entityId', tag: 'Entities' },
|
|
137
|
-
{ operationId: 'deleteEntity', method: 'DELETE', path: '/api/v2/entities/:entityId', tag: 'Entities' },
|
|
138
|
-
{ operationId: 'testEntityAuth', method: 'GET', path: '/api/v2/entities/:entityId/test-auth', tag: 'Entities' },
|
|
139
|
-
{ operationId: 'getEntityOptions', method: 'GET', path: '/api/v2/entities/:entityId/options', tag: 'Entities' },
|
|
140
|
-
{ operationId: 'refreshEntityOptions', method: 'POST', path: '/api/v2/entities/:entityId/options/refresh', tag: 'Entities' },
|
|
141
|
-
{ operationId: 'proxyEntityRequest', method: 'POST', path: '/api/v2/entities/:entityId/proxy', tag: 'Entities', stability: 'beta', capability: 'entityProxy' },
|
|
142
|
-
|
|
143
|
-
// Credentials
|
|
144
|
-
{ operationId: 'listCredentials', method: 'GET', path: '/api/v2/credentials', tag: 'Credentials', capability: 'credentials' },
|
|
145
|
-
{ operationId: 'getCredential', method: 'GET', path: '/api/v2/credentials/:credentialId', tag: 'Credentials', capability: 'credentials' },
|
|
146
|
-
{ operationId: 'deleteCredential', method: 'DELETE', path: '/api/v2/credentials/:credentialId', tag: 'Credentials', capability: 'credentials' },
|
|
147
|
-
{ operationId: 'getCredentialReauthorizeRequirements', method: 'GET', path: '/api/v2/credentials/:credentialId/reauthorize', tag: 'Credentials', capability: 'credentials' },
|
|
148
|
-
{ operationId: 'reauthorizeCredential', method: 'POST', path: '/api/v2/credentials/:credentialId/reauthorize', tag: 'Credentials', capability: 'credentials' },
|
|
149
|
-
|
|
150
|
-
// Authorization (multi-step)
|
|
151
|
-
{ operationId: 'getAuthorizationRequirements', method: 'GET', path: '/api/v2/authorize', tag: 'Authorization', capability: 'multiStepAuthorize' },
|
|
152
|
-
{ operationId: 'authorize', method: 'POST', path: '/api/v2/authorize', tag: 'Authorization', capability: 'multiStepAuthorize' },
|
|
153
|
-
].map(route('2', FUNCTIONS.AUTH))
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
/** Version-neutral discovery routes, served without auth or database. */
|
|
157
|
-
const META_ROUTES = Object.freeze(
|
|
158
|
-
[
|
|
159
|
-
{ operationId: 'getApiMeta', method: 'GET', path: '/api/meta' },
|
|
160
|
-
...Object.keys(MANAGEMENT_API_VERSIONS).map((major) => ({
|
|
161
|
-
operationId: `getOpenApiV${major}`,
|
|
162
|
-
method: 'GET',
|
|
163
|
-
path: `/api/meta/openapi/v${major}.json`,
|
|
164
|
-
})),
|
|
165
|
-
].map(route(null, FUNCTIONS.HEALTH))
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Unversioned routes the auth Lambda serves next to the Management API
|
|
170
|
-
* (core/handlers/routers/auth.js). The OAuth redirect is the provider's
|
|
171
|
-
* callback target, so it is neither deprecated nor disabled with v1.
|
|
172
|
-
*/
|
|
173
|
-
const AUTH_EXTRA_ROUTES = Object.freeze(
|
|
174
|
-
[
|
|
175
|
-
{
|
|
176
|
-
operationId: 'oauthRedirect',
|
|
177
|
-
method: 'GET',
|
|
178
|
-
path: '/api/integrations/redirect/:appId',
|
|
179
|
-
},
|
|
180
|
-
].map(route(null, FUNCTIONS.AUTH))
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The v2 routes an app serves. Routes tied to an opt-in capability (the entity
|
|
185
|
-
* proxy, ADR-052) are left out unless the app enables it.
|
|
186
|
-
*
|
|
187
|
-
* @param {Object} [options]
|
|
188
|
-
* @param {boolean} [options.proxy=false] - `managementApi.proxy.enable`
|
|
189
|
-
*/
|
|
190
|
-
function listV2Routes({ proxy = false } = {}) {
|
|
191
|
-
return V2_ROUTES.filter(
|
|
192
|
-
(record) => proxy || record.capability !== 'entityProxy'
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* @param {Object} [options]
|
|
198
|
-
* @param {boolean} [options.v1=true] - false drops the v1 routes
|
|
199
|
-
* (`managementApi.v1: false` in the app definition).
|
|
200
|
-
* @param {boolean} [options.proxy=false] - true keeps the entity proxy route.
|
|
201
|
-
* @returns {Array<Object>} Every registered route record.
|
|
202
|
-
*/
|
|
203
|
-
function listRoutes({ v1 = true, proxy = false } = {}) {
|
|
204
|
-
return [
|
|
205
|
-
...(v1 ? V1_ROUTES : []),
|
|
206
|
-
...listV2Routes({ proxy }),
|
|
207
|
-
...META_ROUTES,
|
|
208
|
-
...AUTH_EXTRA_ROUTES,
|
|
209
|
-
];
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** `/api/v2/entities/:entityId` -> `/api/v2/entities/{entityId}` */
|
|
213
|
-
function toGatewayPath(expressPath) {
|
|
214
|
-
return expressPath.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* The resource root a route is grouped under for API Gateway:
|
|
219
|
-
* `/api/v2/<resource>` for v2, `/api/<resource>` for v1, `/api/meta` for meta.
|
|
220
|
-
*/
|
|
221
|
-
function resourceRoot(record) {
|
|
222
|
-
const segments = record.path.split('/').filter(Boolean);
|
|
223
|
-
const depth = record.version === '2' ? 3 : 2;
|
|
224
|
-
return `/${segments.slice(0, depth).join('/')}`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* API Gateway HTTP API routes for the Management API, derived from the
|
|
229
|
-
* registry. Each resource root gets an exact route when the root itself is
|
|
230
|
-
* registered and a greedy `{proxy+}` route when deeper paths are. The auth
|
|
231
|
-
* function uses `ANY` (as it always has, so CORS preflights reach Express);
|
|
232
|
-
* the health function only serves GET.
|
|
233
|
-
*
|
|
234
|
-
* @param {Object} [options]
|
|
235
|
-
* @param {boolean} [options.v1=true]
|
|
236
|
-
* @param {boolean} [options.proxy=false]
|
|
237
|
-
* @returns {Array<{function: string, method: string, path: string}>}
|
|
238
|
-
*/
|
|
239
|
-
function getGatewayRoutes({ v1 = true, proxy = false } = {}) {
|
|
240
|
-
const out = [];
|
|
241
|
-
const seen = new Set();
|
|
242
|
-
const add = (fn, method, path) => {
|
|
243
|
-
const key = `${fn} ${method} ${path}`;
|
|
244
|
-
if (seen.has(key)) return;
|
|
245
|
-
seen.add(key);
|
|
246
|
-
out.push({ function: fn, method, path });
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const grouped = [
|
|
250
|
-
...(v1 ? V1_ROUTES : []),
|
|
251
|
-
...listV2Routes({ proxy }),
|
|
252
|
-
...META_ROUTES,
|
|
253
|
-
];
|
|
254
|
-
const greedyRoots = new Set();
|
|
255
|
-
for (const record of grouped) {
|
|
256
|
-
const root = resourceRoot(record);
|
|
257
|
-
const method = record.function === FUNCTIONS.AUTH ? 'ANY' : 'GET';
|
|
258
|
-
if (record.path === root) {
|
|
259
|
-
add(record.function, method, root);
|
|
260
|
-
} else {
|
|
261
|
-
add(record.function, method, `${root}/{proxy+}`);
|
|
262
|
-
greedyRoots.add(`${record.function} ${root}`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Extra routes get an exact route unless a greedy route on the same
|
|
267
|
-
// function already covers them.
|
|
268
|
-
for (const record of AUTH_EXTRA_ROUTES) {
|
|
269
|
-
const covered = greedyRoots.has(
|
|
270
|
-
`${record.function} ${resourceRoot(record)}`
|
|
271
|
-
);
|
|
272
|
-
if (!covered) {
|
|
273
|
-
add(record.function, record.method, toGatewayPath(record.path));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return out;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
module.exports = {
|
|
281
|
-
MANAGEMENT_API_VERSIONS,
|
|
282
|
-
PREFERRED_API_VERSION,
|
|
283
|
-
RESERVED_V2_SEGMENTS,
|
|
284
|
-
V1_MIGRATION_DOCS_URL,
|
|
285
|
-
FUNCTIONS,
|
|
286
|
-
V1_ROUTES,
|
|
287
|
-
V2_ROUTES,
|
|
288
|
-
META_ROUTES,
|
|
289
|
-
AUTH_EXTRA_ROUTES,
|
|
290
|
-
listRoutes,
|
|
291
|
-
listV2Routes,
|
|
292
|
-
getGatewayRoutes,
|
|
293
|
-
toGatewayPath,
|
|
294
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test helper: send one real HTTP request to an Express app on an ephemeral
|
|
3
|
-
* port and return { status, headers, body }. Real HTTP (not a mocked req/res)
|
|
4
|
-
* so body parsing, the error boundary and headers behave as in Lambda.
|
|
5
|
-
*/
|
|
6
|
-
async function request(app, { method = 'GET', path, headers = {}, body, rawBody } = {}) {
|
|
7
|
-
const server = await new Promise((resolve) => {
|
|
8
|
-
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
|
9
|
-
});
|
|
10
|
-
try {
|
|
11
|
-
const init = { method, headers: { ...headers } };
|
|
12
|
-
if (rawBody !== undefined) {
|
|
13
|
-
init.body = rawBody;
|
|
14
|
-
} else if (body !== undefined) {
|
|
15
|
-
init.body = JSON.stringify(body);
|
|
16
|
-
init.headers['content-type'] = 'application/json';
|
|
17
|
-
}
|
|
18
|
-
const res = await fetch(
|
|
19
|
-
`http://127.0.0.1:${server.address().port}${path}`,
|
|
20
|
-
init
|
|
21
|
-
);
|
|
22
|
-
const text = await res.text();
|
|
23
|
-
let parsed = text;
|
|
24
|
-
try {
|
|
25
|
-
parsed = text ? JSON.parse(text) : undefined;
|
|
26
|
-
} catch {
|
|
27
|
-
// not JSON; keep the text
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
status: res.status,
|
|
31
|
-
headers: Object.fromEntries(res.headers.entries()),
|
|
32
|
-
body: parsed,
|
|
33
|
-
};
|
|
34
|
-
} finally {
|
|
35
|
-
await new Promise((resolve) => server.close(resolve));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = { request };
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
MANAGEMENT_API_VERSIONS,
|
|
3
|
-
PREFERRED_API_VERSION,
|
|
4
|
-
listV2Routes,
|
|
5
|
-
} = require('../route-registry');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Builds the GET /api/meta document (ADR-053 §4): the API majors this
|
|
9
|
-
* deployment serves, their status and OpenAPI links, and the capability flags
|
|
10
|
-
* clients feature-detect with. Reads configuration only: no database, no
|
|
11
|
-
* network.
|
|
12
|
-
*/
|
|
13
|
-
class GetManagementApiMeta {
|
|
14
|
-
/**
|
|
15
|
-
* @param {Object} params
|
|
16
|
-
* @param {ReturnType<import('../management-api-config').resolveManagementApiConfig>} params.config
|
|
17
|
-
* @param {string} params.coreVersion - @friggframework/core version, admin-only.
|
|
18
|
-
*/
|
|
19
|
-
constructor({ config, coreVersion }) {
|
|
20
|
-
this.config = config;
|
|
21
|
-
this.coreVersion = coreVersion;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {Object} [options]
|
|
26
|
-
* @param {boolean} [options.isAdmin=false] - caller presented the admin key.
|
|
27
|
-
*/
|
|
28
|
-
execute({ isAdmin = false } = {}) {
|
|
29
|
-
const versions = {};
|
|
30
|
-
for (const [major, info] of Object.entries(MANAGEMENT_API_VERSIONS)) {
|
|
31
|
-
const disabled = major === '1' && !this.config.v1;
|
|
32
|
-
versions[major] = {
|
|
33
|
-
...info,
|
|
34
|
-
...(disabled && { status: 'disabled' }),
|
|
35
|
-
openapi: `/api/meta/openapi/v${major}.json`,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Capabilities come from the routes the app actually serves, so a flag
|
|
40
|
-
// can never advertise a route that is not mounted.
|
|
41
|
-
const capabilities = [
|
|
42
|
-
...new Set(
|
|
43
|
-
listV2Routes({ proxy: this.config.proxy.enable })
|
|
44
|
-
.map((r) => r.capability)
|
|
45
|
-
.filter(Boolean)
|
|
46
|
-
),
|
|
47
|
-
].sort();
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
api: { versions, preferred: PREFERRED_API_VERSION },
|
|
51
|
-
capabilities,
|
|
52
|
-
...(isAdmin && { frigg: { coreVersion: this.coreVersion } }),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
module.exports = { GetManagementApiMeta };
|