@friggframework/core 2.0.0--canary.680.daae24e.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 (156) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/CLAUDE.md +28 -0
  3. package/README.md +105 -0
  4. package/application/commands/integration-commands.js +2 -2
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/core/Worker.js +31 -7
  8. package/core/create-handler.js +9 -0
  9. package/credential/use-cases/credential-operations-for-user.js +110 -25
  10. package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
  11. package/database/config.js +48 -24
  12. package/database/documentdb-utils.js +6 -1
  13. package/database/encryption/README.md +0 -2
  14. package/database/encryption/encryption-config.js +10 -31
  15. package/database/encryption/encryption-schema-registry.js +99 -47
  16. package/database/encryption/field-encryption-service.js +5 -69
  17. package/database/organization-where.js +18 -0
  18. package/database/prisma.js +4 -2
  19. package/database/unique-violation.js +25 -0
  20. package/extensions/bootstrap-runner.js +43 -0
  21. package/extensions/extension-loader.js +187 -0
  22. package/extensions/index.js +41 -0
  23. package/extensions/initialize-app.js +123 -0
  24. package/extensions/route-mounter.js +103 -0
  25. package/extensions/schema-composer.js +188 -0
  26. package/generated/prisma-mongodb/edge.js +27 -4
  27. package/generated/prisma-mongodb/index-browser.js +24 -1
  28. package/generated/prisma-mongodb/index.d.ts +3227 -47
  29. package/generated/prisma-mongodb/index.js +27 -4
  30. package/generated/prisma-mongodb/package.json +1 -1
  31. package/generated/prisma-mongodb/schema.prisma +65 -0
  32. package/generated/prisma-mongodb/wasm.js +27 -4
  33. package/generated/prisma-postgresql/edge.js +27 -4
  34. package/generated/prisma-postgresql/index-browser.js +24 -1
  35. package/generated/prisma-postgresql/index.d.ts +3519 -67
  36. package/generated/prisma-postgresql/index.js +27 -4
  37. package/generated/prisma-postgresql/package.json +1 -1
  38. package/generated/prisma-postgresql/schema.prisma +61 -0
  39. package/generated/prisma-postgresql/wasm.js +27 -4
  40. package/handlers/app-definition-loader.js +103 -12
  41. package/handlers/routers/auth.js +40 -12
  42. package/handlers/routers/health.js +4 -2
  43. package/handlers/routers/integration-defined-routers.js +144 -92
  44. package/handlers/routers/integration-webhook-routers.js +102 -64
  45. package/index.js +63 -1
  46. package/infrastructure/scheduler/index.js +19 -6
  47. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  48. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  49. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  50. package/integrations/index.js +5 -0
  51. package/integrations/integration-router.js +115 -73
  52. package/integrations/repositories/integration-not-found.js +14 -0
  53. package/integrations/repositories/integration-repository-documentdb.js +35 -3
  54. package/integrations/repositories/integration-repository-interface.js +17 -1
  55. package/integrations/repositories/integration-repository-mongo.js +59 -76
  56. package/integrations/repositories/integration-repository-postgres.js +62 -76
  57. package/integrations/schedule-config.js +80 -0
  58. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  59. package/integrations/use-cases/create-integration.js +55 -20
  60. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  61. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  62. package/integrations/use-cases/index.js +7 -1
  63. package/integrations/use-cases/integration-operations-for-user.js +55 -24
  64. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  65. package/integrations/utils/map-integration-dto.js +44 -1
  66. package/management-api/management-api-config.js +71 -2
  67. package/management-api/meta-router.js +21 -2
  68. package/management-api/openapi/components.js +40 -0
  69. package/management-api/openapi/v2-operations.js +16 -4
  70. package/management-api/route-registry.js +123 -6
  71. package/management-api/v2/dependencies.js +40 -15
  72. package/management-api/v2/dto.js +20 -0
  73. package/management-api/v2/handlers/authorization.js +1 -0
  74. package/management-api/v2/handlers/credentials.js +6 -2
  75. package/management-api/v2/handlers/entities.js +10 -7
  76. package/management-api/v2/handlers/integrations.js +6 -3
  77. package/management-api/v2/request.js +32 -1
  78. package/modules/entity-scope.js +15 -0
  79. package/modules/index.js +8 -0
  80. package/modules/module-factory.js +12 -0
  81. package/modules/module.js +53 -2
  82. package/modules/repositories/entity-not-found.js +18 -0
  83. package/modules/repositories/module-repository-documentdb.js +51 -6
  84. package/modules/repositories/module-repository-interface.js +19 -2
  85. package/modules/repositories/module-repository-mongo.js +67 -74
  86. package/modules/repositories/module-repository-postgres.js +66 -74
  87. package/modules/repositories/module-repository.js +2 -1
  88. package/modules/scope-config.js +192 -0
  89. package/modules/scope-policy.js +112 -0
  90. package/modules/scope-resolution.js +288 -0
  91. package/modules/scope-visibility.js +247 -0
  92. package/modules/tests/doubles/ownership-matching.js +18 -0
  93. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  94. package/modules/use-cases/create-entity-from-credential.js +113 -0
  95. package/modules/use-cases/entity-operations-for-user.js +50 -13
  96. package/modules/use-cases/entity-types.js +31 -7
  97. package/modules/use-cases/execute-entity-proxy-request.js +50 -3
  98. package/modules/use-cases/get-authorization-requirements.js +2 -2
  99. package/modules/use-cases/get-entities-for-user.js +26 -13
  100. package/modules/use-cases/get-entity-module-for-user.js +34 -7
  101. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  102. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  103. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  104. package/modules/use-cases/get-module.js +10 -4
  105. package/modules/use-cases/manage-access.js +21 -0
  106. package/modules/use-cases/multi-step-authorization.js +40 -13
  107. package/modules/use-cases/process-authorization-callback.js +53 -19
  108. package/modules/use-cases/refresh-entity-options.js +10 -4
  109. package/modules/use-cases/test-module-auth.js +10 -4
  110. package/package.json +6 -5
  111. package/prisma-mongodb/schema.prisma +65 -0
  112. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  113. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  114. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  115. package/prisma-postgresql/schema.prisma +61 -0
  116. package/providers/index.js +23 -0
  117. package/providers/resolve-provider.js +93 -0
  118. package/providers/runtime-detection.js +124 -0
  119. package/queues/index.js +25 -0
  120. package/queues/providers/index.js +11 -0
  121. package/queues/providers/netlify-background-provider.js +150 -0
  122. package/queues/providers/qstash-queue-provider.js +219 -0
  123. package/queues/providers/sqs-queue-provider.js +70 -0
  124. package/queues/queue-client-interface.js +55 -0
  125. package/queues/queue-message-auth.js +193 -0
  126. package/queues/queue-provider-factory.js +101 -0
  127. package/queues/queue-provider.js +65 -0
  128. package/queues/sqs-queue-client.js +81 -0
  129. package/types/index.d.ts +1 -0
  130. package/types/modules/index.d.ts +139 -0
  131. package/types/user/index.d.ts +134 -0
  132. package/user/adopter-jwt-config.js +177 -0
  133. package/user/adopter-jwt-errors.js +59 -0
  134. package/user/adopter-token-verifier.js +122 -0
  135. package/user/app-user-helpers.js +97 -0
  136. package/user/create-user-authentication.js +64 -0
  137. package/user/membership.js +51 -0
  138. package/user/repositories/organization-membership-record.js +19 -0
  139. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  140. package/user/repositories/organization-membership-repository-factory.js +42 -0
  141. package/user/repositories/organization-membership-repository-interface.js +57 -0
  142. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  143. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  144. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  145. package/user/repositories/user-repository-documentdb.js +27 -2
  146. package/user/repositories/user-repository-interface.js +18 -0
  147. package/user/repositories/user-repository-mongo.js +24 -0
  148. package/user/repositories/user-repository-postgres.js +18 -0
  149. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  150. package/user/use-cases/authenticate-user.js +7 -1
  151. package/user/use-cases/ensure-organization-membership.js +92 -0
  152. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  153. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  154. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  155. package/user/user.js +64 -1
  156. package/database/encryption/test-support/fake-ciphertext.js +0 -15
