@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.
Files changed (207) hide show
  1. package/CLAUDE.md +28 -40
  2. package/README.md +105 -0
  3. package/application/commands/entity-commands.js +60 -18
  4. package/application/commands/integration-commands.js +20 -73
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/application/index.js +6 -2
  8. package/core/Worker.js +31 -7
  9. package/core/create-handler.js +9 -0
  10. package/credential/repositories/credential-repository-documentdb.js +34 -0
  11. package/credential/repositories/credential-repository-interface.js +15 -0
  12. package/credential/repositories/credential-repository-mongo.js +23 -0
  13. package/credential/repositories/credential-repository-postgres.js +28 -0
  14. package/credential/use-cases/credential-operations-for-user.js +202 -0
  15. package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
  16. package/database/config.js +48 -24
  17. package/database/documentdb-encryption-service.js +9 -22
  18. package/database/documentdb-utils.js +2 -0
  19. package/database/encryption/README.md +76 -14
  20. package/database/encryption/documentdb-encryption-service.md +78 -1
  21. package/database/encryption/encryption-config.js +219 -0
  22. package/database/encryption/encryption-schema-registry.js +93 -12
  23. package/database/encryption/integration-mapping-encryption.js +1 -1
  24. package/database/organization-where.js +18 -0
  25. package/database/prisma.js +15 -38
  26. package/database/unique-violation.js +25 -0
  27. package/database/use-cases/check-encryption-health-use-case.js +43 -52
  28. package/errors/index.js +0 -4
  29. package/extensions/bootstrap-runner.js +43 -0
  30. package/extensions/extension-loader.js +187 -0
  31. package/extensions/index.js +41 -0
  32. package/extensions/initialize-app.js +123 -0
  33. package/extensions/route-mounter.js +103 -0
  34. package/extensions/schema-composer.js +188 -0
  35. package/generated/prisma-mongodb/edge.js +43 -4
  36. package/generated/prisma-mongodb/index-browser.js +40 -1
  37. package/generated/prisma-mongodb/index.d.ts +4761 -133
  38. package/generated/prisma-mongodb/index.js +43 -4
  39. package/generated/prisma-mongodb/package.json +1 -1
  40. package/generated/prisma-mongodb/schema.prisma +92 -0
  41. package/generated/prisma-mongodb/wasm.js +43 -4
  42. package/generated/prisma-postgresql/edge.js +43 -4
  43. package/generated/prisma-postgresql/index-browser.js +40 -1
  44. package/generated/prisma-postgresql/index.d.ts +4961 -28
  45. package/generated/prisma-postgresql/index.js +43 -4
  46. package/generated/prisma-postgresql/package.json +1 -1
  47. package/generated/prisma-postgresql/schema.prisma +87 -0
  48. package/generated/prisma-postgresql/wasm.js +43 -4
  49. package/handlers/WEBHOOKS.md +0 -29
  50. package/handlers/app-definition-loader.js +121 -19
  51. package/handlers/app-handler-helpers.js +25 -3
  52. package/handlers/backend-utils.js +31 -25
  53. package/handlers/middleware/admin-auth.js +14 -1
  54. package/handlers/routers/auth.js +46 -10
  55. package/handlers/routers/health.js +13 -4
  56. package/handlers/routers/integration-defined-routers.js +144 -92
  57. package/handlers/routers/integration-webhook-routers.js +102 -64
  58. package/handlers/workers/integration-defined-workers.js +5 -2
  59. package/index.js +65 -3
  60. package/infrastructure/scheduler/index.js +19 -6
  61. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  62. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  63. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  64. package/integrations/index.js +1 -0
  65. package/integrations/integration-base.js +0 -29
  66. package/integrations/integration-router.js +131 -96
  67. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  68. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  71. package/integrations/repositories/integration-mapping-repository.js +0 -29
  72. package/integrations/repositories/integration-not-found.js +14 -0
  73. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  74. package/integrations/repositories/integration-repository-interface.js +17 -31
  75. package/integrations/repositories/integration-repository-mongo.js +60 -129
  76. package/integrations/repositories/integration-repository-postgres.js +62 -134
  77. package/integrations/repositories/process-repository-mongo.js +1 -1
  78. package/integrations/schedule-config.js +80 -0
  79. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  80. package/integrations/use-cases/create-integration.js +72 -75
  81. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  82. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  83. package/integrations/use-cases/index.js +7 -1
  84. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  85. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  86. package/integrations/utils/map-integration-dto.js +43 -14
  87. package/management-api/create-management-api-router.js +49 -0
  88. package/management-api/error-response.js +116 -0
  89. package/management-api/management-api-config.js +151 -0
  90. package/management-api/meta-router.js +76 -0
  91. package/management-api/openapi/build-openapi.js +144 -0
  92. package/management-api/openapi/components.js +408 -0
  93. package/management-api/openapi/v1-operations.js +83 -0
  94. package/management-api/openapi/v2-operations.js +264 -0
  95. package/management-api/route-registry.js +411 -0
  96. package/management-api/test-support/http-client.js +39 -0
  97. package/management-api/use-cases/get-management-api-meta.js +57 -0
  98. package/management-api/v1-deprecation.js +120 -0
  99. package/management-api/v2/create-v2-router.js +85 -0
  100. package/management-api/v2/dependencies.js +281 -0
  101. package/management-api/v2/dto.js +143 -0
  102. package/management-api/v2/handlers/authorization.js +94 -0
  103. package/management-api/v2/handlers/credentials.js +33 -0
  104. package/management-api/v2/handlers/entities.js +77 -0
  105. package/management-api/v2/handlers/integrations.js +128 -0
  106. package/management-api/v2/handlers/proxy.js +34 -0
  107. package/management-api/v2/request.js +85 -0
  108. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  109. package/modules/domain/authorization-session.js +144 -0
  110. package/modules/entity-scope.js +15 -0
  111. package/modules/entity-type-registry.js +311 -0
  112. package/modules/index.js +14 -0
  113. package/modules/module-factory.js +12 -0
  114. package/modules/module.js +53 -2
  115. package/modules/proxy/proxy-errors.js +20 -0
  116. package/modules/proxy/proxy-path.js +179 -0
  117. package/modules/proxy/proxy-policy.js +248 -0
  118. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  119. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  120. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  121. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  122. package/modules/repositories/entity-not-found.js +18 -0
  123. package/modules/repositories/module-repository-documentdb.js +51 -6
  124. package/modules/repositories/module-repository-interface.js +19 -2
  125. package/modules/repositories/module-repository-mongo.js +67 -74
  126. package/modules/repositories/module-repository-postgres.js +66 -74
  127. package/modules/repositories/module-repository.js +2 -1
  128. package/modules/requester/proxy-request.js +244 -0
  129. package/modules/requester/requester.js +27 -2
  130. package/modules/scope-config.js +192 -0
  131. package/modules/scope-policy.js +112 -0
  132. package/modules/scope-resolution.js +288 -0
  133. package/modules/scope-visibility.js +247 -0
  134. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  135. package/modules/tests/doubles/ownership-matching.js +18 -0
  136. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  137. package/modules/use-cases/create-entity-from-credential.js +113 -0
  138. package/modules/use-cases/entity-operations-for-user.js +162 -0
  139. package/modules/use-cases/entity-types.js +139 -0
  140. package/modules/use-cases/execute-entity-proxy-request.js +286 -0
  141. package/modules/use-cases/get-authorization-requirements.js +100 -0
  142. package/modules/use-cases/get-entities-for-user.js +26 -13
  143. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  144. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  145. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  146. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  147. package/modules/use-cases/get-module.js +10 -4
  148. package/modules/use-cases/manage-access.js +21 -0
  149. package/modules/use-cases/multi-step-authorization.js +329 -0
  150. package/modules/use-cases/process-authorization-callback.js +53 -19
  151. package/modules/use-cases/refresh-entity-options.js +10 -4
  152. package/modules/use-cases/test-module-auth.js +10 -4
  153. package/modules/utils/map-module-dto.js +3 -5
  154. package/package.json +6 -5
  155. package/prisma-mongodb/schema.prisma +92 -0
  156. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  157. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  158. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  159. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  160. package/prisma-postgresql/schema.prisma +87 -0
  161. package/providers/index.js +23 -0
  162. package/providers/resolve-provider.js +93 -0
  163. package/providers/runtime-detection.js +124 -0
  164. package/queues/index.js +25 -0
  165. package/queues/providers/index.js +11 -0
  166. package/queues/providers/netlify-background-provider.js +150 -0
  167. package/queues/providers/qstash-queue-provider.js +219 -0
  168. package/queues/providers/sqs-queue-provider.js +70 -0
  169. package/queues/queue-client-interface.js +55 -0
  170. package/queues/queue-message-auth.js +193 -0
  171. package/queues/queue-provider-factory.js +101 -0
  172. package/queues/queue-provider.js +65 -0
  173. package/queues/sqs-queue-client.js +81 -0
  174. package/types/index.d.ts +2 -0
  175. package/types/modules/index.d.ts +197 -0
  176. package/types/user/index.d.ts +134 -0
  177. package/user/adopter-jwt-config.js +177 -0
  178. package/user/adopter-jwt-errors.js +59 -0
  179. package/user/adopter-token-verifier.js +122 -0
  180. package/user/app-user-helpers.js +97 -0
  181. package/user/create-user-authentication.js +64 -0
  182. package/user/membership.js +51 -0
  183. package/user/repositories/organization-membership-record.js +19 -0
  184. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  185. package/user/repositories/organization-membership-repository-factory.js +42 -0
  186. package/user/repositories/organization-membership-repository-interface.js +57 -0
  187. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  188. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  189. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  190. package/user/repositories/user-repository-documentdb.js +27 -2
  191. package/user/repositories/user-repository-interface.js +18 -0
  192. package/user/repositories/user-repository-mongo.js +24 -0
  193. package/user/repositories/user-repository-postgres.js +18 -0
  194. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  195. package/user/tests/doubles/test-user-repository.js +1 -1
  196. package/user/use-cases/authenticate-user.js +7 -1
  197. package/user/use-cases/ensure-organization-membership.js +92 -0
  198. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  199. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  200. package/user/use-cases/get-user-from-bearer-token.js +2 -7
  201. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  202. package/user/user.js +64 -1
  203. package/errors/mapping-already-exists-error.js +0 -14
  204. package/integrations/use-cases/add-entities-to-integration.js +0 -92
  205. package/integrations/use-cases/remove-entities-from-integration.js +0 -70
  206. package/integrations/utils/assert-entity-ids-input.js +0 -42
  207. package/modules/utils/to-credential-id.js +0 -14
