@friggframework/core 2.0.0--canary.679.a2bcc2f.0 → 2.0.0--canary.686.87e9875.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 (136) hide show
  1. package/CLAUDE.md +25 -0
  2. package/README.md +105 -0
  3. package/application/commands/integration-commands.js +2 -2
  4. package/application/commands/user-commands.js +17 -5
  5. package/core/Worker.js +31 -7
  6. package/credential/use-cases/credential-operations-for-user.js +110 -25
  7. package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
  8. package/database/config.js +48 -24
  9. package/database/documentdb-utils.js +6 -1
  10. package/database/encryption/encryption-schema-registry.js +80 -12
  11. package/database/organization-where.js +18 -0
  12. package/database/prisma.js +4 -2
  13. package/database/unique-violation.js +25 -0
  14. package/extensions/bootstrap-runner.js +43 -0
  15. package/extensions/extension-loader.js +167 -0
  16. package/extensions/index.js +41 -0
  17. package/extensions/initialize-app.js +123 -0
  18. package/extensions/route-mounter.js +103 -0
  19. package/extensions/schema-composer.js +188 -0
  20. package/generated/prisma-mongodb/edge.js +27 -4
  21. package/generated/prisma-mongodb/index-browser.js +24 -1
  22. package/generated/prisma-mongodb/index.d.ts +3227 -47
  23. package/generated/prisma-mongodb/index.js +27 -4
  24. package/generated/prisma-mongodb/package.json +1 -1
  25. package/generated/prisma-mongodb/schema.prisma +65 -0
  26. package/generated/prisma-mongodb/wasm.js +27 -4
  27. package/generated/prisma-postgresql/edge.js +27 -4
  28. package/generated/prisma-postgresql/index-browser.js +24 -1
  29. package/generated/prisma-postgresql/index.d.ts +3519 -67
  30. package/generated/prisma-postgresql/index.js +27 -4
  31. package/generated/prisma-postgresql/package.json +1 -1
  32. package/generated/prisma-postgresql/schema.prisma +61 -0
  33. package/generated/prisma-postgresql/wasm.js +27 -4
  34. package/handlers/app-definition-loader.js +93 -12
  35. package/handlers/routers/auth.js +40 -12
  36. package/handlers/routers/integration-defined-routers.js +144 -92
  37. package/handlers/routers/integration-webhook-routers.js +102 -64
  38. package/index.js +58 -1
  39. package/infrastructure/scheduler/index.js +19 -6
  40. package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
  41. package/infrastructure/scheduler/scheduler-service-factory.js +36 -7
  42. package/integrations/integration-router.js +115 -73
  43. package/integrations/repositories/integration-not-found.js +14 -0
  44. package/integrations/repositories/integration-repository-documentdb.js +35 -3
  45. package/integrations/repositories/integration-repository-interface.js +17 -1
  46. package/integrations/repositories/integration-repository-mongo.js +59 -76
  47. package/integrations/repositories/integration-repository-postgres.js +62 -76
  48. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  49. package/integrations/use-cases/create-integration.js +55 -20
  50. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  51. package/integrations/use-cases/index.js +5 -1
  52. package/integrations/use-cases/integration-operations-for-user.js +55 -24
  53. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  54. package/management-api/management-api-config.js +71 -2
  55. package/management-api/openapi/components.js +40 -0
  56. package/management-api/openapi/v2-operations.js +16 -4
  57. package/management-api/v2/dependencies.js +40 -15
  58. package/management-api/v2/dto.js +20 -0
  59. package/management-api/v2/handlers/authorization.js +1 -0
  60. package/management-api/v2/handlers/credentials.js +6 -2
  61. package/management-api/v2/handlers/entities.js +10 -7
  62. package/management-api/v2/handlers/integrations.js +6 -3
  63. package/management-api/v2/request.js +32 -1
  64. package/modules/entity-scope.js +15 -0
  65. package/modules/index.js +8 -0
  66. package/modules/module-factory.js +12 -0
  67. package/modules/module.js +35 -2
  68. package/modules/repositories/entity-not-found.js +18 -0
  69. package/modules/repositories/module-repository-documentdb.js +51 -6
  70. package/modules/repositories/module-repository-interface.js +19 -2
  71. package/modules/repositories/module-repository-mongo.js +67 -74
  72. package/modules/repositories/module-repository-postgres.js +66 -74
  73. package/modules/repositories/module-repository.js +2 -1
  74. package/modules/scope-config.js +192 -0
  75. package/modules/scope-policy.js +112 -0
  76. package/modules/scope-resolution.js +288 -0
  77. package/modules/scope-visibility.js +247 -0
  78. package/modules/tests/doubles/ownership-matching.js +18 -0
  79. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  80. package/modules/use-cases/create-entity-from-credential.js +113 -0
  81. package/modules/use-cases/entity-operations-for-user.js +50 -13
  82. package/modules/use-cases/entity-types.js +31 -7
  83. package/modules/use-cases/execute-entity-proxy-request.js +50 -3
  84. package/modules/use-cases/get-entities-for-user.js +16 -5
  85. package/modules/use-cases/get-entity-module-for-user.js +32 -5
  86. package/modules/use-cases/get-entity-options-by-id.js +9 -3
  87. package/modules/use-cases/get-module.js +9 -3
  88. package/modules/use-cases/manage-access.js +21 -0
  89. package/modules/use-cases/multi-step-authorization.js +39 -12
  90. package/modules/use-cases/process-authorization-callback.js +52 -18
  91. package/modules/use-cases/refresh-entity-options.js +9 -3
  92. package/modules/use-cases/test-module-auth.js +9 -3
  93. package/package.json +6 -5
  94. package/prisma-mongodb/schema.prisma +65 -0
  95. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  96. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  97. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  98. package/prisma-postgresql/schema.prisma +61 -0
  99. package/providers/index.js +13 -0
  100. package/providers/resolve-provider.js +93 -0
  101. package/queues/index.js +25 -0
  102. package/queues/providers/index.js +11 -0
  103. package/queues/providers/netlify-background-provider.js +76 -0
  104. package/queues/providers/qstash-queue-provider.js +122 -0
  105. package/queues/providers/sqs-queue-provider.js +61 -0
  106. package/queues/queue-client-interface.js +55 -0
  107. package/queues/queue-provider-factory.js +101 -0
  108. package/queues/queue-provider.js +46 -0
  109. package/queues/sqs-queue-client.js +81 -0
  110. package/types/index.d.ts +1 -0
  111. package/types/modules/index.d.ts +129 -0
  112. package/types/user/index.d.ts +134 -0
  113. package/user/adopter-jwt-config.js +177 -0
  114. package/user/adopter-jwt-errors.js +59 -0
  115. package/user/adopter-token-verifier.js +122 -0
  116. package/user/app-user-helpers.js +97 -0
  117. package/user/create-user-authentication.js +64 -0
  118. package/user/membership.js +51 -0
  119. package/user/repositories/organization-membership-record.js +19 -0
  120. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  121. package/user/repositories/organization-membership-repository-factory.js +42 -0
  122. package/user/repositories/organization-membership-repository-interface.js +57 -0
  123. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  124. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  125. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  126. package/user/repositories/user-repository-documentdb.js +27 -2
  127. package/user/repositories/user-repository-interface.js +18 -0
  128. package/user/repositories/user-repository-mongo.js +24 -0
  129. package/user/repositories/user-repository-postgres.js +18 -0
  130. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  131. package/user/use-cases/authenticate-user.js +7 -1
  132. package/user/use-cases/ensure-organization-membership.js +92 -0
  133. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  134. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  135. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  136. package/user/user.js +64 -1
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Netlify Scheduler Adapter
3
+ *
4
+ * Implements SchedulerServiceInterface for Netlify deployments.
5
+ *
6
+ * Netlify Scheduled Functions use cron syntax but are defined statically in
7
+ * netlify.toml or via the @netlify/functions schedule() helper. They cannot
8
+ * be created dynamically at runtime like EventBridge Scheduler.
9
+ *
10
+ * Strategy for one-time scheduled jobs on Netlify:
11
+ * 1. Store the schedule in the database (same as mock adapter pattern)
12
+ * 2. A Netlify Scheduled Function runs on a cron interval (e.g., every 5 min)
13
+ * 3. The cron function queries for due schedules and dispatches them
14
+ * to a background function for execution
15
+ *
16
+ * This is a "poll-and-dispatch" pattern vs EventBridge's "push" pattern.
17
+ * Trade-off: slightly less precise timing (up to cron interval delay),
18
+ * but works on any platform without cloud-specific scheduling APIs.
19
+ *
20
+ * For integrations that need the queue provider to dispatch jobs:
21
+ * - queueResourceId maps to the background function URL path
22
+ * - Payload is stored and forwarded when the cron fires
23
+ */
24
+
25
+ const { SchedulerServiceInterface } = require('./scheduler-service-interface');
26
+
27
+ class NetlifySchedulerAdapter extends SchedulerServiceInterface {
28
+ /**
29
+ * @param {Object} options
30
+ * @param {Object} options.repository - Repository for persisting schedules
31
+ * Must implement: save(schedule), delete(scheduleName),
32
+ * findByName(scheduleName), findDue(now)
33
+ * @param {Object} [options.queueProvider] - Queue provider for dispatching due jobs
34
+ */
35
+ constructor(options = {}) {
36
+ super();
37
+ this.repository = options.repository;
38
+ this.queueProvider = options.queueProvider;
39
+
40
+ if (!this.repository) {
41
+ console.warn(
42
+ '[NetlifyScheduler] No repository provided. ' +
43
+ 'Schedules will be stored in memory (lost on function restart). ' +
44
+ 'Provide a database-backed repository for production use.'
45
+ );
46
+ this._inMemorySchedules = new Map();
47
+ }
48
+ }
49
+
50
+ async scheduleOneTime({
51
+ scheduleName,
52
+ scheduleAt,
53
+ queueResourceId,
54
+ payload,
55
+ }) {
56
+ if (!scheduleName) throw new Error('scheduleName is required');
57
+ if (!scheduleAt || !(scheduleAt instanceof Date))
58
+ throw new Error('scheduleAt must be a valid Date object');
59
+ if (!queueResourceId) throw new Error('queueResourceId is required');
60
+
61
+ const scheduleData = {
62
+ scheduleName,
63
+ scheduledAt: scheduleAt.toISOString(),
64
+ queueResourceId,
65
+ payload,
66
+ createdAt: new Date().toISOString(),
67
+ state: 'PENDING',
68
+ };
69
+
70
+ if (this.repository) {
71
+ await this.repository.save(scheduleData);
72
+ } else {
73
+ this._inMemorySchedules.set(scheduleName, scheduleData);
74
+ }
75
+
76
+ console.log(
77
+ `[NetlifyScheduler] Scheduled: ${scheduleName} for ${scheduleAt.toISOString()}`
78
+ );
79
+
80
+ return {
81
+ scheduledJobId: `netlify-schedule-${scheduleName}`,
82
+ scheduledAt: scheduleAt.toISOString(),
83
+ };
84
+ }
85
+
86
+ async deleteSchedule(scheduleName) {
87
+ if (!scheduleName) throw new Error('scheduleName is required');
88
+
89
+ if (this.repository) {
90
+ await this.repository.delete(scheduleName);
91
+ } else {
92
+ this._inMemorySchedules.delete(scheduleName);
93
+ }
94
+
95
+ console.log(`[NetlifyScheduler] Deleted schedule: ${scheduleName}`);
96
+ }
97
+
98
+ async getScheduleStatus(scheduleName) {
99
+ if (!scheduleName) throw new Error('scheduleName is required');
100
+
101
+ let schedule;
102
+ if (this.repository) {
103
+ schedule = await this.repository.findByName(scheduleName);
104
+ } else {
105
+ schedule = this._inMemorySchedules.get(scheduleName);
106
+ }
107
+
108
+ if (!schedule) {
109
+ return { exists: false };
110
+ }
111
+
112
+ return {
113
+ exists: true,
114
+ scheduledAt: schedule.scheduledAt,
115
+ state: schedule.state,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Process due schedules. Called by the Netlify cron function.
121
+ *
122
+ * Finds all schedules where scheduledAt <= now, dispatches them
123
+ * to the queue provider, and marks them as completed.
124
+ *
125
+ * @returns {Promise<{ processed: number, errors: number }>}
126
+ */
127
+ async processDueSchedules() {
128
+ if (!this.repository) {
129
+ console.warn(
130
+ '[NetlifyScheduler] Cannot process due schedules without a repository'
131
+ );
132
+ return { processed: 0, errors: 0 };
133
+ }
134
+
135
+ const now = new Date();
136
+ const dueSchedules = await this.repository.findDue(now);
137
+
138
+ let processed = 0;
139
+ let errors = 0;
140
+
141
+ for (const schedule of dueSchedules) {
142
+ try {
143
+ // Mark as PROCESSING before dispatch so the next cron tick
144
+ // won't re-pick this schedule (findDue only returns PENDING).
145
+ await this.repository.save({
146
+ ...schedule,
147
+ state: 'PROCESSING',
148
+ });
149
+
150
+ if (this.queueProvider) {
151
+ await this.queueProvider.send(
152
+ schedule.payload,
153
+ schedule.queueResourceId
154
+ );
155
+ } else {
156
+ console.log(
157
+ `[NetlifyScheduler] No queue provider — logging payload for: ${schedule.scheduleName}`,
158
+ JSON.stringify(schedule.payload)
159
+ );
160
+ }
161
+
162
+ await this.repository.delete(schedule.scheduleName);
163
+ processed++;
164
+ } catch (error) {
165
+ console.error(
166
+ `[NetlifyScheduler] Error processing schedule ${schedule.scheduleName}:`,
167
+ error
168
+ );
169
+ // Mark as FAILED so it won't be re-dispatched automatically.
170
+ // Operators can inspect FAILED schedules and retry manually.
171
+ await this.repository
172
+ .save({ ...schedule, state: 'FAILED' })
173
+ .catch(() => {});
174
+ errors++;
175
+ }
176
+ }
177
+
178
+ console.log(
179
+ `[NetlifyScheduler] Processed ${processed} due schedules, ${errors} errors`
180
+ );
181
+ return { processed, errors };
182
+ }
183
+ }
184
+
185
+ module.exports = { NetlifySchedulerAdapter };
@@ -7,16 +7,20 @@
7
7
  * Environment Detection:
8
8
  * - SCHEDULER_PROVIDER=eventbridge -> Use AWS EventBridge Scheduler
9
9
  * - SCHEDULER_PROVIDER=mock -> Use in-memory mock scheduler
10
+ * - SCHEDULER_PROVIDER=netlify -> Netlify poll-and-dispatch scheduler
11
+ * (friggframework/frigg#545; needs `repository`, usually `queueProvider`)
10
12
  * - Default in dev/test/local stages -> Mock scheduler
11
13
  * - Default in other stages -> EventBridge scheduler
14
+ * - Any other name -> require('@friggframework/provider-<name>').SchedulerAdapter
15
+ *
16
+ * Adapters are required inside their branch so building one never loads
17
+ * another's SDK (@aws-sdk/client-scheduler is not a core dependency).
12
18
  */
13
19
 
14
- const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
15
- const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
16
-
17
20
  const SCHEDULER_PROVIDERS = {
18
21
  EVENTBRIDGE: 'eventbridge',
19
22
  MOCK: 'mock',
23
+ NETLIFY: 'netlify',
20
24
  };
21
25
 
22
26
  const LOCAL_STAGES = ['dev', 'test', 'local'];
@@ -47,22 +51,47 @@ function determineProvider() {
47
51
  * @param {string} options.provider - Scheduler provider ('eventbridge' or 'mock')
48
52
  * @param {string} options.region - AWS region (for EventBridge)
49
53
  * @param {boolean} options.verbose - Verbose logging (for Mock)
54
+ * @param {Object} [options.repository] - Schedule repository (for Netlify: persists schedules)
55
+ * @param {Object} [options.queueProvider] - Queue provider (for Netlify: dispatches due jobs)
50
56
  * @returns {SchedulerServiceInterface} Implementation of scheduler interface
51
57
  */
52
58
  function createSchedulerService(options = {}) {
53
59
  const provider = options.provider || determineProvider();
54
60
 
55
61
  switch (provider) {
56
- case SCHEDULER_PROVIDERS.EVENTBRIDGE:
62
+ case SCHEDULER_PROVIDERS.EVENTBRIDGE: {
63
+ const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
57
64
  return new EventBridgeSchedulerAdapter({
58
65
  region: options.region,
59
66
  });
60
- case SCHEDULER_PROVIDERS.MOCK:
67
+ }
68
+ case SCHEDULER_PROVIDERS.MOCK: {
69
+ const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
61
70
  return new MockSchedulerAdapter({
62
71
  verbose: options.verbose,
63
72
  });
64
- default:
65
- throw new Error(`Unknown scheduler provider: ${provider}`);
73
+ }
74
+ case SCHEDULER_PROVIDERS.NETLIFY: {
75
+ const { NetlifySchedulerAdapter } = require('./netlify-scheduler-adapter');
76
+ return new NetlifySchedulerAdapter({
77
+ repository: options.repository,
78
+ queueProvider: options.queueProvider,
79
+ });
80
+ }
81
+ default: {
82
+ let plugin;
83
+ try {
84
+ plugin = require('../../providers/resolve-provider').resolveProvider(null, {
85
+ provider,
86
+ });
87
+ } catch (error) {
88
+ throw new Error(`Unknown scheduler provider: ${provider} (${error.message})`);
89
+ }
90
+ if (!plugin.SchedulerAdapter) {
91
+ throw new Error(`Unknown scheduler provider: ${provider}`);
92
+ }
93
+ return new plugin.SchedulerAdapter(options);
94
+ }
66
95
  }
67
96
  }
68
97
 
@@ -51,22 +51,27 @@ const {
51
51
  const {
52
52
  createUserRepository,
53
53
  } = require('../user/repositories/user-repository-factory');
54
- const {
55
- GetUserFromBearerToken,
56
- } = require('../user/use-cases/get-user-from-bearer-token');
57
- const {
58
- GetUserFromXFriggHeaders,
59
- } = require('../user/use-cases/get-user-from-x-frigg-headers');
60
- const {
61
- GetUserFromAdopterJwt,
62
- } = require('../user/use-cases/get-user-from-adopter-jwt');
63
54
  const {
64
55
  AuthenticateWithSharedSecret,
65
56
  } = require('../user/use-cases/authenticate-with-shared-secret');
66
57
  const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
58
+ const {
59
+ buildUserAuthentication,
60
+ } = require('../user/create-user-authentication');
67
61
  const {
68
62
  ProcessAuthorizationCallback,
69
63
  } = require('../modules/use-cases/process-authorization-callback');
64
+ const { createScopePolicy } = require('../modules/scope-policy');
65
+ const {
66
+ AuthorizeEntityForUser,
67
+ } = require('../modules/use-cases/authorize-entity-for-user');
68
+ const {
69
+ CreateEntityFromCredential,
70
+ } = require('../modules/use-cases/create-entity-from-credential');
71
+ const {
72
+ ResolveIntegrationOwnership,
73
+ } = require('./use-cases/resolve-integration-ownership');
74
+ const { GetOwnedIntegrationRecord } = require('./use-cases/integration-operations-for-user');
70
75
 
71
76
  const log = getLogger('frigg.integrations');
72
77
 
@@ -77,26 +82,20 @@ function createIntegrationRouter() {
77
82
  integrations: integrationClasses,
78
83
  userConfig,
79
84
  moduleDefinitions,
85
+ ownership,
86
+ entityTypeRegistry,
80
87
  } = loadAppDefinition();
88
+ // Ownership by scope (ADR-056 §3): one object every use case asks.
89
+ const scopePolicy = createScopePolicy({ ownership, entityTypeRegistry });
81
90
  const moduleRepository = createModuleRepository();
82
91
  const integrationRepository = createIntegrationRepository();
83
92
  const credentialRepository = createCredentialRepository();
84
93
  const userRepository = createUserRepository();
85
-
86
- const getUserFromBearerToken = new GetUserFromBearerToken({
87
- userRepository,
88
- userConfig,
89
- });
90
-
91
- const getUserFromXFriggHeaders = new GetUserFromXFriggHeaders({
92
- userRepository,
93
- userConfig,
94
- });
95
-
96
- const getUserFromAdopterJwt = new GetUserFromAdopterJwt({
97
- userRepository,
98
- userConfig,
99
- });
94
+ const {
95
+ getUserFromBearerToken,
96
+ getUserFromXFriggHeaders,
97
+ getUserFromAdopterJwt,
98
+ } = buildUserAuthentication({ userConfig, userRepository });
100
99
 
101
100
  const authenticateWithSharedSecret = new AuthenticateWithSharedSecret();
102
101
 
@@ -123,6 +122,19 @@ function createIntegrationRouter() {
123
122
  integrationClasses,
124
123
  moduleFactory,
125
124
  moduleRepository,
125
+ scopePolicy,
126
+ });
127
+
128
+ // Decides who owns a new integration and which entities the caller may bind
129
+ // (the v1 create used to check neither), and who may reach an existing one.
130
+ const resolveIntegrationOwnership = new ResolveIntegrationOwnership({
131
+ moduleRepository,
132
+ integrationClasses,
133
+ scopePolicy,
134
+ });
135
+ const getOwnedIntegrationRecord = new GetOwnedIntegrationRecord({
136
+ integrationRepository,
137
+ scopePolicy,
126
138
  });
127
139
 
128
140
  const getCredentialForUser = new GetCredentialForUser({
@@ -138,6 +150,7 @@ function createIntegrationRouter() {
138
150
  const getEntitiesForUser = new GetEntitiesForUser({
139
151
  moduleRepository,
140
152
  moduleDefinitions,
153
+ scopePolicy,
141
154
  });
142
155
 
143
156
  const getIntegrationInstance = new GetIntegrationInstance({
@@ -163,37 +176,55 @@ function createIntegrationRouter() {
163
176
  const testModuleAuth = new TestModuleAuth({
164
177
  moduleRepository,
165
178
  moduleDefinitions,
179
+ scopePolicy,
166
180
  });
167
181
 
168
182
  const getModule = new GetModule({
169
183
  moduleRepository,
170
184
  moduleDefinitions,
185
+ scopePolicy,
171
186
  });
172
187
 
173
188
  const getEntityOptionsById = new GetEntityOptionsById({
174
189
  moduleRepository,
175
190
  moduleDefinitions,
191
+ scopePolicy,
176
192
  });
177
193
 
178
194
  const refreshEntityOptions = new RefreshEntityOptions({
179
195
  moduleRepository,
180
196
  moduleDefinitions,
197
+ scopePolicy,
181
198
  });
182
199
 
183
200
  const getPossibleIntegrations = new GetPossibleIntegrations({
184
201
  integrationClasses,
185
202
  });
186
203
 
204
+ // The owner of what an authorization creates follows its scope (ADR-056 §3.2).
187
205
  const processAuthorizationCallback = new ProcessAuthorizationCallback({
188
206
  moduleRepository,
189
207
  credentialRepository,
190
208
  integrationRepository,
191
209
  moduleDefinitions,
192
210
  });
211
+ const authorizeEntityForUser = new AuthorizeEntityForUser({
212
+ scopePolicy,
213
+ processAuthorizationCallback,
214
+ });
215
+ // v1 POST /api/entity: same owner resolution as /api/authorize.
216
+ const createEntityFromCredential = new CreateEntityFromCredential({
217
+ authorizeEntityForUser,
218
+ processAuthorizationCallback,
219
+ credentialRepository,
220
+ moduleDefinitions,
221
+ });
193
222
 
194
223
  const router = express();
195
224
 
196
225
  setIntegrationRoutes(router, authenticateUser, {
226
+ resolveIntegrationOwnership,
227
+ getOwnedIntegrationRecord,
197
228
  createIntegration,
198
229
  deleteIntegrationForUser,
199
230
  getIntegrationsForUser,
@@ -210,7 +241,8 @@ function createIntegrationRouter() {
210
241
  getModule,
211
242
  getEntityOptionsById,
212
243
  refreshEntityOptions,
213
- processAuthorizationCallback,
244
+ authorizeEntityForUser,
245
+ createEntityFromCredential,
214
246
  });
215
247
  return router;
216
248
  }
@@ -247,6 +279,8 @@ function checkRequiredParams(params, requiredKeys) {
247
279
  */
248
280
  function setIntegrationRoutes(router, authenticateUser, useCases) {
249
281
  const {
282
+ resolveIntegrationOwnership,
283
+ getOwnedIntegrationRecord,
250
284
  createIntegration,
251
285
  deleteIntegrationForUser,
252
286
  getIntegrationsForUser,
@@ -255,15 +289,22 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
255
289
  updateIntegration,
256
290
  getPossibleIntegrations,
257
291
  } = useCases;
292
+
293
+ // The owner of an integration the caller may reach (404 otherwise), which is
294
+ // what the id-and-owner use cases below expect. `manage: true` is for
295
+ // changing or deleting: an organization's integration needs an admin (403).
296
+ const ownerOf = async (integrationId, user, options) =>
297
+ (await getOwnedIntegrationRecord.execute(integrationId, user, options))
298
+ .userId;
299
+
258
300
  router.route('/api/integrations').get(
259
301
  catchAsyncError(async (req, res) => {
260
302
  const user = await authenticateUser.execute(req);
261
- const userId = user.getId();
262
- const integrations = await getIntegrationsForUser.execute(userId);
303
+ const integrations = await getIntegrationsForUser.execute(user);
263
304
  const results = {
264
305
  entities: {
265
306
  options: await getPossibleIntegrations.execute(),
266
- authorized: await getEntitiesForUser.execute(userId),
307
+ authorized: await getEntitiesForUser.execute(user),
267
308
  },
268
309
  integrations: integrations,
269
310
  };
@@ -275,7 +316,6 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
275
316
  router.route('/api/integrations').post(
276
317
  catchAsyncError(async (req, res) => {
277
318
  const user = await authenticateUser.execute(req);
278
- const userId = user.getId();
279
319
  const params = checkRequiredParams(req.body, [
280
320
  'entities',
281
321
  'config',
@@ -283,10 +323,17 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
283
323
 
284
324
  get(params.config, 'type');
285
325
 
326
+ const { ownerId, organizationId, adoptUnscoped } =
327
+ await resolveIntegrationOwnership.execute({
328
+ entityIds: params.entities,
329
+ config: params.config,
330
+ user,
331
+ });
286
332
  const integration = await createIntegration.execute(
287
333
  params.entities,
288
- userId,
289
- params.config
334
+ ownerId,
335
+ params.config,
336
+ { organizationId, adoptUnscoped }
290
337
  );
291
338
 
292
339
  res.status(201).json(integration);
@@ -296,12 +343,11 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
296
343
  router.route('/api/integrations/:integrationId').patch(
297
344
  catchAsyncError(async (req, res) => {
298
345
  const user = await authenticateUser.execute(req);
299
- const userId = user.getId();
300
346
  const params = checkRequiredParams(req.body, ['config']);
301
347
 
302
348
  const integration = await updateIntegration.execute(
303
349
  req.params.integrationId,
304
- userId,
350
+ await ownerOf(req.params.integrationId, user, { manage: true }),
305
351
  params.config
306
352
  );
307
353
  res.json(integration);
@@ -314,7 +360,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
314
360
  const params = checkRequiredParams(req.params, ['integrationId']);
315
361
  await deleteIntegrationForUser.execute(
316
362
  params.integrationId,
317
- user.getId()
363
+ await ownerOf(params.integrationId, user, { manage: true })
318
364
  );
319
365
  res.status(204).json({});
320
366
  })
@@ -326,7 +372,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
326
372
  const params = checkRequiredParams(req.params, ['integrationId']);
327
373
  const integration = await getIntegrationInstance.execute(
328
374
  params.integrationId,
329
- user.getId()
375
+ await ownerOf(params.integrationId, user)
330
376
  );
331
377
  res.json(await integration.send('GET_CONFIG_OPTIONS'));
332
378
  })
@@ -342,7 +388,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
342
388
  ]);
343
389
  const integration = await getIntegrationInstance.execute(
344
390
  params.integrationId,
345
- user.getId()
391
+ await ownerOf(params.integrationId, user)
346
392
  );
347
393
 
348
394
  res.json(
@@ -356,7 +402,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
356
402
  const params = checkRequiredParams(req.params, ['integrationId']);
357
403
  const integration = await getIntegrationInstance.execute(
358
404
  params.integrationId,
359
- user.getId()
405
+ await ownerOf(params.integrationId, user)
360
406
  );
361
407
  res.json(await integration.send('GET_USER_ACTIONS', req.body));
362
408
  })
@@ -373,7 +419,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
373
419
  ]);
374
420
  const integration = await getIntegrationInstance.execute(
375
421
  params.integrationId,
376
- user.getId()
422
+ await ownerOf(params.integrationId, user)
377
423
  );
378
424
 
379
425
  res.json(
@@ -398,7 +444,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
398
444
  ]);
399
445
  const integration = await getIntegrationInstance.execute(
400
446
  params.integrationId,
401
- user.getId()
447
+ await ownerOf(params.integrationId, user)
402
448
  );
403
449
 
404
450
  res.json(
@@ -419,7 +465,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
419
465
  ]);
420
466
  const integration = await getIntegrationInstance.execute(
421
467
  params.integrationId,
422
- user.getId()
468
+ await ownerOf(params.integrationId, user)
423
469
  );
424
470
  res.json(await integration.send(params.actionId, req.body));
425
471
  })
@@ -436,7 +482,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
436
482
  const params = checkRequiredParams(req.params, ['integrationId']);
437
483
  const integration = await getIntegrationInstance.execute(
438
484
  params.integrationId,
439
- user.getId()
485
+ await ownerOf(params.integrationId, user)
440
486
  );
441
487
 
442
488
  // We could perhaps augment router with dynamic options? Haven't decided yet, but here may be the place
@@ -456,7 +502,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
456
502
  const params = checkRequiredParams(req.params, ['integrationId']);
457
503
  const instance = await getIntegrationInstance.execute(
458
504
  params.integrationId,
459
- user.getId()
505
+ await ownerOf(params.integrationId, user)
460
506
  );
461
507
 
462
508
  if (!instance) {
@@ -494,16 +540,21 @@ function setEntityRoutes(router, authenticateUser, useCases) {
494
540
  getModule,
495
541
  getEntityOptionsById,
496
542
  refreshEntityOptions,
497
- processAuthorizationCallback,
543
+ authorizeEntityForUser,
544
+ createEntityFromCredential,
498
545
  } = useCases;
499
546
 
500
547
  router.route('/api/authorize').get(
501
548
  catchAsyncError(async (req, res) => {
502
549
  const user = await authenticateUser.execute(req);
503
- const userId = user.getId();
504
550
  const params = checkRequiredParams(req.query, ['entityType']);
551
+ // The owner POST stores under, so GET and POST describe one connection.
552
+ const { ownerId } = authorizeEntityForUser.resolveOwnership({
553
+ user,
554
+ entityType: params.entityType,
555
+ });
505
556
  const module = await getModuleInstanceFromType.execute(
506
- userId,
557
+ ownerId,
507
558
  params.entityType,
508
559
  { state: req.query.state }
509
560
  );
@@ -522,11 +573,15 @@ function setEntityRoutes(router, authenticateUser, useCases) {
522
573
  router.route('/api/authorize').post(
523
574
  catchAsyncError(async (req, res) => {
524
575
  const user = await authenticateUser.execute(req);
525
- const userId = user.getId();
526
576
  const params = checkRequiredParams(req.body, [
527
577
  'entityType',
528
578
  'data',
529
579
  ]);
580
+ const ownership = authorizeEntityForUser.resolveOwnership({
581
+ user,
582
+ entityType: params.entityType,
583
+ });
584
+ const userId = ownership.ownerId;
530
585
 
531
586
  const dataKeys =
532
587
  params.data && typeof params.data === 'object'
@@ -538,11 +593,13 @@ function setEntityRoutes(router, authenticateUser, useCases) {
538
593
  dataKeys,
539
594
  });
540
595
 
541
- const entityDetails = await processAuthorizationCallback.execute(
542
- userId,
543
- params.entityType,
544
- params.data
545
- );
596
+ const { result: entityDetails } =
597
+ await authorizeEntityForUser.execute({
598
+ user,
599
+ entityType: params.entityType,
600
+ data: params.data,
601
+ ownership,
602
+ });
546
603
 
547
604
  log.info('Entity authorized', {
548
605
  eventName: 'frigg.integrations.authorized',
@@ -559,35 +616,20 @@ function setEntityRoutes(router, authenticateUser, useCases) {
559
616
  router.route('/api/entity').post(
560
617
  catchAsyncError(async (req, res) => {
561
618
  const user = await authenticateUser.execute(req);
562
- const userId = user.getId();
563
619
  const params = checkRequiredParams(req.body, [
564
620
  'entityType',
565
621
  'data',
566
622
  ]);
567
623
  checkRequiredParams(req.body.data, ['credential_id']);
568
624
 
569
- // May want to pass along the user ID as well so credential ID's can't be fished???
570
- const credential = await getCredentialForUser.execute(
571
- params.data.credential_id,
572
- userId
573
- );
574
-
575
- if (!credential) {
576
- throw Boom.badRequest('Invalid credential ID');
577
- }
578
-
579
- const module = await getModuleInstanceFromType.execute(
580
- userId,
581
- params.entityType
582
- );
583
- const entityDetails = await module.getEntityDetails(
584
- module.api,
585
- null,
586
- null,
587
- userId
625
+ // The owner follows the entity type's scope, as /api/authorize.
626
+ res.json(
627
+ await createEntityFromCredential.execute({
628
+ user,
629
+ entityType: params.entityType,
630
+ credentialId: params.data.credential_id,
631
+ })
588
632
  );
589
-
590
- res.json(await module.findOrCreateEntity(entityDetails));
591
633
  })
592
634
  );
593
635