@friggframework/core 2.0.0--canary.704.79d9a63.0 → 2.0.0--canary.686.e94de4d.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/CLAUDE.md +28 -40
- package/README.md +105 -0
- package/application/commands/entity-commands.js +60 -18
- package/application/commands/integration-commands.js +20 -73
- 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-encryption-service.js +9 -22
- package/database/documentdb-utils.js +2 -0
- package/database/encryption/README.md +76 -14
- package/database/encryption/documentdb-encryption-service.md +78 -1
- package/database/encryption/encryption-config.js +219 -0
- package/database/encryption/encryption-schema-registry.js +93 -12
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/organization-where.js +18 -0
- package/database/prisma.js +15 -38
- package/database/unique-violation.js +25 -0
- package/database/use-cases/check-encryption-health-use-case.js +43 -52
- package/errors/index.js +0 -4
- 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/WEBHOOKS.md +0 -29
- 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 -3
- 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-base.js +0 -29
- package/integrations/integration-router.js +131 -96
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -35
- package/integrations/repositories/integration-repository-interface.js +17 -31
- package/integrations/repositories/integration-repository-mongo.js +60 -129
- package/integrations/repositories/integration-repository-postgres.js +62 -134
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +72 -75
- 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/modules/utils/map-module-dto.js +3 -5
- 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/tests/doubles/test-user-repository.js +1 -1
- 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-bearer-token.js +2 -7
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/errors/mapping-already-exists-error.js +0 -14
- package/integrations/use-cases/add-entities-to-integration.js +0 -92
- package/integrations/use-cases/remove-entities-from-integration.js +0 -70
- package/integrations/utils/assert-entity-ids-input.js +0 -42
- package/modules/utils/to-credential-id.js +0 -14
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
// ADR-056 §3.4: the same two filters on every list that mixes ownership.
|
|
38
|
+
const listFilters = [
|
|
39
|
+
query('scope', 'Only records of this scope', { type: 'string', enum: ['individual', 'organization'] }),
|
|
40
|
+
query('owner', 'Only the caller\'s own (`me`) or the active organization\'s (`org`)', { type: 'string', enum: ['me', 'org'] }),
|
|
41
|
+
];
|
|
42
|
+
const anyJson = { description: 'Defined by the integration or module' };
|
|
43
|
+
|
|
44
|
+
const V2_OPERATIONS = {
|
|
45
|
+
// Integrations
|
|
46
|
+
listIntegrations: {
|
|
47
|
+
summary: "List the caller's integrations",
|
|
48
|
+
description:
|
|
49
|
+
'Returns only `{ integrations }`: the caller\'s and the active organization\'s, created in that organization (ADR-056 §3.6). Each carries `scope`, `owner`, `organizationId` and `access`. Integration types are at /api/v2/integrations/options and connected accounts at /api/v2/entities.',
|
|
50
|
+
parameters: listFilters,
|
|
51
|
+
responses: { 200: ok('Integrations', ref('ListIntegrationsResponse')), ...errors('Unauthorized') },
|
|
52
|
+
},
|
|
53
|
+
listIntegrationOptions: {
|
|
54
|
+
summary: 'List the integration types this app offers',
|
|
55
|
+
responses: { 200: ok('Integration types', ref('ListIntegrationOptionsResponse')), ...errors('Unauthorized') },
|
|
56
|
+
},
|
|
57
|
+
createIntegration: {
|
|
58
|
+
summary: 'Create an integration between entities the caller owns',
|
|
59
|
+
description: 'An identical existing integration (same user, type and entities) is reused.',
|
|
60
|
+
requestBody: body(ref('CreateIntegrationRequest')),
|
|
61
|
+
responses: {
|
|
62
|
+
201: ok('Created (or reused) integration', ref('Integration')),
|
|
63
|
+
...errors('BadRequest', 'Unauthorized', 'NotFound'),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
getIntegration: {
|
|
67
|
+
summary: 'Get an integration',
|
|
68
|
+
responses: { 200: ok('Integration', ref('Integration')), ...errors('Unauthorized', 'NotFound') },
|
|
69
|
+
},
|
|
70
|
+
updateIntegration: {
|
|
71
|
+
summary: "Replace an integration's config",
|
|
72
|
+
requestBody: body(ref('UpdateIntegrationRequest')),
|
|
73
|
+
responses: { 200: ok('Updated integration', ref('Integration')), ...errors('BadRequest', 'Unauthorized', 'NotFound') },
|
|
74
|
+
},
|
|
75
|
+
deleteIntegration: {
|
|
76
|
+
summary: 'Delete an integration',
|
|
77
|
+
responses: { 204: { description: 'Deleted' }, ...errors('Unauthorized', 'NotFound') },
|
|
78
|
+
},
|
|
79
|
+
getIntegrationConfigOptions: {
|
|
80
|
+
summary: 'Get config options (form schema) for an integration',
|
|
81
|
+
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
82
|
+
},
|
|
83
|
+
refreshIntegrationConfigOptions: {
|
|
84
|
+
summary: 'Refresh config options with the current form values',
|
|
85
|
+
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
86
|
+
responses: { 200: ok('Options', anyJson), ...errors('BadRequest', 'Unauthorized', 'NotFound') },
|
|
87
|
+
},
|
|
88
|
+
listIntegrationActions: {
|
|
89
|
+
summary: 'List user actions an integration offers',
|
|
90
|
+
parameters: [query('actionType', 'Only actions of this userActionType')],
|
|
91
|
+
responses: { 200: ok('User actions', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
92
|
+
},
|
|
93
|
+
getIntegrationActionOptions: {
|
|
94
|
+
summary: 'Get the options form for a user action',
|
|
95
|
+
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
96
|
+
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
97
|
+
},
|
|
98
|
+
refreshIntegrationActionOptions: {
|
|
99
|
+
summary: 'Refresh a user action options form',
|
|
100
|
+
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
101
|
+
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
102
|
+
},
|
|
103
|
+
runIntegrationAction: {
|
|
104
|
+
summary: 'Run a user action',
|
|
105
|
+
description: 'Only actions the integration lists as user actions can run; anything else is 404 ACTION_NOT_FOUND.',
|
|
106
|
+
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
107
|
+
responses: { 200: ok('Action result', anyJson), ...errors('BadRequest', 'Unauthorized', 'NotFound') },
|
|
108
|
+
},
|
|
109
|
+
testIntegrationAuth: {
|
|
110
|
+
summary: "Test an integration's connections",
|
|
111
|
+
responses: { 200: ok('Result', ref('AuthTestResult')), ...errors('Unauthorized', 'NotFound') },
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Entities
|
|
115
|
+
listEntities: {
|
|
116
|
+
summary: "List the caller's connected entities",
|
|
117
|
+
description:
|
|
118
|
+
"The active organization's entities and the caller's individual ones (ADR-056 §3.6): an individual entity belongs to the organization it was connected in, unless `ownership.individualAcross` is `all`. Each carries `scope`, `owner`, `organizationId` and `access`.",
|
|
119
|
+
parameters: listFilters,
|
|
120
|
+
responses: { 200: ok('Entities', ref('ListEntitiesResponse')), ...errors('Unauthorized') },
|
|
121
|
+
},
|
|
122
|
+
listEntityTypes: {
|
|
123
|
+
summary: 'List the entity types (API modules) this app can connect',
|
|
124
|
+
description:
|
|
125
|
+
'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.',
|
|
126
|
+
responses: { 200: ok('Entity types', ref('ListEntityTypesResponse')), ...errors('Unauthorized') },
|
|
127
|
+
},
|
|
128
|
+
getEntityType: {
|
|
129
|
+
summary: 'Describe an entity type',
|
|
130
|
+
description:
|
|
131
|
+
'Includes `usedBy`, the integrations that bind the type (empty for a registry-only type).',
|
|
132
|
+
responses: { 200: ok('Entity type', ref('EntityType')), ...errors('Unauthorized', 'NotFound') },
|
|
133
|
+
},
|
|
134
|
+
getEntityTypeRequirements: {
|
|
135
|
+
summary: 'Describe one authorization step of an entity type',
|
|
136
|
+
description: 'Read-only: does not start a session. Use GET /api/v2/authorize to start a flow.',
|
|
137
|
+
parameters: [stepQuery[0]],
|
|
138
|
+
responses: {
|
|
139
|
+
200: ok('Requirements', ref('AuthorizationRequirements')),
|
|
140
|
+
...errors('BadRequest', 'Unauthorized', 'NotFound'),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
getEntity: {
|
|
144
|
+
summary: 'Get an entity',
|
|
145
|
+
responses: { 200: ok('Entity', ref('Entity')), ...errors('Unauthorized', 'NotFound') },
|
|
146
|
+
},
|
|
147
|
+
deleteEntity: {
|
|
148
|
+
summary: 'Delete an entity',
|
|
149
|
+
description: 'Refused with 409 ENTITY_IN_USE while integrations use it. The credential stays.',
|
|
150
|
+
responses: { 204: { description: 'Deleted' }, ...errors('Unauthorized', 'NotFound', 'Conflict') },
|
|
151
|
+
},
|
|
152
|
+
testEntityAuth: {
|
|
153
|
+
summary: "Test an entity's stored credential",
|
|
154
|
+
responses: { 200: ok('Result', ref('AuthTestResult')), ...errors('Unauthorized', 'NotFound') },
|
|
155
|
+
},
|
|
156
|
+
getEntityOptions: {
|
|
157
|
+
summary: "Get an entity's options",
|
|
158
|
+
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
159
|
+
},
|
|
160
|
+
refreshEntityOptions: {
|
|
161
|
+
summary: "Refresh an entity's options",
|
|
162
|
+
requestBody: body({ type: 'object', additionalProperties: true }, false),
|
|
163
|
+
responses: { 200: ok('Options', anyJson), ...errors('Unauthorized', 'NotFound') },
|
|
164
|
+
},
|
|
165
|
+
proxyEntityRequest: {
|
|
166
|
+
summary: "Call the entity's API through Frigg (beta)",
|
|
167
|
+
description:
|
|
168
|
+
"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. ADR-056 §3.5: the caller needs use access to the entity in the active organization (an entity they cannot see there is 404), and the entity's scope must be in managementApi.proxy.scopes (default individual only; 403 PROXY_NOT_ALLOWED with details.reason 'scope', or 'access' for view-only access). The HTTP status follows the upstream status; upstream 5xx is 502 UPSTREAM_ERROR, 429 keeps Retry-After.",
|
|
169
|
+
requestBody: body(ref('ProxyRequest')),
|
|
170
|
+
responses: {
|
|
171
|
+
200: ok('Upstream answer (the status mirrors the upstream status)', ref('ProxyResponse')),
|
|
172
|
+
400: ok('INVALID_PROXY_REQUEST', ref('ProxyError')),
|
|
173
|
+
401: ok('INVALID_CREDENTIALS or unauthenticated', ref('ProxyError')),
|
|
174
|
+
403: ok("PROXY_NOT_ALLOWED: not in the allow-list, or details.reason 'scope' / 'access'", ref('ProxyError')),
|
|
175
|
+
404: ok('Entity not found, or not visible in the active organization', ref('Error')),
|
|
176
|
+
413: ok('PAYLOAD_TOO_LARGE', ref('ProxyError')),
|
|
177
|
+
415: ok('UNSUPPORTED_MEDIA_TYPE', ref('ProxyError')),
|
|
178
|
+
429: ok('RATE_LIMITED (Retry-After passed through)', ref('ProxyError')),
|
|
179
|
+
502: ok('UPSTREAM_ERROR, NETWORK_ERROR or RESPONSE_TOO_LARGE', ref('ProxyError')),
|
|
180
|
+
504: ok('TIMEOUT', ref('ProxyError')),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// Credentials
|
|
185
|
+
listCredentials: {
|
|
186
|
+
summary: "List the caller's credentials (masked)",
|
|
187
|
+
description:
|
|
188
|
+
'A credential is listed through the entities that use it that the caller may see, so each organization lists only its own connections; one no entity uses is listed to its owner. Each carries `scope`, `owner`, `organizationId` and `access`, taken from who owns the credential.',
|
|
189
|
+
parameters: listFilters,
|
|
190
|
+
responses: { 200: ok('Credentials', ref('ListCredentialsResponse')), ...errors('Unauthorized') },
|
|
191
|
+
},
|
|
192
|
+
getCredential: {
|
|
193
|
+
summary: 'Get a credential (masked)',
|
|
194
|
+
responses: { 200: ok('Credential', ref('Credential')), ...errors('Unauthorized', 'NotFound') },
|
|
195
|
+
},
|
|
196
|
+
deleteCredential: {
|
|
197
|
+
summary: 'Delete a credential',
|
|
198
|
+
description: 'Entities that used it stay, with no credential, until re-authorized.',
|
|
199
|
+
responses: { 204: { description: 'Deleted' }, ...errors('Unauthorized', 'NotFound') },
|
|
200
|
+
},
|
|
201
|
+
getCredentialReauthorizeRequirements: {
|
|
202
|
+
summary: 'Start or continue re-authorizing a credential',
|
|
203
|
+
parameters: stepQuery,
|
|
204
|
+
responses: {
|
|
205
|
+
200: ok('Requirements', ref('AuthorizationRequirements')),
|
|
206
|
+
...errors('BadRequest', 'Unauthorized', 'NotFound', 'Conflict'),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
reauthorizeCredential: {
|
|
210
|
+
summary: 'Submit a re-authorization step',
|
|
211
|
+
requestBody: body(ref('ReauthorizeRequest')),
|
|
212
|
+
responses: {
|
|
213
|
+
200: ok('Complete or pending', ref('AuthorizeResponse')),
|
|
214
|
+
...errors('BadRequest', 'Unauthorized', 'NotFound', 'Conflict', 'BadGateway'),
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
// Authorization
|
|
219
|
+
getAuthorizationRequirements: {
|
|
220
|
+
summary: 'Start or continue an authorization',
|
|
221
|
+
description:
|
|
222
|
+
'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.',
|
|
223
|
+
parameters: [
|
|
224
|
+
query('entityType', 'Module name', { type: 'string' }, true),
|
|
225
|
+
...stepQuery,
|
|
226
|
+
query('state', 'OAuth state forwarded to the provider'),
|
|
227
|
+
],
|
|
228
|
+
responses: {
|
|
229
|
+
200: ok('Requirements', ref('AuthorizationRequirements')),
|
|
230
|
+
...errors('BadRequest', 'Unauthorized', 'NotFound', 'Conflict'),
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
authorize: {
|
|
234
|
+
summary: 'Submit an authorization step',
|
|
235
|
+
description:
|
|
236
|
+
'Answers { status: "complete", entity, credential } or { status: "pending", step, totalSteps, sessionId, requirements }.',
|
|
237
|
+
requestBody: body(ref('AuthorizeRequest')),
|
|
238
|
+
responses: {
|
|
239
|
+
200: ok('Complete or pending', ref('AuthorizeResponse')),
|
|
240
|
+
...errors('BadRequest', 'Unauthorized', 'NotFound', 'Conflict', 'BadGateway'),
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const META_OPERATIONS = {
|
|
246
|
+
getApiMeta: {
|
|
247
|
+
summary: 'Supported API versions and capabilities',
|
|
248
|
+
description: 'No authentication, no database. The core version is added for admin callers only.',
|
|
249
|
+
security: [],
|
|
250
|
+
responses: { 200: ok('Discovery document', ref('ApiMeta')) },
|
|
251
|
+
},
|
|
252
|
+
getOpenApiV1: {
|
|
253
|
+
summary: 'OpenAPI document for Management API v1 (deprecated)',
|
|
254
|
+
security: [],
|
|
255
|
+
responses: { 200: ok('OpenAPI 3 document', { type: 'object' }) },
|
|
256
|
+
},
|
|
257
|
+
getOpenApiV2: {
|
|
258
|
+
summary: 'OpenAPI document for Management API v2',
|
|
259
|
+
security: [],
|
|
260
|
+
responses: { 200: ok('OpenAPI 3 document', { type: 'object' }) },
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
module.exports = { V2_OPERATIONS, META_OPERATIONS };
|
|
@@ -0,0 +1,411 @@
|
|
|
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
|
+
/**
|
|
54
|
+
* Functions (devtools names) that serve registered routes. `queueWorker` is
|
|
55
|
+
* the HTTP queue worker of hosts that deliver queue messages over HTTP
|
|
56
|
+
* (provider-netlify's worker-background); AWS delivers through SQS.
|
|
57
|
+
*/
|
|
58
|
+
const FUNCTIONS = Object.freeze({
|
|
59
|
+
AUTH: 'auth',
|
|
60
|
+
HEALTH: 'health',
|
|
61
|
+
QUEUE_WORKER: 'queueWorker',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const route = (version, fn) => (definition) =>
|
|
65
|
+
Object.freeze({ version, function: fn, ...definition });
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The v1 routes registered by createIntegrationRouter(). `ANY` marks routes
|
|
69
|
+
* registered with `router.all()`. Frozen: no behaviour change except the
|
|
70
|
+
* deprecation headers.
|
|
71
|
+
*/
|
|
72
|
+
const V1_ROUTES = Object.freeze(
|
|
73
|
+
[
|
|
74
|
+
{ method: 'GET', path: '/api/integrations' },
|
|
75
|
+
{ method: 'POST', path: '/api/integrations' },
|
|
76
|
+
{ method: 'PATCH', path: '/api/integrations/:integrationId' },
|
|
77
|
+
{ method: 'DELETE', path: '/api/integrations/:integrationId' },
|
|
78
|
+
{
|
|
79
|
+
method: 'GET',
|
|
80
|
+
path: '/api/integrations/:integrationId/config/options',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
method: 'POST',
|
|
84
|
+
path: '/api/integrations/:integrationId/config/options/refresh',
|
|
85
|
+
},
|
|
86
|
+
{ method: 'ANY', path: '/api/integrations/:integrationId/actions' },
|
|
87
|
+
{
|
|
88
|
+
method: 'ANY',
|
|
89
|
+
path: '/api/integrations/:integrationId/actions/:actionId/options',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
method: 'POST',
|
|
93
|
+
path: '/api/integrations/:integrationId/actions/:actionId/options/refresh',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
method: 'POST',
|
|
97
|
+
path: '/api/integrations/:integrationId/actions/:actionId',
|
|
98
|
+
},
|
|
99
|
+
{ method: 'GET', path: '/api/integrations/:integrationId' },
|
|
100
|
+
{ method: 'GET', path: '/api/integrations/:integrationId/test-auth' },
|
|
101
|
+
{ method: 'GET', path: '/api/authorize' },
|
|
102
|
+
{ method: 'POST', path: '/api/authorize' },
|
|
103
|
+
{ method: 'POST', path: '/api/entity' },
|
|
104
|
+
{ method: 'GET', path: '/api/entity/options/:credentialId' },
|
|
105
|
+
{ method: 'GET', path: '/api/entities/:entityId/test-auth' },
|
|
106
|
+
{ method: 'GET', path: '/api/entities/:entityId' },
|
|
107
|
+
{ method: 'POST', path: '/api/entities/:entityId/options' },
|
|
108
|
+
{ method: 'POST', path: '/api/entities/:entityId/options/refresh' },
|
|
109
|
+
].map(route('1', FUNCTIONS.AUTH))
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The v2 routes. Order matters: Express matches in this order, so literal
|
|
114
|
+
* segments (`/options`, `/types`) come before `/:id` siblings.
|
|
115
|
+
*
|
|
116
|
+
* - `operationId` keys the handler in management-api/v2 and the OpenAPI
|
|
117
|
+
* operation.
|
|
118
|
+
* - `stability: 'beta'` marks routes that may still change in a core minor
|
|
119
|
+
* (ADR-053 §2); it is published as `x-frigg-stability`.
|
|
120
|
+
* - `capability` names the /api/meta capability a route belongs to.
|
|
121
|
+
*/
|
|
122
|
+
const V2_ROUTES = Object.freeze(
|
|
123
|
+
[
|
|
124
|
+
// Integrations
|
|
125
|
+
{ operationId: 'listIntegrations', method: 'GET', path: '/api/v2/integrations', tag: 'Integrations' },
|
|
126
|
+
{ operationId: 'listIntegrationOptions', method: 'GET', path: '/api/v2/integrations/options', tag: 'Integrations' },
|
|
127
|
+
{ operationId: 'createIntegration', method: 'POST', path: '/api/v2/integrations', tag: 'Integrations' },
|
|
128
|
+
{ operationId: 'getIntegration', method: 'GET', path: '/api/v2/integrations/:integrationId', tag: 'Integrations' },
|
|
129
|
+
{ operationId: 'updateIntegration', method: 'PATCH', path: '/api/v2/integrations/:integrationId', tag: 'Integrations' },
|
|
130
|
+
{ operationId: 'deleteIntegration', method: 'DELETE', path: '/api/v2/integrations/:integrationId', tag: 'Integrations' },
|
|
131
|
+
{ operationId: 'getIntegrationConfigOptions', method: 'GET', path: '/api/v2/integrations/:integrationId/config/options', tag: 'Integrations' },
|
|
132
|
+
{ operationId: 'refreshIntegrationConfigOptions', method: 'POST', path: '/api/v2/integrations/:integrationId/config/options/refresh', tag: 'Integrations' },
|
|
133
|
+
{ operationId: 'listIntegrationActions', method: 'GET', path: '/api/v2/integrations/:integrationId/actions', tag: 'Integrations' },
|
|
134
|
+
{ operationId: 'getIntegrationActionOptions', method: 'POST', path: '/api/v2/integrations/:integrationId/actions/:actionId/options', tag: 'Integrations' },
|
|
135
|
+
{ operationId: 'refreshIntegrationActionOptions', method: 'POST', path: '/api/v2/integrations/:integrationId/actions/:actionId/options/refresh', tag: 'Integrations' },
|
|
136
|
+
{ operationId: 'runIntegrationAction', method: 'POST', path: '/api/v2/integrations/:integrationId/actions/:actionId', tag: 'Integrations' },
|
|
137
|
+
{ operationId: 'testIntegrationAuth', method: 'GET', path: '/api/v2/integrations/:integrationId/test-auth', tag: 'Integrations' },
|
|
138
|
+
|
|
139
|
+
// Entities
|
|
140
|
+
{ operationId: 'listEntities', method: 'GET', path: '/api/v2/entities', tag: 'Entities' },
|
|
141
|
+
{ operationId: 'listEntityTypes', method: 'GET', path: '/api/v2/entities/types', tag: 'Entities' },
|
|
142
|
+
{ operationId: 'getEntityType', method: 'GET', path: '/api/v2/entities/types/:entityType', tag: 'Entities' },
|
|
143
|
+
{ operationId: 'getEntityTypeRequirements', method: 'GET', path: '/api/v2/entities/types/:entityType/requirements', tag: 'Entities' },
|
|
144
|
+
{ operationId: 'getEntity', method: 'GET', path: '/api/v2/entities/:entityId', tag: 'Entities' },
|
|
145
|
+
{ operationId: 'deleteEntity', method: 'DELETE', path: '/api/v2/entities/:entityId', tag: 'Entities' },
|
|
146
|
+
{ operationId: 'testEntityAuth', method: 'GET', path: '/api/v2/entities/:entityId/test-auth', tag: 'Entities' },
|
|
147
|
+
{ operationId: 'getEntityOptions', method: 'GET', path: '/api/v2/entities/:entityId/options', tag: 'Entities' },
|
|
148
|
+
{ operationId: 'refreshEntityOptions', method: 'POST', path: '/api/v2/entities/:entityId/options/refresh', tag: 'Entities' },
|
|
149
|
+
{ operationId: 'proxyEntityRequest', method: 'POST', path: '/api/v2/entities/:entityId/proxy', tag: 'Entities', stability: 'beta', capability: 'entityProxy' },
|
|
150
|
+
|
|
151
|
+
// Credentials
|
|
152
|
+
{ operationId: 'listCredentials', method: 'GET', path: '/api/v2/credentials', tag: 'Credentials', capability: 'credentials' },
|
|
153
|
+
{ operationId: 'getCredential', method: 'GET', path: '/api/v2/credentials/:credentialId', tag: 'Credentials', capability: 'credentials' },
|
|
154
|
+
{ operationId: 'deleteCredential', method: 'DELETE', path: '/api/v2/credentials/:credentialId', tag: 'Credentials', capability: 'credentials' },
|
|
155
|
+
{ operationId: 'getCredentialReauthorizeRequirements', method: 'GET', path: '/api/v2/credentials/:credentialId/reauthorize', tag: 'Credentials', capability: 'credentials' },
|
|
156
|
+
{ operationId: 'reauthorizeCredential', method: 'POST', path: '/api/v2/credentials/:credentialId/reauthorize', tag: 'Credentials', capability: 'credentials' },
|
|
157
|
+
|
|
158
|
+
// Authorization (multi-step)
|
|
159
|
+
{ operationId: 'getAuthorizationRequirements', method: 'GET', path: '/api/v2/authorize', tag: 'Authorization', capability: 'multiStepAuthorize' },
|
|
160
|
+
{ operationId: 'authorize', method: 'POST', path: '/api/v2/authorize', tag: 'Authorization', capability: 'multiStepAuthorize' },
|
|
161
|
+
].map(route('2', FUNCTIONS.AUTH))
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
/** Version-neutral discovery routes, served without auth or database. */
|
|
165
|
+
const META_ROUTES = Object.freeze(
|
|
166
|
+
[
|
|
167
|
+
{ operationId: 'getApiMeta', method: 'GET', path: '/api/meta' },
|
|
168
|
+
...Object.keys(MANAGEMENT_API_VERSIONS).map((major) => ({
|
|
169
|
+
operationId: `getOpenApiV${major}`,
|
|
170
|
+
method: 'GET',
|
|
171
|
+
path: `/api/meta/openapi/v${major}.json`,
|
|
172
|
+
})),
|
|
173
|
+
].map(route(null, FUNCTIONS.HEALTH))
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Unversioned routes the auth Lambda serves next to the Management API
|
|
178
|
+
* (core/handlers/routers/auth.js). The OAuth redirect is the provider's
|
|
179
|
+
* callback target, so it is neither deprecated nor disabled with v1.
|
|
180
|
+
*/
|
|
181
|
+
const AUTH_EXTRA_ROUTES = Object.freeze(
|
|
182
|
+
[
|
|
183
|
+
{
|
|
184
|
+
operationId: 'oauthRedirect',
|
|
185
|
+
method: 'GET',
|
|
186
|
+
path: '/api/integrations/redirect/:appId',
|
|
187
|
+
},
|
|
188
|
+
].map(route(null, FUNCTIONS.AUTH))
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Unversioned routes the DB-free health Lambda serves next to /api/meta.
|
|
193
|
+
* GET /api/docs redirects to the current OpenAPI document; the docs
|
|
194
|
+
* themselves live under /api/meta (ADR-053).
|
|
195
|
+
*/
|
|
196
|
+
const DOCS_ROUTES = Object.freeze(
|
|
197
|
+
[
|
|
198
|
+
{
|
|
199
|
+
operationId: 'redirectToOpenApi',
|
|
200
|
+
method: 'GET',
|
|
201
|
+
path: '/api/docs',
|
|
202
|
+
},
|
|
203
|
+
].map(route(null, FUNCTIONS.HEALTH))
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Admin routes live under /admin (as core's /admin/db-migrate and the
|
|
208
|
+
* admin-scripts /admin/scripts do), never under the public /api prefix. A
|
|
209
|
+
* core extension (appDefinition.coreExtensions[].routes.path) mounts its admin
|
|
210
|
+
* API at /admin/<name>, served by the auth function behind the admin API key;
|
|
211
|
+
* these segments belong to other functions and cannot be taken.
|
|
212
|
+
*/
|
|
213
|
+
const ADMIN_ROUTE_PREFIX = '/admin/';
|
|
214
|
+
const RESERVED_ADMIN_SEGMENTS = Object.freeze(['scripts', 'db-migrate', 'queue']);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The queue delivery endpoint of hosts that deliver queue messages over HTTP
|
|
218
|
+
* (QStash posts here on Netlify). Every message must be signed: the worker
|
|
219
|
+
* verifies it before parsing anything. Not part of the public /api surface.
|
|
220
|
+
*/
|
|
221
|
+
const HTTP_QUEUE_ROUTES = Object.freeze(
|
|
222
|
+
[
|
|
223
|
+
{
|
|
224
|
+
operationId: 'deliverQueueMessage',
|
|
225
|
+
method: 'POST',
|
|
226
|
+
path: '/admin/queue',
|
|
227
|
+
},
|
|
228
|
+
].map(route(null, FUNCTIONS.QUEUE_WORKER))
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Why a core extension route path is not acceptable, or null when it is:
|
|
233
|
+
* `/admin/<segment>` (letters, digits, `-` and `_`, one or more segments), not
|
|
234
|
+
* a reserved segment.
|
|
235
|
+
*
|
|
236
|
+
* @param {*} path
|
|
237
|
+
* @returns {string|null}
|
|
238
|
+
*/
|
|
239
|
+
function coreExtensionRoutePathError(path) {
|
|
240
|
+
if (typeof path !== 'string' || !path.startsWith(ADMIN_ROUTE_PREFIX)) {
|
|
241
|
+
return `must start with ${ADMIN_ROUTE_PREFIX} (admin routes are not served under /api)`;
|
|
242
|
+
}
|
|
243
|
+
if (!/^\/admin(\/[A-Za-z0-9_-]+)+$/.test(path)) {
|
|
244
|
+
return 'must be /admin/<segment>[/<segment>...] using letters, digits, - and _';
|
|
245
|
+
}
|
|
246
|
+
const segment = path.split('/')[2];
|
|
247
|
+
if (RESERVED_ADMIN_SEGMENTS.includes(segment)) {
|
|
248
|
+
return `/admin/${segment} is reserved (${RESERVED_ADMIN_SEGMENTS.map((s) => `/admin/${s}`).join(', ')})`;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Route records for the core extensions' admin routes: each path and
|
|
255
|
+
* everything under it, on the auth function (core's auth router mounts the
|
|
256
|
+
* extension routes behind the admin API key).
|
|
257
|
+
*
|
|
258
|
+
* @param {Array<Object>} [coreExtensions] - appDefinition.coreExtensions (plain data;
|
|
259
|
+
* only `routes.path` is read)
|
|
260
|
+
* @returns {Array<Object>}
|
|
261
|
+
*/
|
|
262
|
+
function listCoreExtensionRoutes(coreExtensions = []) {
|
|
263
|
+
if (!Array.isArray(coreExtensions)) return [];
|
|
264
|
+
const out = [];
|
|
265
|
+
for (const ext of coreExtensions) {
|
|
266
|
+
const path = ext?.routes?.path;
|
|
267
|
+
if (typeof path !== 'string' || coreExtensionRoutePathError(path)) continue;
|
|
268
|
+
out.push(
|
|
269
|
+
Object.freeze({ version: null, function: FUNCTIONS.AUTH, method: 'ANY', path, extension: ext.name }),
|
|
270
|
+
Object.freeze({ version: null, function: FUNCTIONS.AUTH, method: 'ANY', path: `${path}/{proxy+}`, extension: ext.name })
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The v2 routes an app serves. Routes tied to an opt-in capability (the entity
|
|
278
|
+
* proxy, ADR-052) are left out unless the app enables it.
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} [options]
|
|
281
|
+
* @param {boolean} [options.proxy=false] - `managementApi.proxy.enable`
|
|
282
|
+
*/
|
|
283
|
+
function listV2Routes({ proxy = false } = {}) {
|
|
284
|
+
return V2_ROUTES.filter(
|
|
285
|
+
(record) => proxy || record.capability !== 'entityProxy'
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @param {Object} [options]
|
|
291
|
+
* @param {boolean} [options.v1=true] - false drops the v1 routes
|
|
292
|
+
* (`managementApi.v1: false` in the app definition).
|
|
293
|
+
* @param {boolean} [options.proxy=false] - true keeps the entity proxy route.
|
|
294
|
+
* @returns {Array<Object>} Every registered route record.
|
|
295
|
+
*/
|
|
296
|
+
function listRoutes({ v1 = true, proxy = false } = {}) {
|
|
297
|
+
return [
|
|
298
|
+
...(v1 ? V1_ROUTES : []),
|
|
299
|
+
...listV2Routes({ proxy }),
|
|
300
|
+
...META_ROUTES,
|
|
301
|
+
...AUTH_EXTRA_ROUTES,
|
|
302
|
+
...DOCS_ROUTES,
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** `/api/v2/entities/:entityId` -> `/api/v2/entities/{entityId}` */
|
|
307
|
+
function toGatewayPath(expressPath) {
|
|
308
|
+
return expressPath.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The resource root a route is grouped under for API Gateway:
|
|
313
|
+
* `/api/v2/<resource>` for v2, `/api/<resource>` for v1, `/api/meta` for meta.
|
|
314
|
+
*/
|
|
315
|
+
function resourceRoot(record) {
|
|
316
|
+
const segments = record.path.split('/').filter(Boolean);
|
|
317
|
+
const depth = record.version === '2' ? 3 : 2;
|
|
318
|
+
return `/${segments.slice(0, depth).join('/')}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Gateway routes derived from the registry: the Management API, its
|
|
323
|
+
* discovery and docs routes, the core extensions' admin routes and, for
|
|
324
|
+
* hosts that deliver queue messages over HTTP, the queue endpoint. Each
|
|
325
|
+
* Management API resource root gets an exact route when the root itself is
|
|
326
|
+
* registered and a greedy `{proxy+}` route when deeper paths are. The auth
|
|
327
|
+
* function uses `ANY` (as it always has, so CORS preflights reach Express);
|
|
328
|
+
* the health function only serves GET.
|
|
329
|
+
*
|
|
330
|
+
* devtools turns these into API Gateway HTTP API events; provider-netlify
|
|
331
|
+
* into netlify.toml redirects.
|
|
332
|
+
*
|
|
333
|
+
* @param {Object} [options]
|
|
334
|
+
* @param {boolean} [options.v1=true]
|
|
335
|
+
* @param {boolean} [options.proxy=false]
|
|
336
|
+
* @param {Array<Object>} [options.coreExtensions=[]] - appDefinition.coreExtensions
|
|
337
|
+
* @param {boolean} [options.httpQueue=false] - include the HTTP queue route
|
|
338
|
+
* @returns {Array<{function: string, method: string, path: string}>}
|
|
339
|
+
*/
|
|
340
|
+
function getGatewayRoutes({ v1 = true, proxy = false, coreExtensions = [], httpQueue = false } = {}) {
|
|
341
|
+
const out = [];
|
|
342
|
+
const seen = new Set();
|
|
343
|
+
const add = (fn, method, path) => {
|
|
344
|
+
const key = `${fn} ${method} ${path}`;
|
|
345
|
+
if (seen.has(key)) return;
|
|
346
|
+
seen.add(key);
|
|
347
|
+
out.push({ function: fn, method, path });
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const grouped = [
|
|
351
|
+
...(v1 ? V1_ROUTES : []),
|
|
352
|
+
...listV2Routes({ proxy }),
|
|
353
|
+
...META_ROUTES,
|
|
354
|
+
];
|
|
355
|
+
const greedyRoots = new Set();
|
|
356
|
+
for (const record of grouped) {
|
|
357
|
+
const root = resourceRoot(record);
|
|
358
|
+
const method = record.function === FUNCTIONS.AUTH ? 'ANY' : 'GET';
|
|
359
|
+
if (record.path === root) {
|
|
360
|
+
add(record.function, method, root);
|
|
361
|
+
} else {
|
|
362
|
+
add(record.function, method, `${root}/{proxy+}`);
|
|
363
|
+
greedyRoots.add(`${record.function} ${root}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Extra routes get an exact route unless a greedy route on the same
|
|
368
|
+
// function already covers them.
|
|
369
|
+
for (const record of [...AUTH_EXTRA_ROUTES, ...DOCS_ROUTES]) {
|
|
370
|
+
const covered = greedyRoots.has(
|
|
371
|
+
`${record.function} ${resourceRoot(record)}`
|
|
372
|
+
);
|
|
373
|
+
if (!covered) {
|
|
374
|
+
add(record.function, record.method, toGatewayPath(record.path));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const record of listCoreExtensionRoutes(coreExtensions)) {
|
|
379
|
+
add(record.function, record.method, record.path);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (httpQueue) {
|
|
383
|
+
for (const record of HTTP_QUEUE_ROUTES) {
|
|
384
|
+
add(record.function, record.method, record.path);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
MANAGEMENT_API_VERSIONS,
|
|
393
|
+
PREFERRED_API_VERSION,
|
|
394
|
+
RESERVED_V2_SEGMENTS,
|
|
395
|
+
V1_MIGRATION_DOCS_URL,
|
|
396
|
+
FUNCTIONS,
|
|
397
|
+
V1_ROUTES,
|
|
398
|
+
V2_ROUTES,
|
|
399
|
+
META_ROUTES,
|
|
400
|
+
AUTH_EXTRA_ROUTES,
|
|
401
|
+
DOCS_ROUTES,
|
|
402
|
+
HTTP_QUEUE_ROUTES,
|
|
403
|
+
ADMIN_ROUTE_PREFIX,
|
|
404
|
+
RESERVED_ADMIN_SEGMENTS,
|
|
405
|
+
coreExtensionRoutePathError,
|
|
406
|
+
listCoreExtensionRoutes,
|
|
407
|
+
listRoutes,
|
|
408
|
+
listV2Routes,
|
|
409
|
+
getGatewayRoutes,
|
|
410
|
+
toGatewayPath,
|
|
411
|
+
};
|