@friggframework/core 2.0.0--canary.679.a2bcc2f.0 → 2.0.0--canary.686.219e1e7.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 (143) hide show
  1. package/CLAUDE.md +28 -0
  2. package/README.md +105 -0
  3. package/application/commands/integration-commands.js +2 -2
  4. package/application/commands/scheduler-commands.js +11 -7
  5. package/application/commands/user-commands.js +17 -5
  6. package/core/Worker.js +31 -7
  7. package/core/create-handler.js +9 -0
  8. package/credential/use-cases/credential-operations-for-user.js +110 -25
  9. package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
  10. package/database/config.js +48 -24
  11. package/database/documentdb-utils.js +6 -1
  12. package/database/encryption/encryption-schema-registry.js +80 -12
  13. package/database/organization-where.js +18 -0
  14. package/database/prisma.js +4 -2
  15. package/database/unique-violation.js +25 -0
  16. package/extensions/bootstrap-runner.js +43 -0
  17. package/extensions/extension-loader.js +167 -0
  18. package/extensions/index.js +41 -0
  19. package/extensions/initialize-app.js +123 -0
  20. package/extensions/route-mounter.js +103 -0
  21. package/extensions/schema-composer.js +188 -0
  22. package/generated/prisma-mongodb/edge.js +27 -4
  23. package/generated/prisma-mongodb/index-browser.js +24 -1
  24. package/generated/prisma-mongodb/index.d.ts +3227 -47
  25. package/generated/prisma-mongodb/index.js +27 -4
  26. package/generated/prisma-mongodb/package.json +1 -1
  27. package/generated/prisma-mongodb/schema.prisma +65 -0
  28. package/generated/prisma-mongodb/wasm.js +27 -4
  29. package/generated/prisma-postgresql/edge.js +27 -4
  30. package/generated/prisma-postgresql/index-browser.js +24 -1
  31. package/generated/prisma-postgresql/index.d.ts +3519 -67
  32. package/generated/prisma-postgresql/index.js +27 -4
  33. package/generated/prisma-postgresql/package.json +1 -1
  34. package/generated/prisma-postgresql/schema.prisma +61 -0
  35. package/generated/prisma-postgresql/wasm.js +27 -4
  36. package/handlers/app-definition-loader.js +93 -12
  37. package/handlers/routers/auth.js +40 -12
  38. package/handlers/routers/integration-defined-routers.js +144 -92
  39. package/handlers/routers/integration-webhook-routers.js +102 -64
  40. package/index.js +58 -1
  41. package/infrastructure/scheduler/index.js +19 -6
  42. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  43. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  44. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  45. package/integrations/integration-router.js +115 -73
  46. package/integrations/repositories/integration-not-found.js +14 -0
  47. package/integrations/repositories/integration-repository-documentdb.js +35 -3
  48. package/integrations/repositories/integration-repository-interface.js +17 -1
  49. package/integrations/repositories/integration-repository-mongo.js +59 -76
  50. package/integrations/repositories/integration-repository-postgres.js +62 -76
  51. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  52. package/integrations/use-cases/create-integration.js +55 -20
  53. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  54. package/integrations/use-cases/index.js +5 -1
  55. package/integrations/use-cases/integration-operations-for-user.js +55 -24
  56. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  57. package/management-api/management-api-config.js +71 -2
  58. package/management-api/openapi/components.js +40 -0
  59. package/management-api/openapi/v2-operations.js +16 -4
  60. package/management-api/v2/dependencies.js +40 -15
  61. package/management-api/v2/dto.js +20 -0
  62. package/management-api/v2/handlers/authorization.js +1 -0
  63. package/management-api/v2/handlers/credentials.js +6 -2
  64. package/management-api/v2/handlers/entities.js +10 -7
  65. package/management-api/v2/handlers/integrations.js +6 -3
  66. package/management-api/v2/request.js +32 -1
  67. package/modules/entity-scope.js +15 -0
  68. package/modules/index.js +8 -0
  69. package/modules/module-factory.js +12 -0
  70. package/modules/module.js +44 -2
  71. package/modules/repositories/entity-not-found.js +18 -0
  72. package/modules/repositories/module-repository-documentdb.js +51 -6
  73. package/modules/repositories/module-repository-interface.js +19 -2
  74. package/modules/repositories/module-repository-mongo.js +67 -74
  75. package/modules/repositories/module-repository-postgres.js +66 -74
  76. package/modules/repositories/module-repository.js +2 -1
  77. package/modules/scope-config.js +192 -0
  78. package/modules/scope-policy.js +112 -0
  79. package/modules/scope-resolution.js +288 -0
  80. package/modules/scope-visibility.js +247 -0
  81. package/modules/tests/doubles/ownership-matching.js +18 -0
  82. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  83. package/modules/use-cases/create-entity-from-credential.js +113 -0
  84. package/modules/use-cases/entity-operations-for-user.js +50 -13
  85. package/modules/use-cases/entity-types.js +31 -7
  86. package/modules/use-cases/execute-entity-proxy-request.js +50 -3
  87. package/modules/use-cases/get-authorization-requirements.js +2 -2
  88. package/modules/use-cases/get-entities-for-user.js +26 -13
  89. package/modules/use-cases/get-entity-module-for-user.js +34 -7
  90. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  91. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  92. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  93. package/modules/use-cases/get-module.js +10 -4
  94. package/modules/use-cases/manage-access.js +21 -0
  95. package/modules/use-cases/multi-step-authorization.js +40 -13
  96. package/modules/use-cases/process-authorization-callback.js +53 -19
  97. package/modules/use-cases/refresh-entity-options.js +10 -4
  98. package/modules/use-cases/test-module-auth.js +10 -4
  99. package/package.json +6 -5
  100. package/prisma-mongodb/schema.prisma +65 -0
  101. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  102. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  103. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  104. package/prisma-postgresql/schema.prisma +61 -0
  105. package/providers/index.js +13 -0
  106. package/providers/resolve-provider.js +93 -0
  107. package/queues/index.js +25 -0
  108. package/queues/providers/index.js +11 -0
  109. package/queues/providers/netlify-background-provider.js +148 -0
  110. package/queues/providers/qstash-queue-provider.js +197 -0
  111. package/queues/providers/sqs-queue-provider.js +70 -0
  112. package/queues/queue-client-interface.js +55 -0
  113. package/queues/queue-message-auth.js +187 -0
  114. package/queues/queue-provider-factory.js +101 -0
  115. package/queues/queue-provider.js +62 -0
  116. package/queues/sqs-queue-client.js +81 -0
  117. package/types/index.d.ts +1 -0
  118. package/types/modules/index.d.ts +129 -0
  119. package/types/user/index.d.ts +134 -0
  120. package/user/adopter-jwt-config.js +177 -0
  121. package/user/adopter-jwt-errors.js +59 -0
  122. package/user/adopter-token-verifier.js +122 -0
  123. package/user/app-user-helpers.js +97 -0
  124. package/user/create-user-authentication.js +64 -0
  125. package/user/membership.js +51 -0
  126. package/user/repositories/organization-membership-record.js +19 -0
  127. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  128. package/user/repositories/organization-membership-repository-factory.js +42 -0
  129. package/user/repositories/organization-membership-repository-interface.js +57 -0
  130. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  131. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  132. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  133. package/user/repositories/user-repository-documentdb.js +27 -2
  134. package/user/repositories/user-repository-interface.js +18 -0
  135. package/user/repositories/user-repository-mongo.js +24 -0
  136. package/user/repositories/user-repository-postgres.js +18 -0
  137. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  138. package/user/use-cases/authenticate-user.js +7 -1
  139. package/user/use-cases/ensure-organization-membership.js +92 -0
  140. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  141. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  142. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  143. package/user/user.js +64 -1
