@friggframework/core 2.0.0--canary.663.8de669c.0 → 2.0.0--canary.686.821f8a3.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/CHANGELOG.md +11 -0
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +6 -1
- package/database/encryption/README.md +2 -0
- package/database/encryption/encryption-config.js +10 -31
- package/database/encryption/encryption-schema-registry.js +121 -24
- package/database/encryption/field-encryption-service.js +69 -5
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- 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 +187 -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 +43 -4
- package/generated/prisma-mongodb/index-browser.js +40 -1
- package/generated/prisma-mongodb/index.d.ts +4761 -133
- package/generated/prisma-mongodb/index.js +43 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +92 -0
- package/generated/prisma-mongodb/wasm.js +43 -4
- package/generated/prisma-postgresql/edge.js +43 -4
- package/generated/prisma-postgresql/index-browser.js +40 -1
- package/generated/prisma-postgresql/index.d.ts +4961 -28
- package/generated/prisma-postgresql/index.js +43 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +87 -0
- package/generated/prisma-postgresql/wasm.js +43 -4
- package/handlers/app-definition-loader.js +121 -19
- package/handlers/app-handler-helpers.js +25 -3
- package/handlers/backend-utils.js +31 -25
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +13 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +65 -1
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +1 -0
- package/integrations/integration-router.js +131 -96
- 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/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +7 -1
- package/integrations/use-cases/integration-operations-for-user.js +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +76 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +408 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +411 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-errors.js +20 -0
- package/modules/proxy/proxy-path.js +179 -0
- package/modules/proxy/proxy-policy.js +248 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- 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/requester/proxy-request.js +244 -0
- package/modules/requester/requester.js +27 -2
- 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/in-memory-authorization-session-repository.js +45 -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 +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +286 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +329 -0
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +92 -0
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -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 +87 -0
- package/providers/index.js +23 -0
- package/providers/resolve-provider.js +93 -0
- package/providers/runtime-detection.js +124 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +219 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +193 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +2 -0
- package/types/modules/index.d.ts +197 -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
|
@@ -7,16 +7,22 @@
|
|
|
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`)
|
|
12
|
+
* - Default on Netlify (NETLIFY=true, set by the Netlify runtime and build;
|
|
13
|
+
* provider-netlify's generated functions also set SCHEDULER_PROVIDER) -> Netlify
|
|
10
14
|
* - Default in dev/test/local stages -> Mock scheduler
|
|
11
15
|
* - Default in other stages -> EventBridge scheduler
|
|
16
|
+
* - Any other name -> require('@friggframework/provider-<name>').SchedulerAdapter
|
|
17
|
+
*
|
|
18
|
+
* Adapters are required inside their branch so building one never loads
|
|
19
|
+
* another's SDK (@aws-sdk/client-scheduler is not a core dependency).
|
|
12
20
|
*/
|
|
13
21
|
|
|
14
|
-
const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
|
|
15
|
-
const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
|
|
16
|
-
|
|
17
22
|
const SCHEDULER_PROVIDERS = {
|
|
18
23
|
EVENTBRIDGE: 'eventbridge',
|
|
19
24
|
MOCK: 'mock',
|
|
25
|
+
NETLIFY: 'netlify',
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
const LOCAL_STAGES = ['dev', 'test', 'local'];
|
|
@@ -32,6 +38,12 @@ function determineProvider() {
|
|
|
32
38
|
return explicitProvider;
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
// Checked before the stage: a Netlify site with no STAGE (or STAGE=dev)
|
|
42
|
+
// must still persist schedules where scheduled-sync reads them.
|
|
43
|
+
if (process.env.NETLIFY === 'true') {
|
|
44
|
+
return SCHEDULER_PROVIDERS.NETLIFY;
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
const stage = process.env.STAGE || 'dev';
|
|
36
48
|
if (LOCAL_STAGES.includes(stage)) {
|
|
37
49
|
return SCHEDULER_PROVIDERS.MOCK;
|
|
@@ -40,6 +52,13 @@ function determineProvider() {
|
|
|
40
52
|
return SCHEDULER_PROVIDERS.EVENTBRIDGE;
|
|
41
53
|
}
|
|
42
54
|
|
|
55
|
+
function createDefaultScheduledJobRepository() {
|
|
56
|
+
const { ScheduledJobRepository } = require('./scheduled-job-repository');
|
|
57
|
+
// The prisma export is a lazy proxy: nothing connects until a query runs.
|
|
58
|
+
const { prisma } = require('../../database/prisma');
|
|
59
|
+
return new ScheduledJobRepository({ prismaClient: prisma });
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
/**
|
|
44
63
|
* Create a scheduler service instance
|
|
45
64
|
*
|
|
@@ -47,22 +66,51 @@ function determineProvider() {
|
|
|
47
66
|
* @param {string} options.provider - Scheduler provider ('eventbridge' or 'mock')
|
|
48
67
|
* @param {string} options.region - AWS region (for EventBridge)
|
|
49
68
|
* @param {boolean} options.verbose - Verbose logging (for Mock)
|
|
69
|
+
* @param {Object} [options.repository] - Schedule repository (for Netlify: persists schedules)
|
|
70
|
+
* @param {Object} [options.queueProvider] - Queue provider (for Netlify: dispatches due jobs)
|
|
50
71
|
* @returns {SchedulerServiceInterface} Implementation of scheduler interface
|
|
51
72
|
*/
|
|
52
73
|
function createSchedulerService(options = {}) {
|
|
53
74
|
const provider = options.provider || determineProvider();
|
|
54
75
|
|
|
55
76
|
switch (provider) {
|
|
56
|
-
case SCHEDULER_PROVIDERS.EVENTBRIDGE:
|
|
77
|
+
case SCHEDULER_PROVIDERS.EVENTBRIDGE: {
|
|
78
|
+
const { EventBridgeSchedulerAdapter } = require('./eventbridge-scheduler-adapter');
|
|
57
79
|
return new EventBridgeSchedulerAdapter({
|
|
58
80
|
region: options.region,
|
|
59
81
|
});
|
|
60
|
-
|
|
82
|
+
}
|
|
83
|
+
case SCHEDULER_PROVIDERS.MOCK: {
|
|
84
|
+
const { MockSchedulerAdapter } = require('./mock-scheduler-adapter');
|
|
61
85
|
return new MockSchedulerAdapter({
|
|
62
86
|
verbose: options.verbose,
|
|
63
87
|
});
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
}
|
|
89
|
+
case SCHEDULER_PROVIDERS.NETLIFY: {
|
|
90
|
+
const { NetlifySchedulerAdapter } = require('./netlify-scheduler-adapter');
|
|
91
|
+
return new NetlifySchedulerAdapter({
|
|
92
|
+
// Default: the Prisma ScheduledJob rows that the Netlify
|
|
93
|
+
// scheduled-sync function polls. Without it the adapter
|
|
94
|
+
// falls back to memory, and a schedule made in one function
|
|
95
|
+
// is never seen by the cron.
|
|
96
|
+
repository: options.repository || createDefaultScheduledJobRepository(),
|
|
97
|
+
queueProvider: options.queueProvider,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
default: {
|
|
101
|
+
let plugin;
|
|
102
|
+
try {
|
|
103
|
+
plugin = require('../../providers/resolve-provider').resolveProvider(null, {
|
|
104
|
+
provider,
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`Unknown scheduler provider: ${provider} (${error.message})`);
|
|
108
|
+
}
|
|
109
|
+
if (!plugin.SchedulerAdapter) {
|
|
110
|
+
throw new Error(`Unknown scheduler provider: ${provider}`);
|
|
111
|
+
}
|
|
112
|
+
return new plugin.SchedulerAdapter(options);
|
|
113
|
+
}
|
|
66
114
|
}
|
|
67
115
|
}
|
|
68
116
|
|
package/integrations/index.js
CHANGED
|
@@ -31,9 +31,6 @@ const {
|
|
|
31
31
|
GetIntegrationInstance,
|
|
32
32
|
} = require('./use-cases/get-integration-instance');
|
|
33
33
|
const { UpdateIntegration } = require('./use-cases/update-integration');
|
|
34
|
-
const {
|
|
35
|
-
getModulesDefinitionFromIntegrationClasses,
|
|
36
|
-
} = require('./utils/map-integration-dto');
|
|
37
34
|
const {
|
|
38
35
|
GetModuleInstanceFromType,
|
|
39
36
|
} = require('../modules/use-cases/get-module-instance-from-type');
|
|
@@ -54,47 +51,51 @@ const {
|
|
|
54
51
|
const {
|
|
55
52
|
createUserRepository,
|
|
56
53
|
} = require('../user/repositories/user-repository-factory');
|
|
57
|
-
const {
|
|
58
|
-
GetUserFromBearerToken,
|
|
59
|
-
} = require('../user/use-cases/get-user-from-bearer-token');
|
|
60
|
-
const {
|
|
61
|
-
GetUserFromXFriggHeaders,
|
|
62
|
-
} = require('../user/use-cases/get-user-from-x-frigg-headers');
|
|
63
|
-
const {
|
|
64
|
-
GetUserFromAdopterJwt,
|
|
65
|
-
} = require('../user/use-cases/get-user-from-adopter-jwt');
|
|
66
54
|
const {
|
|
67
55
|
AuthenticateWithSharedSecret,
|
|
68
56
|
} = require('../user/use-cases/authenticate-with-shared-secret');
|
|
69
57
|
const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
|
|
58
|
+
const {
|
|
59
|
+
buildUserAuthentication,
|
|
60
|
+
} = require('../user/create-user-authentication');
|
|
70
61
|
const {
|
|
71
62
|
ProcessAuthorizationCallback,
|
|
72
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');
|
|
73
75
|
|
|
74
76
|
const log = getLogger('frigg.integrations');
|
|
75
77
|
|
|
76
78
|
function createIntegrationRouter() {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// moduleDefinitions is the app registry's definitions (ADR-059), so
|
|
80
|
+
// /api/authorize and friends accept a type no integration binds.
|
|
81
|
+
const {
|
|
82
|
+
integrations: integrationClasses,
|
|
83
|
+
userConfig,
|
|
84
|
+
moduleDefinitions,
|
|
85
|
+
ownership,
|
|
86
|
+
entityTypeRegistry,
|
|
87
|
+
} = loadAppDefinition();
|
|
88
|
+
// Ownership by scope (ADR-056 §3): one object every use case asks.
|
|
89
|
+
const scopePolicy = createScopePolicy({ ownership, entityTypeRegistry });
|
|
79
90
|
const moduleRepository = createModuleRepository();
|
|
80
91
|
const integrationRepository = createIntegrationRepository();
|
|
81
92
|
const credentialRepository = createCredentialRepository();
|
|
82
93
|
const userRepository = createUserRepository();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const getUserFromXFriggHeaders = new GetUserFromXFriggHeaders({
|
|
90
|
-
userRepository,
|
|
91
|
-
userConfig,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const getUserFromAdopterJwt = new GetUserFromAdopterJwt({
|
|
95
|
-
userRepository,
|
|
96
|
-
userConfig,
|
|
97
|
-
});
|
|
94
|
+
const {
|
|
95
|
+
getUserFromBearerToken,
|
|
96
|
+
getUserFromXFriggHeaders,
|
|
97
|
+
getUserFromAdopterJwt,
|
|
98
|
+
} = buildUserAuthentication({ userConfig, userRepository });
|
|
98
99
|
|
|
99
100
|
const authenticateWithSharedSecret = new AuthenticateWithSharedSecret();
|
|
100
101
|
|
|
@@ -108,8 +109,7 @@ function createIntegrationRouter() {
|
|
|
108
109
|
|
|
109
110
|
const moduleFactory = new ModuleFactory({
|
|
110
111
|
moduleRepository,
|
|
111
|
-
moduleDefinitions
|
|
112
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
112
|
+
moduleDefinitions,
|
|
113
113
|
});
|
|
114
114
|
const deleteIntegrationForUser = new DeleteIntegrationForUser({
|
|
115
115
|
integrationRepository,
|
|
@@ -122,6 +122,19 @@ function createIntegrationRouter() {
|
|
|
122
122
|
integrationClasses,
|
|
123
123
|
moduleFactory,
|
|
124
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,
|
|
125
138
|
});
|
|
126
139
|
|
|
127
140
|
const getCredentialForUser = new GetCredentialForUser({
|
|
@@ -136,8 +149,8 @@ function createIntegrationRouter() {
|
|
|
136
149
|
|
|
137
150
|
const getEntitiesForUser = new GetEntitiesForUser({
|
|
138
151
|
moduleRepository,
|
|
139
|
-
moduleDefinitions
|
|
140
|
-
|
|
152
|
+
moduleDefinitions,
|
|
153
|
+
scopePolicy,
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
const getIntegrationInstance = new GetIntegrationInstance({
|
|
@@ -153,54 +166,65 @@ function createIntegrationRouter() {
|
|
|
153
166
|
});
|
|
154
167
|
|
|
155
168
|
const getModuleInstanceFromType = new GetModuleInstanceFromType({
|
|
156
|
-
moduleDefinitions
|
|
157
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
169
|
+
moduleDefinitions,
|
|
158
170
|
});
|
|
159
171
|
|
|
160
172
|
const getEntityOptionsByType = new GetEntityOptionsByType({
|
|
161
|
-
moduleDefinitions
|
|
162
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
173
|
+
moduleDefinitions,
|
|
163
174
|
});
|
|
164
175
|
|
|
165
176
|
const testModuleAuth = new TestModuleAuth({
|
|
166
177
|
moduleRepository,
|
|
167
|
-
moduleDefinitions
|
|
168
|
-
|
|
178
|
+
moduleDefinitions,
|
|
179
|
+
scopePolicy,
|
|
169
180
|
});
|
|
170
181
|
|
|
171
182
|
const getModule = new GetModule({
|
|
172
183
|
moduleRepository,
|
|
173
|
-
moduleDefinitions
|
|
174
|
-
|
|
184
|
+
moduleDefinitions,
|
|
185
|
+
scopePolicy,
|
|
175
186
|
});
|
|
176
187
|
|
|
177
188
|
const getEntityOptionsById = new GetEntityOptionsById({
|
|
178
189
|
moduleRepository,
|
|
179
|
-
moduleDefinitions
|
|
180
|
-
|
|
190
|
+
moduleDefinitions,
|
|
191
|
+
scopePolicy,
|
|
181
192
|
});
|
|
182
193
|
|
|
183
194
|
const refreshEntityOptions = new RefreshEntityOptions({
|
|
184
195
|
moduleRepository,
|
|
185
|
-
moduleDefinitions
|
|
186
|
-
|
|
196
|
+
moduleDefinitions,
|
|
197
|
+
scopePolicy,
|
|
187
198
|
});
|
|
188
199
|
|
|
189
200
|
const getPossibleIntegrations = new GetPossibleIntegrations({
|
|
190
201
|
integrationClasses,
|
|
191
202
|
});
|
|
192
203
|
|
|
204
|
+
// The owner of what an authorization creates follows its scope (ADR-056 §3.2).
|
|
193
205
|
const processAuthorizationCallback = new ProcessAuthorizationCallback({
|
|
194
206
|
moduleRepository,
|
|
195
207
|
credentialRepository,
|
|
196
208
|
integrationRepository,
|
|
197
|
-
moduleDefinitions
|
|
198
|
-
|
|
209
|
+
moduleDefinitions,
|
|
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,
|
|
199
221
|
});
|
|
200
222
|
|
|
201
223
|
const router = express();
|
|
202
224
|
|
|
203
225
|
setIntegrationRoutes(router, authenticateUser, {
|
|
226
|
+
resolveIntegrationOwnership,
|
|
227
|
+
getOwnedIntegrationRecord,
|
|
204
228
|
createIntegration,
|
|
205
229
|
deleteIntegrationForUser,
|
|
206
230
|
getIntegrationsForUser,
|
|
@@ -217,7 +241,8 @@ function createIntegrationRouter() {
|
|
|
217
241
|
getModule,
|
|
218
242
|
getEntityOptionsById,
|
|
219
243
|
refreshEntityOptions,
|
|
220
|
-
|
|
244
|
+
authorizeEntityForUser,
|
|
245
|
+
createEntityFromCredential,
|
|
221
246
|
});
|
|
222
247
|
return router;
|
|
223
248
|
}
|
|
@@ -254,6 +279,8 @@ function checkRequiredParams(params, requiredKeys) {
|
|
|
254
279
|
*/
|
|
255
280
|
function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
256
281
|
const {
|
|
282
|
+
resolveIntegrationOwnership,
|
|
283
|
+
getOwnedIntegrationRecord,
|
|
257
284
|
createIntegration,
|
|
258
285
|
deleteIntegrationForUser,
|
|
259
286
|
getIntegrationsForUser,
|
|
@@ -262,15 +289,22 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
262
289
|
updateIntegration,
|
|
263
290
|
getPossibleIntegrations,
|
|
264
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
|
+
|
|
265
300
|
router.route('/api/integrations').get(
|
|
266
301
|
catchAsyncError(async (req, res) => {
|
|
267
302
|
const user = await authenticateUser.execute(req);
|
|
268
|
-
const
|
|
269
|
-
const integrations = await getIntegrationsForUser.execute(userId);
|
|
303
|
+
const integrations = await getIntegrationsForUser.execute(user);
|
|
270
304
|
const results = {
|
|
271
305
|
entities: {
|
|
272
306
|
options: await getPossibleIntegrations.execute(),
|
|
273
|
-
authorized: await getEntitiesForUser.execute(
|
|
307
|
+
authorized: await getEntitiesForUser.execute(user),
|
|
274
308
|
},
|
|
275
309
|
integrations: integrations,
|
|
276
310
|
};
|
|
@@ -282,7 +316,6 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
282
316
|
router.route('/api/integrations').post(
|
|
283
317
|
catchAsyncError(async (req, res) => {
|
|
284
318
|
const user = await authenticateUser.execute(req);
|
|
285
|
-
const userId = user.getId();
|
|
286
319
|
const params = checkRequiredParams(req.body, [
|
|
287
320
|
'entities',
|
|
288
321
|
'config',
|
|
@@ -290,10 +323,17 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
290
323
|
|
|
291
324
|
get(params.config, 'type');
|
|
292
325
|
|
|
326
|
+
const { ownerId, organizationId, adoptUnscoped } =
|
|
327
|
+
await resolveIntegrationOwnership.execute({
|
|
328
|
+
entityIds: params.entities,
|
|
329
|
+
config: params.config,
|
|
330
|
+
user,
|
|
331
|
+
});
|
|
293
332
|
const integration = await createIntegration.execute(
|
|
294
333
|
params.entities,
|
|
295
|
-
|
|
296
|
-
params.config
|
|
334
|
+
ownerId,
|
|
335
|
+
params.config,
|
|
336
|
+
{ organizationId, adoptUnscoped }
|
|
297
337
|
);
|
|
298
338
|
|
|
299
339
|
res.status(201).json(integration);
|
|
@@ -303,12 +343,11 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
303
343
|
router.route('/api/integrations/:integrationId').patch(
|
|
304
344
|
catchAsyncError(async (req, res) => {
|
|
305
345
|
const user = await authenticateUser.execute(req);
|
|
306
|
-
const userId = user.getId();
|
|
307
346
|
const params = checkRequiredParams(req.body, ['config']);
|
|
308
347
|
|
|
309
348
|
const integration = await updateIntegration.execute(
|
|
310
349
|
req.params.integrationId,
|
|
311
|
-
|
|
350
|
+
await ownerOf(req.params.integrationId, user, { manage: true }),
|
|
312
351
|
params.config
|
|
313
352
|
);
|
|
314
353
|
res.json(integration);
|
|
@@ -321,7 +360,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
321
360
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
322
361
|
await deleteIntegrationForUser.execute(
|
|
323
362
|
params.integrationId,
|
|
324
|
-
user
|
|
363
|
+
await ownerOf(params.integrationId, user, { manage: true })
|
|
325
364
|
);
|
|
326
365
|
res.status(204).json({});
|
|
327
366
|
})
|
|
@@ -333,7 +372,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
333
372
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
334
373
|
const integration = await getIntegrationInstance.execute(
|
|
335
374
|
params.integrationId,
|
|
336
|
-
user
|
|
375
|
+
await ownerOf(params.integrationId, user)
|
|
337
376
|
);
|
|
338
377
|
res.json(await integration.send('GET_CONFIG_OPTIONS'));
|
|
339
378
|
})
|
|
@@ -349,7 +388,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
349
388
|
]);
|
|
350
389
|
const integration = await getIntegrationInstance.execute(
|
|
351
390
|
params.integrationId,
|
|
352
|
-
user
|
|
391
|
+
await ownerOf(params.integrationId, user)
|
|
353
392
|
);
|
|
354
393
|
|
|
355
394
|
res.json(
|
|
@@ -363,7 +402,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
363
402
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
364
403
|
const integration = await getIntegrationInstance.execute(
|
|
365
404
|
params.integrationId,
|
|
366
|
-
user
|
|
405
|
+
await ownerOf(params.integrationId, user)
|
|
367
406
|
);
|
|
368
407
|
res.json(await integration.send('GET_USER_ACTIONS', req.body));
|
|
369
408
|
})
|
|
@@ -380,7 +419,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
380
419
|
]);
|
|
381
420
|
const integration = await getIntegrationInstance.execute(
|
|
382
421
|
params.integrationId,
|
|
383
|
-
user
|
|
422
|
+
await ownerOf(params.integrationId, user)
|
|
384
423
|
);
|
|
385
424
|
|
|
386
425
|
res.json(
|
|
@@ -405,7 +444,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
405
444
|
]);
|
|
406
445
|
const integration = await getIntegrationInstance.execute(
|
|
407
446
|
params.integrationId,
|
|
408
|
-
user
|
|
447
|
+
await ownerOf(params.integrationId, user)
|
|
409
448
|
);
|
|
410
449
|
|
|
411
450
|
res.json(
|
|
@@ -426,7 +465,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
426
465
|
]);
|
|
427
466
|
const integration = await getIntegrationInstance.execute(
|
|
428
467
|
params.integrationId,
|
|
429
|
-
user
|
|
468
|
+
await ownerOf(params.integrationId, user)
|
|
430
469
|
);
|
|
431
470
|
res.json(await integration.send(params.actionId, req.body));
|
|
432
471
|
})
|
|
@@ -443,7 +482,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
443
482
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
444
483
|
const integration = await getIntegrationInstance.execute(
|
|
445
484
|
params.integrationId,
|
|
446
|
-
user
|
|
485
|
+
await ownerOf(params.integrationId, user)
|
|
447
486
|
);
|
|
448
487
|
|
|
449
488
|
// We could perhaps augment router with dynamic options? Haven't decided yet, but here may be the place
|
|
@@ -463,7 +502,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
463
502
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
464
503
|
const instance = await getIntegrationInstance.execute(
|
|
465
504
|
params.integrationId,
|
|
466
|
-
user
|
|
505
|
+
await ownerOf(params.integrationId, user)
|
|
467
506
|
);
|
|
468
507
|
|
|
469
508
|
if (!instance) {
|
|
@@ -501,16 +540,21 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
501
540
|
getModule,
|
|
502
541
|
getEntityOptionsById,
|
|
503
542
|
refreshEntityOptions,
|
|
504
|
-
|
|
543
|
+
authorizeEntityForUser,
|
|
544
|
+
createEntityFromCredential,
|
|
505
545
|
} = useCases;
|
|
506
546
|
|
|
507
547
|
router.route('/api/authorize').get(
|
|
508
548
|
catchAsyncError(async (req, res) => {
|
|
509
549
|
const user = await authenticateUser.execute(req);
|
|
510
|
-
const userId = user.getId();
|
|
511
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
|
+
});
|
|
512
556
|
const module = await getModuleInstanceFromType.execute(
|
|
513
|
-
|
|
557
|
+
ownerId,
|
|
514
558
|
params.entityType,
|
|
515
559
|
{ state: req.query.state }
|
|
516
560
|
);
|
|
@@ -529,11 +573,15 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
529
573
|
router.route('/api/authorize').post(
|
|
530
574
|
catchAsyncError(async (req, res) => {
|
|
531
575
|
const user = await authenticateUser.execute(req);
|
|
532
|
-
const userId = user.getId();
|
|
533
576
|
const params = checkRequiredParams(req.body, [
|
|
534
577
|
'entityType',
|
|
535
578
|
'data',
|
|
536
579
|
]);
|
|
580
|
+
const ownership = authorizeEntityForUser.resolveOwnership({
|
|
581
|
+
user,
|
|
582
|
+
entityType: params.entityType,
|
|
583
|
+
});
|
|
584
|
+
const userId = ownership.ownerId;
|
|
537
585
|
|
|
538
586
|
const dataKeys =
|
|
539
587
|
params.data && typeof params.data === 'object'
|
|
@@ -545,11 +593,13 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
545
593
|
dataKeys,
|
|
546
594
|
});
|
|
547
595
|
|
|
548
|
-
const entityDetails =
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
596
|
+
const { result: entityDetails } =
|
|
597
|
+
await authorizeEntityForUser.execute({
|
|
598
|
+
user,
|
|
599
|
+
entityType: params.entityType,
|
|
600
|
+
data: params.data,
|
|
601
|
+
ownership,
|
|
602
|
+
});
|
|
553
603
|
|
|
554
604
|
log.info('Entity authorized', {
|
|
555
605
|
eventName: 'frigg.integrations.authorized',
|
|
@@ -566,35 +616,20 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
566
616
|
router.route('/api/entity').post(
|
|
567
617
|
catchAsyncError(async (req, res) => {
|
|
568
618
|
const user = await authenticateUser.execute(req);
|
|
569
|
-
const userId = user.getId();
|
|
570
619
|
const params = checkRequiredParams(req.body, [
|
|
571
620
|
'entityType',
|
|
572
621
|
'data',
|
|
573
622
|
]);
|
|
574
623
|
checkRequiredParams(req.body.data, ['credential_id']);
|
|
575
624
|
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
})
|
|
580
632
|
);
|
|
581
|
-
|
|
582
|
-
if (!credential) {
|
|
583
|
-
throw Boom.badRequest('Invalid credential ID');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const module = await getModuleInstanceFromType.execute(
|
|
587
|
-
userId,
|
|
588
|
-
params.entityType
|
|
589
|
-
);
|
|
590
|
-
const entityDetails = await module.getEntityDetails(
|
|
591
|
-
module.api,
|
|
592
|
-
null,
|
|
593
|
-
null,
|
|
594
|
-
userId
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
res.json(await module.findOrCreateEntity(entityDetails));
|
|
598
633
|
})
|
|
599
634
|
);
|
|
600
635
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error `findIntegrationById` throws for an id with no row, and the one
|
|
3
|
+
* predicate that recognises it. The adapters build the error here and the
|
|
4
|
+
* use cases match it here, so the message is written once and a missing
|
|
5
|
+
* integration can be told from any other failure without widening a regex.
|
|
6
|
+
*/
|
|
7
|
+
const MESSAGE = /^Integration with id .+ not found$/;
|
|
8
|
+
|
|
9
|
+
const integrationNotFoundError = (id) =>
|
|
10
|
+
new Error(`Integration with id ${id} not found`);
|
|
11
|
+
|
|
12
|
+
const isIntegrationNotFound = (error) => MESSAGE.test(error?.message || '');
|
|
13
|
+
|
|
14
|
+
module.exports = { integrationNotFoundError, isIntegrationNotFound };
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
IntegrationRepositoryInterface,
|
|
15
15
|
} = require('./integration-repository-interface');
|
|
16
16
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
17
|
+
const { integrationNotFoundError } = require('./integration-not-found');
|
|
17
18
|
|
|
18
19
|
class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
19
20
|
constructor() {
|
|
@@ -28,6 +29,28 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
28
29
|
return records.map((doc) => this._mapIntegration(doc));
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Find integrations matching any of the owner/organization pairs, in one
|
|
34
|
+
* query. `organizationId: null` matches rows with no organization (a raw
|
|
35
|
+
* MongoDB `null` also matches a missing field). The caller (a use case)
|
|
36
|
+
* decides which pairs; this only runs them.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
39
|
+
* @returns {Promise<Array>}
|
|
40
|
+
*/
|
|
41
|
+
async findIntegrationsByOwnership(clauses) {
|
|
42
|
+
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
43
|
+
// Drained: a list can hold more than the first cursor batch (~101).
|
|
44
|
+
const records = await findManyDrained(this.prisma, 'Integration', {
|
|
45
|
+
$or: clauses.map(({ userId, organizationId }) => ({
|
|
46
|
+
userId: toObjectId(userId),
|
|
47
|
+
organizationId:
|
|
48
|
+
organizationId === null ? null : toObjectId(organizationId),
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
51
|
+
return records.map((doc) => this._mapIntegration(doc));
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
async findIntegrations({ type, status } = {}) {
|
|
32
55
|
const filter = {};
|
|
33
56
|
if (type) {
|
|
@@ -68,11 +91,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
68
91
|
async findIntegrationById(id) {
|
|
69
92
|
const objectId = toObjectId(id);
|
|
70
93
|
if (!objectId) {
|
|
71
|
-
throw
|
|
94
|
+
throw integrationNotFoundError(id);
|
|
72
95
|
}
|
|
73
96
|
const doc = await findOne(this.prisma, 'Integration', { _id: objectId });
|
|
74
97
|
if (!doc) {
|
|
75
|
-
throw
|
|
98
|
+
throw integrationNotFoundError(id);
|
|
76
99
|
}
|
|
77
100
|
return this._mapIntegration(doc);
|
|
78
101
|
}
|
|
@@ -132,10 +155,18 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
132
155
|
return true;
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
|
|
158
|
+
/**
|
|
159
|
+
* @param {Array<string>} entities - entity ids
|
|
160
|
+
* @param {string} userId - owner (the person, or the organization's user)
|
|
161
|
+
* @param {Object} config - Integration configuration
|
|
162
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the
|
|
163
|
+
* integration is created in (ADR-056 §3.6)
|
|
164
|
+
*/
|
|
165
|
+
async createIntegration(entities, userId, config, tenancy = {}) {
|
|
136
166
|
const now = new Date();
|
|
137
167
|
const document = {
|
|
138
168
|
userId: toObjectId(userId) || null,
|
|
169
|
+
organizationId: toObjectId(tenancy.organizationId) || null,
|
|
139
170
|
config,
|
|
140
171
|
version: '0.0.0',
|
|
141
172
|
status: 'IN_CREATION',
|
|
@@ -298,6 +329,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
298
329
|
id: fromObjectId(doc?._id),
|
|
299
330
|
entitiesIds: (doc?.entityIds || []).map((value) => fromObjectId(value)),
|
|
300
331
|
userId: fromObjectId(doc?.userId),
|
|
332
|
+
organizationId: fromObjectId(doc?.organizationId) ?? null,
|
|
301
333
|
config: doc?.config ?? null,
|
|
302
334
|
version: doc?.version ?? null,
|
|
303
335
|
status: doc?.status ?? null,
|