@@ -6,6 +6,12 @@ const bodyParser = require('body-parser');
6
6
  const cors = require('cors');
7
7
  const Boom = require('@hapi/boom');
8
8
  const serverlessHttp = require('serverless-http');
9
+ const {
10
+ API_VERSION_HEADER,
11
+ isVersionedManagementApiPath,
12
+ toBoom,
13
+ toV2ErrorBody,
14
+ } = require('../management-api/error-response');
9
15
 
10
16
  const log = getLogger('frigg.http');
11
17
 
@@ -26,11 +32,15 @@ const createApp = (applyMiddleware) => {
26
32
  if (applyMiddleware) applyMiddleware(app);
27
33
 
28
34
  // The express boundary: send the error response and log it one time.
35
+ // A Boom error keeps its status; other errors keep theirs only when they
36
+ // mark a 4xx safe to expose (body-parser's 400/413 via http-errors), so a
37
+ // malformed body is a 400, not a 500.
29
38
  app.use((err, req, res, next) => {
30
- const boomError = err.isBoom ? err : Boom.boomify(err);
39
+ const boomError = toBoom(err);
31
40
  const {
32
41
  output: { statusCode = 500 },
33
42
  } = boomError;
43
+ const versioned = isVersionedManagementApiPath(req.path);
34
44
 
35
45
  if (statusCode >= 500) {
36
46
  log.error('Request failed', {
@@ -39,7 +49,6 @@ const createApp = (applyMiddleware) => {
39
49
  invocation: summarizeExpressRequest(req),
40
50
  error: boomError,
41
51
  });
42
- res.status(statusCode).json({ error: 'Internal Server Error' });
43
52
  } else {
44
53
  // A client error needs no stack; the logger scrubs the reason.
45
54
  log.warn('Request rejected', {
@@ -47,8 +56,21 @@ const createApp = (applyMiddleware) => {
47
56
  statusCode,
48
57
  reason: boomError.message,
49
58
  });
50
- res.status(statusCode).json({ error: err.message });
51
59
  }
60
+
61
+ if (versioned) {
62
+ // Management API v2 / meta: { error: { code, message, details? } }.
63
+ if (req.path.startsWith('/api/v2')) {
64
+ res.set(API_VERSION_HEADER, '2');
65
+ }
66
+ return res.status(statusCode).json(toV2ErrorBody(boomError));
67
+ }
68
+
69
+ // v1 and other routes keep their historical body.
70
+ if (statusCode >= 500) {
71
+ return res.status(statusCode).json({ error: 'Internal Server Error' });
72
+ }
73
+ return res.status(statusCode).json({ error: err.message });
52
74
  });
53
75
 
54
76
  return app;
@@ -18,8 +18,9 @@ const {
18
18
  createModuleRepository,
19
19
  } = require('../modules/repositories/module-repository-factory');
20
20
  const {
21
- getModulesDefinitionFromIntegrationClasses,
22
- } = require('../integrations/utils/map-integration-dto');
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 } = loadAppDefinition();
67
+ const { integrations: integrationClasses, moduleDefinitions } =
68
+ loadAppDefinition();
80
69
 
81
70
  const { integrationRepository, moduleRepository } =
82
71
  initializeRepositories();
83
72
 
84
- const moduleFactory = createModuleFactoryWithDefinitions(
73
+ const moduleFactory = new ModuleFactory({
85
74
  moduleRepository,
86
- integrationClasses
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 (processId, integrationClass) => {
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 = createModuleFactoryWithDefinitions(moduleRepository, [
137
- integrationClass,
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
- const createQueueWorker = (integrationClass) => {
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 };
@@ -1,15 +1,51 @@
1
- const { createIntegrationRouter } = require('@friggframework/core');
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
- const router = createIntegrationRouter();
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
- router.route('/api/integrations/redirect/:appId').get((req, res) => {
7
- res.redirect(
8
- `${process.env.FRONTEND_URI}/redirect/${req.params.appId
9
- }?${new URLSearchParams(req.query)}`
10
- );
11
- });
20
+ function buildRouter() {
21
+ const built = express.Router();
22
+ built.use(createManagementApiRouter());
12
23
 
13
- const handler = createAppHandler('HTTP Event: Auth', router);
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
- module.exports = { handler };
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 = getModulesDefinitionFromIntegrationClasses(integrationClasses);
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
- //todo: this should be in a use case class
19
- for (const IntegrationClass of integrationClasses) {
20
- const router = Router();
21
- const basePath = `/api/${IntegrationClass.Definition.name}-integration`;
22
- // Track (method, path) tuples to fail fast on conflicts between Definition.routes,
23
- // extension routes, or two extensions claiming the same path.
24
- const claimedRoutes = new Map();
25
- const claim = (method, path, source) => {
26
- const key = routeKey(method, path);
27
- if (claimedRoutes.has(key)) {
28
- const prev = claimedRoutes.get(key);
29
- throw new Error(
30
- `Integration "${IntegrationClass.Definition.name}" route conflict: ` +
31
- `${key} declared by ${prev} and ${source}`
32
- );
33
- }
34
- claimedRoutes.set(key, source);
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
- console.log(`\n│ Configuring routes for ${IntegrationClass.Definition.name} Integration:`);
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
- const routes = IntegrationClass.Definition.routes || [];
40
- for (const routeDef of routes) {
41
- if (typeof routeDef === 'function') {
42
- router.use(basePath, routeDef(IntegrationClass));
43
- console.log(`│ ANY ${basePath}/* (function handler)`);
44
- } else if (routeDef instanceof express.Router) {
45
- router.use(basePath, routeDef);
46
- console.log(`│ ANY ${basePath}/* (express router)`);
47
- } else if (typeof routeDef === 'object') {
48
- claim(routeDef.method, routeDef.path, 'Definition.routes');
49
- router.use(
50
- basePath,
51
- loadRouterFromObject(IntegrationClass, routeDef)
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
- // Each extension binding gets its own namespaced handler (/{bindingName}),
60
- // so two modules' extensions can share a path like /webhooks without colliding.
61
- const bindingGroups = new Map();
62
- for (const extRoute of getExtensionRoutes(IntegrationClass)) {
63
- const namespacedPath = `/${extRoute.bindingName}${extRoute.path}`;
64
- claim(
65
- extRoute.method,
66
- namespacedPath,
67
- `extension "${extRoute.extensionName}" (binding "${extRoute.bindingName}")`
68
- );
69
- if (!bindingGroups.has(extRoute.bindingName)) {
70
- bindingGroups.set(extRoute.bindingName, {
71
- router: Router(),
72
- useDatabase: extRoute.useDatabase,
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
- console.log('│');
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
- handlers[`${IntegrationClass.Definition.name}`] = {
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
- `HTTP Event: ${IntegrationClass.Definition.name}`,
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 = { handlers };
165
+ module.exports = {
166
+ handlers,
167
+ buildIntegrationRouters,
168
+ createIntegrationDefinedRouter,
169
+ };