@@ -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
+ };
@@ -6,82 +6,120 @@ const {
6
6
  IntegrationEventDispatcher,
7
7
  } = require('../integration-event-dispatcher');
8
8
 
9
- const handlers = {};
10
- const { integrations: integrationClasses } = loadAppDefinition();
11
-
12
9
  // `webhooks.received` is counted at the DB-connected ON_WEBHOOK dispatch (see
13
10
  // usage-rollup-subscriber), not in this DB-free receipt handler.
14
11
 
15
- for (const IntegrationClass of integrationClasses) {
16
- const webhookConfig = IntegrationClass.Definition.webhooks;
12
+ /**
13
+ * Builds the webhook receiver router for every integration with webhooks
14
+ * enabled. Pure: no handler wrapping, so the AWS handlers below and other
15
+ * hosts (provider-netlify's webhooks function) mount the same routers.
16
+ *
17
+ * @param {Array<Function>} integrationClasses
18
+ * @returns {Array<{ key: string, eventName: string, router: import('express').Router }>}
19
+ */
20
+ function buildWebhookRouters(integrationClasses) {
21
+ const entries = [];
22
+ for (const IntegrationClass of integrationClasses) {
23
+ const webhookConfig = IntegrationClass.Definition.webhooks;
17
24
 
18
- // Skip if webhooks not enabled
19
- if (
20
- !webhookConfig ||
21
- (typeof webhookConfig === 'object' && !webhookConfig.enabled)
22
- ) {
23
- continue;
24
- }
25
+ // Skip if webhooks not enabled
26
+ if (
27
+ !webhookConfig ||
28
+ (typeof webhookConfig === 'object' && !webhookConfig.enabled)
29
+ ) {
30
+ continue;
31
+ }
25
32
 
26
- const router = Router();
27
- const basePath = `/api/${IntegrationClass.Definition.name}-integration/webhooks`;
33
+ const router = Router();
34
+ const basePath = `/api/${IntegrationClass.Definition.name}-integration/webhooks`;
28
35
 
29
- console.log(
30
- `\n│ Configuring webhook routes for ${IntegrationClass.Definition.name}:`
31
- );
36
+ console.log(
37
+ `\n│ Configuring webhook routes for ${IntegrationClass.Definition.name}:`
38
+ );
32
39
 
33
- // General webhook route (no integration ID)
34
- router.post(basePath, async (req, res, next) => {
35
- try {
36
- const integrationInstance = new IntegrationClass();
37
- const dispatcher = new IntegrationEventDispatcher(
38
- integrationInstance
39
- );
40
- await dispatcher.dispatchHttp({
41
- event: 'WEBHOOK_RECEIVED',
42
- req,
43
- res,
44
- next,
45
- });
46
- } catch (error) {
47
- next(error);
48
- }
49
- });
50
- console.log(`│ POST ${basePath}`);
40
+ // General webhook route (no integration ID)
41
+ router.post(basePath, async (req, res, next) => {
42
+ try {
43
+ const integrationInstance = new IntegrationClass();
44
+ const dispatcher = new IntegrationEventDispatcher(
45
+ integrationInstance
46
+ );
47
+ await dispatcher.dispatchHttp({
48
+ event: 'WEBHOOK_RECEIVED',
49
+ req,
50
+ res,
51
+ next,
52
+ });
53
+ } catch (error) {
54
+ next(error);
55
+ }
56
+ });
57
+ console.log(`│ POST ${basePath}`);
51
58
 
52
- // Integration-specific webhook route (with integration ID)
53
- router.post(`${basePath}/:integrationId`, async (req, res, next) => {
54
- try {
55
- const integrationInstance = new IntegrationClass();
56
- const dispatcher = new IntegrationEventDispatcher(
57
- integrationInstance
58
- );
59
- // Logs only: the id comes from an unauthenticated URL, so it must
60
- // not reach the telemetry context that drives usage attribution.
61
- await runInContext(
62
- { [LOGGER_SCOPE_KEY]: { integrationId: req.params.integrationId } },
63
- () =>
64
- dispatcher.dispatchHttp({
65
- event: 'WEBHOOK_RECEIVED',
66
- req,
67
- res,
68
- next,
69
- })
70
- );
71
- } catch (error) {
72
- next(error);
73
- }
74
- });
75
- console.log(`│ POST ${basePath}/:integrationId`);
76
- console.log('│');
59
+ // Integration-specific webhook route (with integration ID)
60
+ router.post(`${basePath}/:integrationId`, async (req, res, next) => {
61
+ try {
62
+ const integrationInstance = new IntegrationClass();
63
+ const dispatcher = new IntegrationEventDispatcher(
64
+ integrationInstance
65
+ );
66
+ // Logs only: the id comes from an unauthenticated URL, so it must
67
+ // not reach the telemetry context that drives usage attribution.
68
+ await runInContext(
69
+ {
70
+ [LOGGER_SCOPE_KEY]: {
71
+ integrationId: req.params.integrationId,
72
+ },
73
+ },
74
+ () =>
75
+ dispatcher.dispatchHttp({
76
+ event: 'WEBHOOK_RECEIVED',
77
+ req,
78
+ res,
79
+ next,
80
+ })
81
+ );
82
+ } catch (error) {
83
+ next(error);
84
+ }
85
+ });
86
+ console.log(`│ POST ${basePath}/:integrationId`);
87
+ console.log('│');
77
88
 
78
- handlers[`${IntegrationClass.Definition.name}Webhook`] = {
79
- handler: createAppHandler(
80
- `HTTP Event: ${IntegrationClass.Definition.name} Webhook`,
89
+ entries.push({
90
+ key: `${IntegrationClass.Definition.name}Webhook`,
91
+ eventName: `HTTP Event: ${IntegrationClass.Definition.name} Webhook`,
81
92
  router,
93
+ });
94
+ }
95
+ return entries;
96
+ }
97
+
98
+ /**
99
+ * One router serving every integration's webhook routes, for hosts that run
100
+ * them all in one function (provider-netlify).
101
+ *
102
+ * @param {Array<Function>} [integrationClasses] - defaults to the app's.
103
+ * @returns {import('express').Router}
104
+ */
105
+ function createWebhookRouter(integrationClasses) {
106
+ const classes = integrationClasses || loadAppDefinition().integrations;
107
+ const combined = Router();
108
+ for (const entry of buildWebhookRouters(classes)) {
109
+ combined.use(entry.router);
110
+ }
111
+ return combined;
112
+ }
113
+
114
+ const handlers = {};
115
+ for (const entry of buildWebhookRouters(loadAppDefinition().integrations)) {
116
+ handlers[entry.key] = {
117
+ handler: createAppHandler(
118
+ entry.eventName,
119
+ entry.router,
82
120
  false // shouldUseDatabase = false
83
121
  ),
84
122
  };
85
123
  }
86
124
 
87
- module.exports = { handlers };
125
+ module.exports = { handlers, buildWebhookRouters, createWebhookRouter };
package/index.js CHANGED
@@ -25,6 +25,14 @@ const {
25
25
  UserRepositoryMongo,
26
26
  UserRepositoryPostgres,
27
27
  } = require('./user/repositories/user-repository-factory');
28
+ const {
29
+ createOrganizationMembershipRepository,
30
+ OrganizationMembershipRepositoryMongo,
31
+ OrganizationMembershipRepositoryPostgres,
32
+ } = require('./user/repositories/organization-membership-repository-factory');
33
+ const {
34
+ EnsureOrganizationMembership,
35
+ } = require('./user/use-cases/ensure-organization-membership');
28
36
  const {
29
37
  GetUserFromXFriggHeaders,
30
38
  } = require('./user/use-cases/get-user-from-x-frigg-headers');
@@ -101,11 +109,33 @@ const {
101
109
  ModuleFactory,
102
110
  getEntityTypeRegistry,
103
111
  EntityRegistryError,
112
+ resolveScope,
113
+ listBindingScopes,
114
+ OwnershipConfigError,
104
115
  } = require('./modules/index');
116
+ const {
117
+ ManagementApiConfigError,
118
+ } = require('./management-api/management-api-config');
105
119
  const application = require('./application');
106
120
  const utils = require('./utils');
107
121
 
108
- const { QueuerUtil } = require('./queues');
122
+ const {
123
+ QueuerUtil,
124
+ QueueProvider,
125
+ QueueClientInterface,
126
+ createQueueProvider,
127
+ QUEUE_PROVIDERS,
128
+ } = require('./queues');
129
+ const {
130
+ resolveProvider,
131
+ determineProviderName,
132
+ providerPackageName,
133
+ KNOWN_PROVIDERS,
134
+ } = require('./providers');
135
+
136
+ // App-level extensions (appDefinition.extensions): Prisma models, encrypted
137
+ // fields, admin routes and bootstrap hooks (friggframework/frigg#545).
138
+ const extensions = require('./extensions');
109
139
 
110
140
  module.exports = {
111
141
  // assertions
@@ -132,6 +162,10 @@ module.exports = {
132
162
  createUserRepository,
133
163
  UserRepositoryMongo,
134
164
  UserRepositoryPostgres,
165
+ createOrganizationMembershipRepository,
166
+ OrganizationMembershipRepositoryMongo,
167
+ OrganizationMembershipRepositoryPostgres,
168
+ EnsureOrganizationMembership,
135
169
  GetUserFromXFriggHeaders,
136
170
  GetUserFromAdopterJwt,
137
171
  AuthenticateUser,
@@ -211,8 +245,31 @@ module.exports = {
211
245
  ModuleFactory,
212
246
  getEntityTypeRegistry,
213
247
  EntityRegistryError,
248
+ resolveScope,
249
+ listBindingScopes,
250
+ OwnershipConfigError,
251
+ ManagementApiConfigError,
214
252
  // queues
215
253
  QueuerUtil,
254
+ QueueProvider,
255
+ QueueClientInterface,
256
+ createQueueProvider,
257
+ QUEUE_PROVIDERS,
258
+
259
+ // provider plugins (appDefinition.provider)
260
+ resolveProvider,
261
+ determineProviderName,
262
+ providerPackageName,
263
+ KNOWN_PROVIDERS,
264
+
265
+ // app-level extensions
266
+ extensions,
267
+ loadExtensions: extensions.loadExtensions,
268
+ composeSchemas: extensions.composeSchemas,
269
+ mountExtensionRoutes: extensions.mountExtensionRoutes,
270
+ createExtensionRoutesRouter: extensions.createExtensionRoutesRouter,
271
+ runExtensionBootstraps: extensions.runExtensionBootstraps,
272
+ initializeApp: extensions.initializeApp,
216
273
 
217
274
  // utils
218
275
  ...utils,
@@ -5,13 +5,19 @@
5
5
  * Follows hexagonal architecture with interface + adapters pattern.
6
6
  *
7
7
  * Providers:
8
- * - eventbridge: AWS EventBridge Scheduler (production)
8
+ * - eventbridge: AWS EventBridge Scheduler (production on AWS)
9
+ * - netlify: poll-and-dispatch from a Netlify scheduled function
9
10
  * - mock: In-memory mock scheduler (local development)
11
+ *
12
+ * Adapters are lazy getters (friggframework/frigg#545, 1176a004): the
13
+ * EventBridge adapter requires @aws-sdk/client-scheduler, which is not a
14
+ * core dependency, and core/index -> application -> scheduler-commands ->
15
+ * this barrel runs on every require('@friggframework/core'). An eager
16
+ * require crashed any deployment without that SDK (e.g. a pruned Netlify
17
+ * function bundle).
10
18
  */
11
19
 
12
20
  const { SchedulerServiceInterface } = require('./scheduler-service-interface');
13
- const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
14
- const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
15
21
  const {
16
22
  createSchedulerService,
17
23
  SCHEDULER_PROVIDERS,
@@ -22,9 +28,16 @@ module.exports = {
22
28
  // Interface (Port)
23
29
  SchedulerServiceInterface,
24
30
 
25
- // Adapters
26
- EventBridgeSchedulerAdapter,
27
- MockSchedulerAdapter,
31
+ // Adapters (loaded on access)
32
+ get EventBridgeSchedulerAdapter() {
33
+ return require('./eventbridge-scheduler-adapter').EventBridgeSchedulerAdapter;
34
+ },
35
+ get MockSchedulerAdapter() {
36
+ return require('./mock-scheduler-adapter').MockSchedulerAdapter;
37
+ },
38
+ get NetlifySchedulerAdapter() {
39
+ return require('./netlify-scheduler-adapter').NetlifySchedulerAdapter;
40
+ },
28
41
 
29
42
  // Factory
30
43
  createSchedulerService,