@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,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 };