@@ -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');
@@ -63,6 +71,7 @@ const {
63
71
  Options,
64
72
  createIntegrationRouter,
65
73
  checkRequiredParams,
74
+ getModulesDefinitionFromIntegrationClasses,
66
75
  LoadIntegrationContextUseCase,
67
76
  } = require('./integrations/index');
68
77
  const {
@@ -100,11 +109,34 @@ const {
100
109
  ModuleFactory,
101
110
  getEntityTypeRegistry,
102
111
  EntityRegistryError,
112
+ resolveScope,
113
+ listBindingScopes,
114
+ OwnershipConfigError,
103
115
  } = require('./modules/index');
116
+ const {
117
+ ManagementApiConfigError,
118
+ } = require('./management-api/management-api-config');
104
119
  const application = require('./application');
105
120
  const utils = require('./utils');
106
121
 
107
- 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
+ isDeployedRuntime,
135
+ } = require('./providers');
136
+
137
+ // App-level core extensions (appDefinition.coreExtensions): Prisma models, encrypted
138
+ // fields, admin routes and bootstrap hooks (friggframework/frigg#545).
139
+ const extensions = require('./extensions');
108
140
 
109
141
  module.exports = {
110
142
  // assertions
@@ -131,6 +163,10 @@ module.exports = {
131
163
  createUserRepository,
132
164
  UserRepositoryMongo,
133
165
  UserRepositoryPostgres,
166
+ createOrganizationMembershipRepository,
167
+ OrganizationMembershipRepositoryMongo,
168
+ OrganizationMembershipRepositoryPostgres,
169
+ EnsureOrganizationMembership,
134
170
  GetUserFromXFriggHeaders,
135
171
  GetUserFromAdopterJwt,
136
172
  AuthenticateUser,
@@ -151,6 +187,8 @@ module.exports = {
151
187
  Options,
152
188
  checkRequiredParams,
153
189
  createIntegrationRouter,
190
+ // Deprecated (ADR-059): use getEntityTypeRegistry.
191
+ getModulesDefinitionFromIntegrationClasses,
154
192
  LoadIntegrationContextUseCase,
155
193
  CreateProcess,
156
194
  UpdateProcessState,
@@ -208,8 +246,32 @@ module.exports = {
208
246
  ModuleFactory,
209
247
  getEntityTypeRegistry,
210
248
  EntityRegistryError,
249
+ resolveScope,
250
+ listBindingScopes,
251
+ OwnershipConfigError,
252
+ ManagementApiConfigError,
211
253
  // queues
212
254
  QueuerUtil,
255
+ QueueProvider,
256
+ QueueClientInterface,
257
+ createQueueProvider,
258
+ QUEUE_PROVIDERS,
259
+
260
+ // provider plugins (appDefinition.provider)
261
+ resolveProvider,
262
+ determineProviderName,
263
+ providerPackageName,
264
+ KNOWN_PROVIDERS,
265
+ isDeployedRuntime,
266
+
267
+ // app-level extensions
268
+ extensions,
269
+ loadExtensions: extensions.loadExtensions,
270
+ composeSchemas: extensions.composeSchemas,
271
+ mountExtensionRoutes: extensions.mountExtensionRoutes,
272
+ createExtensionRoutesRouter: extensions.createExtensionRoutesRouter,
273
+ runExtensionBootstraps: extensions.runExtensionBootstraps,
274
+ initializeApp: extensions.initializeApp,
213
275
 
214
276
  // utils
215
277
  ...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,