@friggframework/core 2.0.0--canary.663.8de669c.0 → 2.0.0--canary.686.821f8a3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +6 -1
- package/database/encryption/README.md +2 -0
- package/database/encryption/encryption-config.js +10 -31
- package/database/encryption/encryption-schema-registry.js +121 -24
- package/database/encryption/field-encryption-service.js +69 -5
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- package/database/organization-where.js +18 -0
- package/database/prisma.js +4 -2
- package/database/unique-violation.js +25 -0
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +187 -0
- package/extensions/index.js +41 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +43 -4
- package/generated/prisma-mongodb/index-browser.js +40 -1
- package/generated/prisma-mongodb/index.d.ts +4761 -133
- package/generated/prisma-mongodb/index.js +43 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +92 -0
- package/generated/prisma-mongodb/wasm.js +43 -4
- package/generated/prisma-postgresql/edge.js +43 -4
- package/generated/prisma-postgresql/index-browser.js +40 -1
- package/generated/prisma-postgresql/index.d.ts +4961 -28
- package/generated/prisma-postgresql/index.js +43 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +87 -0
- package/generated/prisma-postgresql/wasm.js +43 -4
- package/handlers/app-definition-loader.js +121 -19
- package/handlers/app-handler-helpers.js +25 -3
- package/handlers/backend-utils.js +31 -25
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +13 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +65 -1
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +1 -0
- package/integrations/integration-router.js +131 -96
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -3
- package/integrations/repositories/integration-repository-interface.js +17 -1
- package/integrations/repositories/integration-repository-mongo.js +59 -76
- package/integrations/repositories/integration-repository-postgres.js +62 -76
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +7 -1
- package/integrations/use-cases/integration-operations-for-user.js +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +76 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +408 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +411 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-errors.js +20 -0
- package/modules/proxy/proxy-path.js +179 -0
- package/modules/proxy/proxy-policy.js +248 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/requester/proxy-request.js +244 -0
- package/modules/requester/requester.js +27 -2
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +286 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +329 -0
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +92 -0
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +87 -0
- package/providers/index.js +23 -0
- package/providers/resolve-provider.js +93 -0
- package/providers/runtime-detection.js +124 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +219 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +193 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +2 -0
- package/types/modules/index.d.ts +197 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
|
@@ -18,8 +18,9 @@ const {
|
|
|
18
18
|
createModuleRepository,
|
|
19
19
|
} = require('../modules/repositories/module-repository-factory');
|
|
20
20
|
const {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
getEntityTypeRegistry,
|
|
22
|
+
getEntityTypeDefinitions,
|
|
23
|
+
} = require('../modules/entity-type-registry');
|
|
23
24
|
const { processNotFound } = require('../integrations/use-cases/process-errors');
|
|
24
25
|
|
|
25
26
|
const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
@@ -61,30 +62,18 @@ const initializeRepositories = () => {
|
|
|
61
62
|
return { processRepository, integrationRepository, moduleRepository };
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
const createModuleFactoryWithDefinitions = (
|
|
65
|
-
moduleRepository,
|
|
66
|
-
integrationClasses
|
|
67
|
-
) => {
|
|
68
|
-
const moduleDefinitions =
|
|
69
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses);
|
|
70
|
-
|
|
71
|
-
return new ModuleFactory({
|
|
72
|
-
moduleRepository,
|
|
73
|
-
moduleDefinitions,
|
|
74
|
-
});
|
|
75
|
-
};
|
|
76
|
-
|
|
77
65
|
const loadIntegrationForWebhook = async (integrationId) => {
|
|
78
66
|
const { loadAppDefinition } = require('./app-definition-loader');
|
|
79
|
-
const { integrations: integrationClasses } =
|
|
67
|
+
const { integrations: integrationClasses, moduleDefinitions } =
|
|
68
|
+
loadAppDefinition();
|
|
80
69
|
|
|
81
70
|
const { integrationRepository, moduleRepository } =
|
|
82
71
|
initializeRepositories();
|
|
83
72
|
|
|
84
|
-
const moduleFactory =
|
|
73
|
+
const moduleFactory = new ModuleFactory({
|
|
85
74
|
moduleRepository,
|
|
86
|
-
|
|
87
|
-
);
|
|
75
|
+
moduleDefinitions,
|
|
76
|
+
});
|
|
88
77
|
|
|
89
78
|
const getIntegrationInstance = new GetIntegrationInstance({
|
|
90
79
|
integrationRepository,
|
|
@@ -129,13 +118,23 @@ const integrationExists = async (integrationId) => {
|
|
|
129
118
|
}
|
|
130
119
|
};
|
|
131
120
|
|
|
132
|
-
const loadIntegrationForProcess = async (
|
|
121
|
+
const loadIntegrationForProcess = async (
|
|
122
|
+
processId,
|
|
123
|
+
integrationClass,
|
|
124
|
+
moduleDefinitions
|
|
125
|
+
) => {
|
|
133
126
|
const { processRepository, integrationRepository, moduleRepository } =
|
|
134
127
|
initializeRepositories();
|
|
135
128
|
|
|
136
|
-
const moduleFactory =
|
|
137
|
-
|
|
138
|
-
|
|
129
|
+
const moduleFactory = new ModuleFactory({
|
|
130
|
+
moduleRepository,
|
|
131
|
+
moduleDefinitions:
|
|
132
|
+
moduleDefinitions ??
|
|
133
|
+
// No app definition handed in: this integration's own modules only.
|
|
134
|
+
getEntityTypeDefinitions(
|
|
135
|
+
getEntityTypeRegistry({ integrations: [integrationClass] })
|
|
136
|
+
),
|
|
137
|
+
});
|
|
139
138
|
|
|
140
139
|
const getIntegrationInstance = new GetIntegrationInstance({
|
|
141
140
|
integrationRepository,
|
|
@@ -161,7 +160,13 @@ const loadIntegrationForProcess = async (processId, integrationClass) => {
|
|
|
161
160
|
return instance;
|
|
162
161
|
};
|
|
163
162
|
|
|
164
|
-
|
|
163
|
+
/**
|
|
164
|
+
* @param {Function} integrationClass
|
|
165
|
+
* @param {{moduleDefinitions?: Array<object>}} [options] - the app's offered
|
|
166
|
+
* module definitions (`loadAppDefinition().moduleDefinitions`). When omitted
|
|
167
|
+
* the worker hydrates with this integration's own modules only.
|
|
168
|
+
*/
|
|
169
|
+
const createQueueWorker = (integrationClass, { moduleDefinitions } = {}) => {
|
|
165
170
|
const integrationName = integrationClass.Definition.name;
|
|
166
171
|
|
|
167
172
|
class QueueWorker extends Worker {
|
|
@@ -183,7 +188,8 @@ const createQueueWorker = (integrationClass) => {
|
|
|
183
188
|
console.log(`[QueueWorker] hydrating by processId`, logCtx);
|
|
184
189
|
integrationInstance = await loadIntegrationForProcess(
|
|
185
190
|
params.data.processId,
|
|
186
|
-
integrationClass
|
|
191
|
+
integrationClass,
|
|
192
|
+
moduleDefinitions
|
|
187
193
|
);
|
|
188
194
|
console.log(`[QueueWorker] hydrated`, {
|
|
189
195
|
...logCtx,
|
|
@@ -29,6 +29,19 @@ function secretsMatch(provided, expected) {
|
|
|
29
29
|
return crypto.timingSafeEqual(providedHash, expectedHash);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* True when `provided` matches the configured ADMIN_API_KEY. False when no
|
|
34
|
+
* key is configured or none was provided. For callers that change a response
|
|
35
|
+
* for admins rather than reject everyone else (GET /api/meta).
|
|
36
|
+
* @param {string|undefined} provided
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
function isValidAdminApiKey(provided) {
|
|
40
|
+
const expectedKey = process.env.ADMIN_API_KEY;
|
|
41
|
+
if (!expectedKey || !provided) return false;
|
|
42
|
+
return secretsMatch(provided, expectedKey);
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
/**
|
|
33
46
|
* Validate admin API key from request header
|
|
34
47
|
* @param {import('express').Request} req
|
|
@@ -70,4 +83,4 @@ function validateAdminApiKey(req, res, next) {
|
|
|
70
83
|
next();
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
module.exports = { validateAdminApiKey };
|
|
86
|
+
module.exports = { validateAdminApiKey, isValidAdminApiKey };
|
package/handlers/routers/auth.js
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
|
-
const
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const {
|
|
3
|
+
createManagementApiRouter,
|
|
4
|
+
} = require('../../management-api/create-management-api-router');
|
|
5
|
+
const {
|
|
6
|
+
createExtensionRoutesRouter,
|
|
7
|
+
} = require('../../extensions/route-mounter');
|
|
2
8
|
const { createAppHandler } = require('./../app-handler-helpers');
|
|
3
9
|
|
|
4
|
-
|
|
10
|
+
// The Management API (ADR-053): v2 under /api/v2, the deprecated v1 at the
|
|
11
|
+
// unprefixed paths (or 410 when managementApi.v1 is false).
|
|
12
|
+
//
|
|
13
|
+
// Built on first access, not at require time (friggframework/frigg#545,
|
|
14
|
+
// c40eca49): a host that presets the app definition (setAppDefinition, as
|
|
15
|
+
// provider-netlify's function entry points do) can require this module
|
|
16
|
+
// before or after doing so.
|
|
17
|
+
let router;
|
|
18
|
+
let handler;
|
|
5
19
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}?${new URLSearchParams(req.query)}`
|
|
10
|
-
);
|
|
11
|
-
});
|
|
20
|
+
function buildRouter() {
|
|
21
|
+
const built = express.Router();
|
|
22
|
+
built.use(createManagementApiRouter());
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
// The OAuth provider's redirect target. Unversioned: it is neither part
|
|
25
|
+
// of v1 nor v2, so it is never deprecated or disabled.
|
|
26
|
+
built.route('/api/integrations/redirect/:appId').get((req, res) => {
|
|
27
|
+
res.redirect(
|
|
28
|
+
`${process.env.FRONTEND_URI}/redirect/${req.params.appId
|
|
29
|
+
}?${new URLSearchParams(req.query)}`
|
|
30
|
+
);
|
|
31
|
+
});
|
|
14
32
|
|
|
15
|
-
|
|
33
|
+
// Core extension routes (appDefinition.coreExtensions[].routes), each
|
|
34
|
+
// behind the admin API key.
|
|
35
|
+
built.use(createExtensionRoutesRouter());
|
|
36
|
+
|
|
37
|
+
return built;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
get router() {
|
|
42
|
+
if (!router) router = buildRouter();
|
|
43
|
+
return router;
|
|
44
|
+
},
|
|
45
|
+
get handler() {
|
|
46
|
+
if (!handler) {
|
|
47
|
+
handler = createAppHandler('HTTP Event: Auth', module.exports.router);
|
|
48
|
+
}
|
|
49
|
+
return handler;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -2,9 +2,6 @@ const { Router } = require('express');
|
|
|
2
2
|
const { createAppHandler } = require('./../app-handler-helpers');
|
|
3
3
|
const { loadAppDefinition } = require('./../app-definition-loader');
|
|
4
4
|
const { ModuleFactory } = require('../../modules/module-factory');
|
|
5
|
-
const {
|
|
6
|
-
getModulesDefinitionFromIntegrationClasses,
|
|
7
|
-
} = require('../../integrations/utils/map-integration-dto');
|
|
8
5
|
const {
|
|
9
6
|
createModuleRepository,
|
|
10
7
|
} = require('../../modules/repositories/module-repository-factory');
|
|
@@ -28,18 +25,25 @@ const {
|
|
|
28
25
|
CheckIntegrationsHealthUseCase,
|
|
29
26
|
} = require('../use-cases/check-integrations-health-use-case');
|
|
30
27
|
|
|
28
|
+
const {
|
|
29
|
+
createManagementApiMetaRouter,
|
|
30
|
+
createDocsRedirectRouter,
|
|
31
|
+
} = require('../../management-api/meta-router');
|
|
32
|
+
|
|
31
33
|
const router = Router();
|
|
32
34
|
const healthCheckRepository = createHealthCheckRepository({ prismaClient: prisma });
|
|
33
35
|
|
|
34
36
|
// Load integrations and create factories just like auth router does
|
|
35
37
|
// This verifies the system can properly load integrations
|
|
36
38
|
let moduleFactory, integrationClasses;
|
|
39
|
+
let appDefinition = {};
|
|
37
40
|
try {
|
|
38
41
|
const appDef = loadAppDefinition();
|
|
42
|
+
appDefinition = appDef;
|
|
39
43
|
integrationClasses = appDef.integrations || [];
|
|
40
44
|
|
|
41
45
|
const moduleRepository = createModuleRepository();
|
|
42
|
-
const moduleDefinitions =
|
|
46
|
+
const { moduleDefinitions } = appDef;
|
|
43
47
|
|
|
44
48
|
moduleFactory = new ModuleFactory({
|
|
45
49
|
moduleRepository,
|
|
@@ -85,6 +89,11 @@ const validateApiKey = (req, res, next) => {
|
|
|
85
89
|
next();
|
|
86
90
|
};
|
|
87
91
|
|
|
92
|
+
// Management API discovery (ADR-053 §4) and the /api/docs redirect to it are
|
|
93
|
+
// public and DB-free, so they are mounted ahead of the health API key check.
|
|
94
|
+
router.use(createManagementApiMetaRouter({ appDefinition }));
|
|
95
|
+
router.use(createDocsRedirectRouter());
|
|
96
|
+
|
|
88
97
|
router.use(validateApiKey);
|
|
89
98
|
|
|
90
99
|
// Helper to detect VPC configuration
|
|
@@ -1,117 +1,169 @@
|
|
|
1
1
|
const { createAppHandler } = require('./../app-handler-helpers');
|
|
2
|
-
const {
|
|
3
|
-
loadAppDefinition,
|
|
4
|
-
} = require('../app-definition-loader');
|
|
2
|
+
const { loadAppDefinition } = require('../app-definition-loader');
|
|
5
3
|
const express = require('express');
|
|
6
4
|
const { Router } = express;
|
|
7
5
|
const { loadRouterFromObject } = require('../backend-utils');
|
|
8
6
|
const { getExtensionRoutes } = require('../../integrations/extension');
|
|
9
7
|
|
|
10
|
-
const handlers = {};
|
|
11
|
-
const { integrations: integrationClasses } = loadAppDefinition();
|
|
12
|
-
|
|
13
8
|
const routeKey = (method, path) => `${(method || 'ANY').toUpperCase()} ${path}`;
|
|
14
9
|
|
|
15
10
|
// Serverless function keys must be alphanumeric; binding keys are developer-chosen.
|
|
16
11
|
const sanitizeBindingKey = (name) => String(name).replace(/[^A-Za-z0-9]/g, '');
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
for
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Builds the Express routers for every integration's Definition.routes and
|
|
15
|
+
* extension bindings. Pure: no handler wrapping, so the AWS handlers below
|
|
16
|
+
* and other hosts (provider-netlify's integration-routes function) mount the
|
|
17
|
+
* very same routers.
|
|
18
|
+
*
|
|
19
|
+
* @param {Array<Function>} integrationClasses
|
|
20
|
+
* @returns {Array<{ key: string, eventName: string, router: import('express').Router, useDatabase: boolean }>}
|
|
21
|
+
* One entry per integration, then one per extension binding (key
|
|
22
|
+
* `${name}__${binding}`), in declaration order.
|
|
23
|
+
*/
|
|
24
|
+
function buildIntegrationRouters(integrationClasses) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
const keys = new Set();
|
|
27
|
+
//todo: this should be in a use case class
|
|
28
|
+
for (const IntegrationClass of integrationClasses) {
|
|
29
|
+
const router = Router();
|
|
30
|
+
const basePath = `/api/${IntegrationClass.Definition.name}-integration`;
|
|
31
|
+
// Track (method, path) tuples to fail fast on conflicts between Definition.routes,
|
|
32
|
+
// extension routes, or two extensions claiming the same path.
|
|
33
|
+
const claimedRoutes = new Map();
|
|
34
|
+
const claim = (method, path, source) => {
|
|
35
|
+
const key = routeKey(method, path);
|
|
36
|
+
if (claimedRoutes.has(key)) {
|
|
37
|
+
const prev = claimedRoutes.get(key);
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Integration "${IntegrationClass.Definition.name}" route conflict: ` +
|
|
40
|
+
`${key} declared by ${prev} and ${source}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
claimedRoutes.set(key, source);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
console.log(
|
|
47
|
+
`\n│ Configuring routes for ${IntegrationClass.Definition.name} Integration:`
|
|
48
|
+
);
|
|
36
49
|
|
|
37
|
-
|
|
50
|
+
const routes = IntegrationClass.Definition.routes || [];
|
|
51
|
+
for (const routeDef of routes) {
|
|
52
|
+
if (typeof routeDef === 'function') {
|
|
53
|
+
router.use(basePath, routeDef(IntegrationClass));
|
|
54
|
+
console.log(`│ ANY ${basePath}/* (function handler)`);
|
|
55
|
+
} else if (routeDef instanceof express.Router) {
|
|
56
|
+
router.use(basePath, routeDef);
|
|
57
|
+
console.log(`│ ANY ${basePath}/* (express router)`);
|
|
58
|
+
} else if (typeof routeDef === 'object') {
|
|
59
|
+
claim(routeDef.method, routeDef.path, 'Definition.routes');
|
|
60
|
+
router.use(
|
|
61
|
+
basePath,
|
|
62
|
+
loadRouterFromObject(IntegrationClass, routeDef)
|
|
63
|
+
);
|
|
64
|
+
const method = (routeDef.method || 'ANY').toUpperCase();
|
|
65
|
+
const fullPath = `${basePath}${routeDef.path}`;
|
|
66
|
+
console.log(`│ ${method} ${fullPath}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
// Each extension binding gets its own namespaced handler (/{bindingName}),
|
|
71
|
+
// so two modules' extensions can share a path like /webhooks without colliding.
|
|
72
|
+
const bindingGroups = new Map();
|
|
73
|
+
for (const extRoute of getExtensionRoutes(IntegrationClass)) {
|
|
74
|
+
const namespacedPath = `/${extRoute.bindingName}${extRoute.path}`;
|
|
75
|
+
claim(
|
|
76
|
+
extRoute.method,
|
|
77
|
+
namespacedPath,
|
|
78
|
+
`extension "${extRoute.extensionName}" (binding "${extRoute.bindingName}")`
|
|
79
|
+
);
|
|
80
|
+
if (!bindingGroups.has(extRoute.bindingName)) {
|
|
81
|
+
bindingGroups.set(extRoute.bindingName, {
|
|
82
|
+
router: Router(),
|
|
83
|
+
useDatabase: extRoute.useDatabase,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const group = bindingGroups.get(extRoute.bindingName);
|
|
87
|
+
group.router.use(
|
|
88
|
+
`${basePath}/${extRoute.bindingName}`,
|
|
89
|
+
loadRouterFromObject(IntegrationClass, extRoute)
|
|
90
|
+
);
|
|
91
|
+
console.log(
|
|
92
|
+
`│ ${extRoute.method.toUpperCase()} ${basePath}/${
|
|
93
|
+
extRoute.bindingName
|
|
94
|
+
}${extRoute.path} (extension: ${
|
|
95
|
+
extRoute.extensionName
|
|
96
|
+
}, useDatabase: ${extRoute.useDatabase})`
|
|
52
97
|
);
|
|
53
|
-
const method = (routeDef.method || 'ANY').toUpperCase();
|
|
54
|
-
const fullPath = `${basePath}${routeDef.path}`;
|
|
55
|
-
console.log(`│ ${method} ${fullPath}`);
|
|
56
98
|
}
|
|
57
|
-
|
|
99
|
+
console.log('│');
|
|
58
100
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
101
|
+
entries.push({
|
|
102
|
+
key: `${IntegrationClass.Definition.name}`,
|
|
103
|
+
eventName: `HTTP Event: ${IntegrationClass.Definition.name}`,
|
|
104
|
+
router,
|
|
105
|
+
useDatabase: true,
|
|
106
|
+
});
|
|
107
|
+
keys.add(`${IntegrationClass.Definition.name}`);
|
|
108
|
+
|
|
109
|
+
for (const [bindingName, group] of bindingGroups) {
|
|
110
|
+
// Wire contract: integration-builder.js (devtools) derives the identical
|
|
111
|
+
// function key for the serverless config. Keep both in sync.
|
|
112
|
+
const fnKey = `${
|
|
113
|
+
IntegrationClass.Definition.name
|
|
114
|
+
}__${sanitizeBindingKey(bindingName)}`;
|
|
115
|
+
// Distinct binding keys can sanitize to the same fnKey — fail loud rather than overwrite.
|
|
116
|
+
if (keys.has(fnKey)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Integration "${IntegrationClass.Definition.name}" extension handler conflict: ` +
|
|
119
|
+
`binding "${bindingName}" sanitizes to "${fnKey}", which is already taken. ` +
|
|
120
|
+
`Use binding keys that are distinct after stripping non-alphanumeric characters.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
entries.push({
|
|
124
|
+
key: fnKey,
|
|
125
|
+
eventName: `HTTP Event: ${IntegrationClass.Definition.name} extension ${bindingName}`,
|
|
126
|
+
router: group.router,
|
|
127
|
+
useDatabase: group.useDatabase,
|
|
73
128
|
});
|
|
129
|
+
keys.add(fnKey);
|
|
74
130
|
}
|
|
75
|
-
const group = bindingGroups.get(extRoute.bindingName);
|
|
76
|
-
group.router.use(
|
|
77
|
-
`${basePath}/${extRoute.bindingName}`,
|
|
78
|
-
loadRouterFromObject(IntegrationClass, extRoute)
|
|
79
|
-
);
|
|
80
|
-
console.log(
|
|
81
|
-
`│ ${extRoute.method.toUpperCase()} ${basePath}/${extRoute.bindingName}${extRoute.path} (extension: ${extRoute.extensionName}, useDatabase: ${extRoute.useDatabase})`
|
|
82
|
-
);
|
|
83
131
|
}
|
|
84
|
-
|
|
132
|
+
return entries;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* One router serving every integration-defined route and extension binding,
|
|
137
|
+
* for hosts that run them all in one function (provider-netlify).
|
|
138
|
+
*
|
|
139
|
+
* @param {Array<Function>} [integrationClasses] - defaults to the app's.
|
|
140
|
+
* @returns {import('express').Router}
|
|
141
|
+
*/
|
|
142
|
+
function createIntegrationDefinedRouter(integrationClasses) {
|
|
143
|
+
const classes = integrationClasses || loadAppDefinition().integrations;
|
|
144
|
+
const combined = Router();
|
|
145
|
+
for (const entry of buildIntegrationRouters(classes)) {
|
|
146
|
+
combined.use(entry.router);
|
|
147
|
+
}
|
|
148
|
+
return combined;
|
|
149
|
+
}
|
|
85
150
|
|
|
86
|
-
|
|
151
|
+
// The AWS handlers (one Lambda per integration and per extension binding,
|
|
152
|
+
// see devtools integration-builder). Built at require time on purpose: a
|
|
153
|
+
// route conflict fails the cold start, not the first request.
|
|
154
|
+
const handlers = {};
|
|
155
|
+
for (const entry of buildIntegrationRouters(loadAppDefinition().integrations)) {
|
|
156
|
+
handlers[entry.key] = {
|
|
87
157
|
handler: createAppHandler(
|
|
88
|
-
|
|
89
|
-
router
|
|
158
|
+
entry.eventName,
|
|
159
|
+
entry.router,
|
|
160
|
+
entry.useDatabase
|
|
90
161
|
),
|
|
91
162
|
};
|
|
92
|
-
|
|
93
|
-
for (const [bindingName, group] of bindingGroups) {
|
|
94
|
-
// Wire contract: integration-builder.js (devtools) derives the identical
|
|
95
|
-
// function key for the serverless config. Keep both in sync.
|
|
96
|
-
const fnKey = `${IntegrationClass.Definition.name}__${sanitizeBindingKey(
|
|
97
|
-
bindingName
|
|
98
|
-
)}`;
|
|
99
|
-
// Distinct binding keys can sanitize to the same fnKey — fail loud rather than overwrite.
|
|
100
|
-
if (Object.prototype.hasOwnProperty.call(handlers, fnKey)) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
`Integration "${IntegrationClass.Definition.name}" extension handler conflict: ` +
|
|
103
|
-
`binding "${bindingName}" sanitizes to "${fnKey}", which is already taken. ` +
|
|
104
|
-
`Use binding keys that are distinct after stripping non-alphanumeric characters.`
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
handlers[fnKey] = {
|
|
108
|
-
handler: createAppHandler(
|
|
109
|
-
`HTTP Event: ${IntegrationClass.Definition.name} extension ${bindingName}`,
|
|
110
|
-
group.router,
|
|
111
|
-
group.useDatabase
|
|
112
|
-
),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
163
|
}
|
|
116
164
|
|
|
117
|
-
module.exports = {
|
|
165
|
+
module.exports = {
|
|
166
|
+
handlers,
|
|
167
|
+
buildIntegrationRouters,
|
|
168
|
+
createIntegrationDefinedRouter,
|
|
169
|
+
};
|