@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.
- package/CLAUDE.md +25 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/user-commands.js +17 -5
- package/core/Worker.js +31 -7
- package/credential/use-cases/credential-operations-for-user.js +110 -25
- package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +6 -1
- package/database/encryption/encryption-schema-registry.js +80 -12
- package/database/organization-where.js +18 -0
- package/database/prisma.js +4 -2
- package/database/unique-violation.js +25 -0
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +167 -0
- package/extensions/index.js +41 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +27 -4
- package/generated/prisma-mongodb/index-browser.js +24 -1
- package/generated/prisma-mongodb/index.d.ts +3227 -47
- package/generated/prisma-mongodb/index.js +27 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +65 -0
- package/generated/prisma-mongodb/wasm.js +27 -4
- package/generated/prisma-postgresql/edge.js +27 -4
- package/generated/prisma-postgresql/index-browser.js +24 -1
- package/generated/prisma-postgresql/index.d.ts +3519 -67
- package/generated/prisma-postgresql/index.js +27 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +61 -0
- package/generated/prisma-postgresql/wasm.js +27 -4
- package/handlers/app-definition-loader.js +93 -12
- package/handlers/routers/auth.js +40 -12
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/index.js +58 -1
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +36 -7
- package/integrations/integration-router.js +115 -73
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -3
- package/integrations/repositories/integration-repository-interface.js +17 -1
- package/integrations/repositories/integration-repository-mongo.js +59 -76
- package/integrations/repositories/integration-repository-postgres.js +62 -76
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +5 -1
- package/integrations/use-cases/integration-operations-for-user.js +55 -24
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/management-api/management-api-config.js +71 -2
- package/management-api/openapi/components.js +40 -0
- package/management-api/openapi/v2-operations.js +16 -4
- package/management-api/v2/dependencies.js +40 -15
- package/management-api/v2/dto.js +20 -0
- package/management-api/v2/handlers/authorization.js +1 -0
- package/management-api/v2/handlers/credentials.js +6 -2
- package/management-api/v2/handlers/entities.js +10 -7
- package/management-api/v2/handlers/integrations.js +6 -3
- package/management-api/v2/request.js +32 -1
- package/modules/entity-scope.js +15 -0
- package/modules/index.js +8 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +35 -2
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +50 -13
- package/modules/use-cases/entity-types.js +31 -7
- package/modules/use-cases/execute-entity-proxy-request.js +50 -3
- package/modules/use-cases/get-entities-for-user.js +16 -5
- package/modules/use-cases/get-entity-module-for-user.js +32 -5
- package/modules/use-cases/get-entity-options-by-id.js +9 -3
- package/modules/use-cases/get-module.js +9 -3
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +39 -12
- package/modules/use-cases/process-authorization-callback.js +52 -18
- package/modules/use-cases/refresh-entity-options.js +9 -3
- package/modules/use-cases/test-module-auth.js +9 -3
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +65 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +61 -0
- package/providers/index.js +13 -0
- package/providers/resolve-provider.js +93 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +76 -0
- package/queues/providers/qstash-queue-provider.js +122 -0
- package/queues/providers/sqs-queue-provider.js +61 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +46 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +129 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|