@friggframework/core 2.0.0--canary.685.a13fdab.0 → 2.0.0--canary.679.a2bcc2f.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 +0 -25
- package/README.md +0 -105
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/user-commands.js +5 -17
- package/credential/use-cases/credential-operations-for-user.js +25 -110
- package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
- package/database/documentdb-utils.js +1 -6
- package/generated/prisma-mongodb/edge.js +4 -15
- package/generated/prisma-mongodb/index-browser.js +1 -12
- package/generated/prisma-mongodb/index.d.ts +86 -2027
- package/generated/prisma-mongodb/index.js +4 -15
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -47
- package/generated/prisma-mongodb/wasm.js +4 -15
- package/generated/prisma-postgresql/edge.js +4 -15
- package/generated/prisma-postgresql/index-browser.js +1 -12
- package/generated/prisma-postgresql/index.d.ts +63 -2162
- package/generated/prisma-postgresql/index.js +4 -15
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -44
- package/generated/prisma-postgresql/wasm.js +4 -15
- package/handlers/app-definition-loader.js +1 -24
- package/index.js +0 -22
- package/integrations/integration-router.js +73 -115
- package/integrations/repositories/integration-repository-documentdb.js +3 -35
- package/integrations/repositories/integration-repository-interface.js +1 -17
- package/integrations/repositories/integration-repository-mongo.js +76 -59
- package/integrations/repositories/integration-repository-postgres.js +76 -62
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/create-integration.js +20 -55
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -5
- package/integrations/use-cases/integration-operations-for-user.js +24 -55
- package/management-api/management-api-config.js +2 -71
- package/management-api/openapi/components.js +0 -40
- package/management-api/openapi/v2-operations.js +4 -16
- package/management-api/v2/dependencies.js +15 -40
- package/management-api/v2/dto.js +0 -20
- package/management-api/v2/handlers/authorization.js +0 -1
- package/management-api/v2/handlers/credentials.js +2 -6
- package/management-api/v2/handlers/entities.js +7 -10
- package/management-api/v2/handlers/integrations.js +3 -6
- package/management-api/v2/request.js +1 -32
- package/modules/index.js +0 -8
- package/modules/repositories/module-repository-documentdb.js +6 -51
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +74 -67
- package/modules/repositories/module-repository-postgres.js +74 -66
- package/modules/repositories/module-repository.js +1 -2
- package/modules/use-cases/entity-operations-for-user.js +13 -50
- package/modules/use-cases/entity-types.js +7 -31
- package/modules/use-cases/execute-entity-proxy-request.js +3 -50
- package/modules/use-cases/get-entities-for-user.js +5 -16
- package/modules/use-cases/get-entity-module-for-user.js +5 -32
- package/modules/use-cases/get-entity-options-by-id.js +3 -9
- package/modules/use-cases/get-module.js +3 -9
- package/modules/use-cases/multi-step-authorization.js +12 -39
- package/modules/use-cases/process-authorization-callback.js +18 -52
- package/modules/use-cases/refresh-entity-options.js +3 -9
- package/modules/use-cases/test-module-auth.js +3 -9
- package/package.json +5 -6
- package/prisma-mongodb/schema.prisma +0 -47
- package/prisma-postgresql/schema.prisma +0 -44
- package/types/index.d.ts +0 -1
- package/types/modules/index.d.ts +0 -129
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/modules/entity-scope.js +0 -15
- package/modules/repositories/entity-not-found.js +0 -18
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -247
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- package/user/use-cases/find-individual-users-by-organization.js +0 -54
|
@@ -2,17 +2,9 @@
|
|
|
2
2
|
const {
|
|
3
3
|
mapIntegrationClassToIntegrationDTO,
|
|
4
4
|
} = require('../utils/map-integration-dto');
|
|
5
|
-
const {
|
|
6
|
-
describeRecord,
|
|
7
|
-
visibilityClauses,
|
|
8
|
-
matchesListFilters,
|
|
9
|
-
} = require('../../modules/scope-visibility');
|
|
10
|
-
const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
|
|
11
5
|
|
|
12
6
|
/**
|
|
13
|
-
* Use case for retrieving
|
|
14
|
-
* or their active organization owns, created in that organization (ADR-056
|
|
15
|
-
* §3.6), each with `tenancy` ({scope, owner, organizationId, access}).
|
|
7
|
+
* Use case for retrieving all integrations for a specific user.
|
|
16
8
|
* @class GetIntegrationsForUser
|
|
17
9
|
*/
|
|
18
10
|
class GetIntegrationsForUser {
|
|
@@ -23,16 +15,13 @@ class GetIntegrationsForUser {
|
|
|
23
15
|
* @param {Array<import('../integration').Integration>} params.integrationClasses - Array of available integration classes.
|
|
24
16
|
* @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management.
|
|
25
17
|
* @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for module and entity data operations.
|
|
26
|
-
* @param {Object} [params.scopePolicy] - from createScopePolicy
|
|
27
18
|
*/
|
|
28
19
|
constructor({
|
|
29
20
|
integrationRepository,
|
|
30
21
|
integrationClasses,
|
|
31
22
|
moduleFactory,
|
|
32
23
|
moduleRepository,
|
|
33
|
-
scopePolicy,
|
|
34
24
|
}) {
|
|
35
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
36
25
|
/**
|
|
37
26
|
* @type {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface}
|
|
38
27
|
*/
|
|
@@ -43,86 +32,57 @@ class GetIntegrationsForUser {
|
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
/**
|
|
35
|
+
* Executes the retrieval of all integrations for a user.
|
|
46
36
|
* @async
|
|
47
|
-
* @param {
|
|
48
|
-
* @
|
|
49
|
-
* @returns {Promise<Object[]>} Array of integration DTOs.
|
|
37
|
+
* @param {string} userId - ID of the user whose integrations to retrieve.
|
|
38
|
+
* @returns {Promise<Object[]>} Array of integration DTOs for the specified user.
|
|
50
39
|
*/
|
|
51
|
-
async execute(
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
visibilityClauses(ctx, 'integration')
|
|
55
|
-
);
|
|
40
|
+
async execute(userId) {
|
|
41
|
+
const integrationRecords =
|
|
42
|
+
await this.integrationRepository.findIntegrationsByUserId(userId);
|
|
56
43
|
|
|
57
44
|
const integrations = [];
|
|
58
|
-
for (const record of records) {
|
|
59
|
-
// The query is the filter; the same rule again means a repository
|
|
60
|
-
// that returns too much cannot widen the list.
|
|
61
|
-
const tenancy = describeRecord(record, ctx, 'integration');
|
|
62
|
-
if (!tenancy || !matchesListFilters(tenancy, filters)) continue;
|
|
63
|
-
integrations.push({ ...(await this.toDto(record)), tenancy });
|
|
64
|
-
}
|
|
65
|
-
return integrations;
|
|
66
|
-
}
|
|
67
45
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const integrationClass = this.integrationClasses.find(
|
|
75
|
-
(integrationClass) =>
|
|
76
|
-
integrationClass.Definition.name ===
|
|
77
|
-
integrationRecord.config.type
|
|
78
|
-
);
|
|
46
|
+
for (const integrationRecord of integrationRecords) {
|
|
47
|
+
const entities = await this.moduleRepository.findEntitiesByIds(
|
|
48
|
+
integrationRecord.entitiesIds
|
|
49
|
+
);
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
integrationRecord.userId
|
|
51
|
+
const integrationClass = this.integrationClasses.find(
|
|
52
|
+
(integrationClass) =>
|
|
53
|
+
integrationClass.Definition.name ===
|
|
54
|
+
integrationRecord.config.type
|
|
85
55
|
);
|
|
86
|
-
modules.push(moduleInstance);
|
|
87
|
-
}
|
|
88
56
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
options: integrationClass.getOptionDetails(),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
57
|
+
const modules = [];
|
|
58
|
+
for (const entity of entities) {
|
|
59
|
+
const moduleInstance =
|
|
60
|
+
await this.moduleFactory.getModuleInstance(
|
|
61
|
+
entity.id,
|
|
62
|
+
integrationRecord.userId
|
|
63
|
+
);
|
|
64
|
+
modules.push(moduleInstance);
|
|
65
|
+
}
|
|
102
66
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
67
|
+
const integrationData = {
|
|
68
|
+
id: integrationRecord.id,
|
|
69
|
+
userId: integrationRecord.userId,
|
|
70
|
+
entities: entities,
|
|
71
|
+
config: integrationRecord.config,
|
|
72
|
+
status: integrationRecord.status,
|
|
73
|
+
version: integrationRecord.version,
|
|
74
|
+
messages: integrationRecord.messages || { errors: [], warnings: [] },
|
|
75
|
+
modules,
|
|
76
|
+
options: integrationClass.getOptionDetails(),
|
|
77
|
+
};
|
|
114
78
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
userId
|
|
119
|
-
);
|
|
120
|
-
const integrations = [];
|
|
121
|
-
for (const record of records) {
|
|
122
|
-
integrations.push(await this.hydrator.toDto(record));
|
|
79
|
+
integrations.push(
|
|
80
|
+
mapIntegrationClassToIntegrationDTO(integrationData)
|
|
81
|
+
);
|
|
123
82
|
}
|
|
83
|
+
|
|
124
84
|
return integrations;
|
|
125
85
|
}
|
|
126
86
|
}
|
|
127
87
|
|
|
128
|
-
module.exports = { GetIntegrationsForUser
|
|
88
|
+
module.exports = { GetIntegrationsForUser };
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
const {
|
|
2
|
-
GetIntegrationsForUser,
|
|
3
|
-
GetIntegrationsByUserId,
|
|
4
|
-
} = require('./get-integrations-for-user');
|
|
1
|
+
const { GetIntegrationsForUser } = require('./get-integrations-for-user');
|
|
5
2
|
const { DeleteIntegrationForUser } = require('./delete-integration-for-user');
|
|
6
3
|
const { CreateIntegration } = require('./create-integration');
|
|
7
4
|
const { GetIntegration } = require('./get-integration');
|
|
@@ -12,7 +9,6 @@ const { GetProcess } = require('./get-process');
|
|
|
12
9
|
|
|
13
10
|
module.exports = {
|
|
14
11
|
GetIntegrationsForUser,
|
|
15
|
-
GetIntegrationsByUserId,
|
|
16
12
|
DeleteIntegrationForUser,
|
|
17
13
|
CreateIntegration,
|
|
18
14
|
GetIntegration,
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
-
const {
|
|
3
|
-
const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
|
|
4
|
-
const { assertCanManage } = require('../../modules/use-cases/manage-access');
|
|
5
|
-
const { isIntegrationNotFound } = require('../repositories/integration-not-found');
|
|
2
|
+
const { userOwns } = require('../../modules/use-cases/get-entity-module-for-user');
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
|
-
* Management API v2 integration operations. They put an
|
|
9
|
-
* (missing or
|
|
10
|
-
* integration use cases, and pass the record's owner id on
|
|
11
|
-
*
|
|
12
|
-
* §3.6), not the owner alone.
|
|
5
|
+
* Management API v2 integration operations. They put an ownership check
|
|
6
|
+
* (missing or foreign integration = 404) in front of the existing
|
|
7
|
+
* integration use cases, and pass the record's owner id on, so the
|
|
8
|
+
* organisation user model works the same as for entities.
|
|
13
9
|
*/
|
|
14
10
|
|
|
15
11
|
const notFound = (integrationId) =>
|
|
@@ -17,61 +13,40 @@ const notFound = (integrationId) =>
|
|
|
17
13
|
code: 'INTEGRATION_NOT_FOUND',
|
|
18
14
|
});
|
|
19
15
|
|
|
20
|
-
/**
|
|
21
|
-
* The integration record, or 404 when it is missing or the caller may not
|
|
22
|
-
* use it: not theirs or their active organization's, or created in another
|
|
23
|
-
* organization.
|
|
24
|
-
*/
|
|
16
|
+
/** The integration record, or 404 when it is missing or not the caller's. */
|
|
25
17
|
class GetOwnedIntegrationRecord {
|
|
26
|
-
constructor({ integrationRepository
|
|
18
|
+
constructor({ integrationRepository }) {
|
|
27
19
|
this.integrationRepository = integrationRepository;
|
|
28
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
|
|
32
|
-
describe(record, user) {
|
|
33
|
-
return describeRecord(record, this.scopePolicy.accessContext(user), 'integration');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @param {string} integrationId
|
|
38
|
-
* @param {Object} user - authenticated User
|
|
39
|
-
* @param {{manage?: boolean}} [options] - `manage: true` for changing or
|
|
40
|
-
* deleting: a person may use an organization's integration but only an
|
|
41
|
-
* admin may change it (403 ORG_ADMIN_REQUIRED)
|
|
42
|
-
*/
|
|
43
|
-
async execute(integrationId, user, { manage = false } = {}) {
|
|
22
|
+
async execute(integrationId, user) {
|
|
44
23
|
let record = null;
|
|
45
24
|
try {
|
|
46
25
|
record = await this.integrationRepository.findIntegrationById(
|
|
47
26
|
integrationId
|
|
48
27
|
);
|
|
49
28
|
} catch (error) {
|
|
50
|
-
if (
|
|
51
|
-
/Invalid ID|ObjectId|Malformed/i.test(error?.message || '') ||
|
|
52
|
-
isIntegrationNotFound(error)
|
|
53
|
-
) {
|
|
29
|
+
if (/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
|
|
54
30
|
throw notFound(integrationId);
|
|
55
31
|
}
|
|
56
32
|
throw error;
|
|
57
33
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
34
|
+
if (!record || !userOwns(user, record.userId)) {
|
|
35
|
+
throw notFound(integrationId);
|
|
36
|
+
}
|
|
61
37
|
return record;
|
|
62
38
|
}
|
|
63
39
|
}
|
|
64
40
|
|
|
65
41
|
/**
|
|
66
|
-
* Creates (or reuses) an integration between entities the caller
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* paired with an organization by `resolveIntegrationOwnership` (§3, §3.6).
|
|
42
|
+
* Creates (or reuses) an integration between entities the caller owns.
|
|
43
|
+
* Unlike v1, the entities' ownership and the integration type are checked
|
|
44
|
+
* before anything is written.
|
|
70
45
|
*/
|
|
71
46
|
class CreateIntegrationForUser {
|
|
72
|
-
constructor({ createIntegration,
|
|
47
|
+
constructor({ createIntegration, getEntityModuleForUser, integrationClasses }) {
|
|
73
48
|
this.createIntegration = createIntegration;
|
|
74
|
-
this.
|
|
49
|
+
this.getEntityModuleForUser = getEntityModuleForUser;
|
|
75
50
|
this.integrationClasses = integrationClasses || [];
|
|
76
51
|
}
|
|
77
52
|
|
|
@@ -99,17 +74,13 @@ class CreateIntegrationForUser {
|
|
|
99
74
|
details: { field: 'entities' },
|
|
100
75
|
});
|
|
101
76
|
}
|
|
102
|
-
const
|
|
103
|
-
await this.
|
|
104
|
-
|
|
105
|
-
config,
|
|
106
|
-
user,
|
|
107
|
-
});
|
|
77
|
+
for (const entityId of entities) {
|
|
78
|
+
await this.getEntityModuleForUser.loadOwnedEntity(String(entityId), user);
|
|
79
|
+
}
|
|
108
80
|
return this.createIntegration.execute(
|
|
109
81
|
entities.map(String),
|
|
110
|
-
|
|
111
|
-
config
|
|
112
|
-
{ organizationId, adoptUnscoped }
|
|
82
|
+
user.getId(),
|
|
83
|
+
config
|
|
113
84
|
);
|
|
114
85
|
}
|
|
115
86
|
}
|
|
@@ -146,8 +117,7 @@ class UpdateIntegrationForUser {
|
|
|
146
117
|
}
|
|
147
118
|
const record = await this.getOwnedIntegrationRecord.execute(
|
|
148
119
|
integrationId,
|
|
149
|
-
user
|
|
150
|
-
{ manage: true }
|
|
120
|
+
user
|
|
151
121
|
);
|
|
152
122
|
return this.updateIntegration.execute(record.id, record.userId, config);
|
|
153
123
|
}
|
|
@@ -163,8 +133,7 @@ class RemoveIntegrationForUser {
|
|
|
163
133
|
async execute(integrationId, user) {
|
|
164
134
|
const record = await this.getOwnedIntegrationRecord.execute(
|
|
165
135
|
integrationId,
|
|
166
|
-
user
|
|
167
|
-
{ manage: true }
|
|
136
|
+
user
|
|
168
137
|
);
|
|
169
138
|
await this.deleteIntegrationForUser.execute(record.id, record.userId);
|
|
170
139
|
}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* timeoutMs: 25000, // whole upstream call, lower only
|
|
10
10
|
* maxRequestBytes: 1048576,
|
|
11
11
|
* maxResponseBytes: 5242880, // lower only
|
|
12
|
-
* scopes: ['individual'], // entity scopes the proxy accepts (ADR-056 §3.5)
|
|
13
12
|
* modules: { // per-module allow-list override
|
|
14
13
|
* hubspot: { allow: 'module' },
|
|
15
14
|
* erp: { allow: [{ method: 'GET', path: '/v1/customers/**' }] },
|
|
@@ -17,36 +16,7 @@
|
|
|
17
16
|
* },
|
|
18
17
|
* }
|
|
19
18
|
* ```
|
|
20
|
-
*
|
|
21
|
-
* Limits that are out of range fall back to their defaults. `proxy.scopes` is a
|
|
22
|
-
* security setting, so it does not fall back: a value that is not a list of
|
|
23
|
-
* known tenant scopes throws ManagementApiConfigError, and loadAppDefinition
|
|
24
|
-
* calls this so the app stops at load.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const { BaseError } = require('../errors/base-error');
|
|
28
|
-
const { SCOPES } = require('../modules/scope-resolution');
|
|
29
|
-
|
|
30
|
-
/** Raised for an invalid `appDefinition.managementApi` setting. */
|
|
31
|
-
class ManagementApiConfigError extends BaseError {
|
|
32
|
-
constructor(message, options) {
|
|
33
|
-
super(message, options);
|
|
34
|
-
this.code = 'MANAGEMENT_API_CONFIG_INVALID';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The scopes `managementApi.proxy.scopes` may list. `global` is not one of
|
|
40
|
-
* them: a global entity is the app owner's and is never a tenant's to proxy.
|
|
41
19
|
*/
|
|
42
|
-
const PROXY_SCOPE_VALUES = Object.freeze([SCOPES.INDIVIDUAL, SCOPES.ORGANIZATION]);
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Narrowest by default (ADR-056 §3.5): proxying through a shared credential
|
|
46
|
-
* lets any member act on the organization's account, so an app opts in to
|
|
47
|
-
* `organization` explicitly.
|
|
48
|
-
*/
|
|
49
|
-
const DEFAULT_PROXY_SCOPES = Object.freeze([SCOPES.INDIVIDUAL]);
|
|
50
20
|
|
|
51
21
|
// ADR-052 §7. API Gateway HTTP APIs time out at 30s and Lambda responses cap
|
|
52
22
|
// at 6MB, so the timeout and response defaults are also ceilings: an app may
|
|
@@ -74,41 +44,9 @@ function positiveOr(value, fallback, ceiling) {
|
|
|
74
44
|
return isPositiveNumber(value) ? Math.min(value, ceiling) : fallback;
|
|
75
45
|
}
|
|
76
46
|
|
|
77
|
-
const proxyScopesError = (message) =>
|
|
78
|
-
new ManagementApiConfigError(`managementApi.proxy.scopes: ${message}`);
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* `managementApi.proxy.scopes`: absent means the default; anything else must be
|
|
82
|
-
* a non-empty list of PROXY_SCOPE_VALUES, each once.
|
|
83
|
-
* @returns {ReadonlyArray<string>}
|
|
84
|
-
* @throws {ManagementApiConfigError}
|
|
85
|
-
*/
|
|
86
|
-
function resolveProxyScopes(scopes) {
|
|
87
|
-
if (scopes === undefined) return DEFAULT_PROXY_SCOPES;
|
|
88
|
-
const allowed = PROXY_SCOPE_VALUES.join(', ');
|
|
89
|
-
if (!Array.isArray(scopes) || scopes.length === 0) {
|
|
90
|
-
throw proxyScopesError(`must be a non-empty list drawn from ${allowed}`);
|
|
91
|
-
}
|
|
92
|
-
for (const scope of scopes) {
|
|
93
|
-
if (scope === SCOPES.GLOBAL) {
|
|
94
|
-
throw proxyScopesError(
|
|
95
|
-
"'global' is not allowed: global entities belong to the app owner and are never proxied for a tenant"
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
if (!PROXY_SCOPE_VALUES.includes(scope)) {
|
|
99
|
-
throw proxyScopesError(`unknown scope ${JSON.stringify(scope)}; use ${allowed}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (new Set(scopes).size !== scopes.length) {
|
|
103
|
-
throw proxyScopesError('lists a scope more than once');
|
|
104
|
-
}
|
|
105
|
-
return Object.freeze([...scopes]);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
47
|
/**
|
|
109
48
|
* @param {Object} [managementApi] - `appDefinition.managementApi`
|
|
110
|
-
* @returns {{ v1: boolean, proxy: { enable: boolean, timeoutMs: number, maxRequestBytes: number, maxResponseBytes: number,
|
|
111
|
-
* @throws {ManagementApiConfigError} for an invalid `proxy.scopes`
|
|
49
|
+
* @returns {{ v1: boolean, proxy: { enable: boolean, timeoutMs: number, maxRequestBytes: number, maxResponseBytes: number, modules: Object } }}
|
|
112
50
|
*/
|
|
113
51
|
function resolveManagementApiConfig(managementApi) {
|
|
114
52
|
const config =
|
|
@@ -133,7 +71,6 @@ function resolveManagementApiConfig(managementApi) {
|
|
|
133
71
|
proxy.maxResponseBytes,
|
|
134
72
|
DEFAULT_PROXY_LIMITS.maxResponseBytes
|
|
135
73
|
),
|
|
136
|
-
scopes: resolveProxyScopes(proxy.scopes),
|
|
137
74
|
modules:
|
|
138
75
|
proxy.modules && typeof proxy.modules === 'object'
|
|
139
76
|
? proxy.modules
|
|
@@ -142,10 +79,4 @@ function resolveManagementApiConfig(managementApi) {
|
|
|
142
79
|
};
|
|
143
80
|
}
|
|
144
81
|
|
|
145
|
-
module.exports = {
|
|
146
|
-
resolveManagementApiConfig,
|
|
147
|
-
DEFAULT_PROXY_LIMITS,
|
|
148
|
-
DEFAULT_PROXY_SCOPES,
|
|
149
|
-
PROXY_SCOPE_VALUES,
|
|
150
|
-
ManagementApiConfigError,
|
|
151
|
-
};
|
|
82
|
+
module.exports = { resolveManagementApiConfig, DEFAULT_PROXY_LIMITS };
|
|
@@ -8,27 +8,6 @@ const ref = (name) => ({ $ref: `#/components/schemas/${name}` });
|
|
|
8
8
|
|
|
9
9
|
const nullable = (schema) => ({ ...schema, nullable: true });
|
|
10
10
|
|
|
11
|
-
// Whose a record is and what the caller may do with it (ADR-056 §3.4). Added to
|
|
12
|
-
// entities, credentials and integrations by the list and get use cases.
|
|
13
|
-
const tenancyProperties = {
|
|
14
|
-
scope: { type: 'string', enum: ['individual', 'organization'], description: 'Whose it is. `global` records are not shown to tenants.' },
|
|
15
|
-
owner: {
|
|
16
|
-
type: 'object',
|
|
17
|
-
properties: { type: { type: 'string', enum: ['individual', 'organization'] }, id: { type: 'string' } },
|
|
18
|
-
description: "The user that owns it: the person, or the organization's user",
|
|
19
|
-
},
|
|
20
|
-
organizationId: nullable({
|
|
21
|
-
type: 'string',
|
|
22
|
-
description:
|
|
23
|
-
'The organization it belongs to. Null for an individual entity connected outside an organization, or under ownership.individualAcross "all".',
|
|
24
|
-
}),
|
|
25
|
-
access: {
|
|
26
|
-
type: 'string',
|
|
27
|
-
enum: ['manage', 'use', 'view'],
|
|
28
|
-
description: 'manage (create, reauthorize, delete), use (bind it, proxy through it) or view (metadata only)',
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
|
|
32
11
|
const errorBody = {
|
|
33
12
|
type: 'object',
|
|
34
13
|
required: ['error'],
|
|
@@ -90,7 +69,6 @@ const SCHEMAS = {
|
|
|
90
69
|
config: { type: 'object', additionalProperties: true },
|
|
91
70
|
version: nullable({ type: 'string' }),
|
|
92
71
|
messages: { type: 'object', additionalProperties: true },
|
|
93
|
-
...tenancyProperties,
|
|
94
72
|
},
|
|
95
73
|
},
|
|
96
74
|
ListIntegrationsResponse: {
|
|
@@ -151,7 +129,6 @@ const SCHEMAS = {
|
|
|
151
129
|
credentialId: nullable({ type: 'string' }),
|
|
152
130
|
userId: nullable({ type: 'string' }),
|
|
153
131
|
authIsValid: nullable({ type: 'boolean' }),
|
|
154
|
-
...tenancyProperties,
|
|
155
132
|
},
|
|
156
133
|
},
|
|
157
134
|
ListEntitiesResponse: {
|
|
@@ -176,16 +153,6 @@ const SCHEMAS = {
|
|
|
176
153
|
isMultiStep: { type: 'boolean' },
|
|
177
154
|
stepCount: { type: 'integer', minimum: 1 },
|
|
178
155
|
capabilities: {},
|
|
179
|
-
scope: {
|
|
180
|
-
type: 'object',
|
|
181
|
-
description:
|
|
182
|
-
'The scope a new connection of this type gets and the scopes a request may choose (ADR-056 §3.1). `global` is never offered.',
|
|
183
|
-
properties: {
|
|
184
|
-
default: { type: 'string', enum: ['individual', 'organization', 'global'] },
|
|
185
|
-
allow: { type: 'array', items: { type: 'string', enum: ['individual', 'organization'] } },
|
|
186
|
-
source: { type: 'string', enum: ['binding', 'integration', 'entityType', 'app', 'default'] },
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
156
|
},
|
|
190
157
|
},
|
|
191
158
|
ListEntityTypesResponse: {
|
|
@@ -206,7 +173,6 @@ const SCHEMAS = {
|
|
|
206
173
|
authIsValid: nullable({ type: 'boolean' }),
|
|
207
174
|
entityIds: { type: 'array', items: { type: 'string' } },
|
|
208
175
|
entityCount: { type: 'integer', minimum: 0 },
|
|
209
|
-
...tenancyProperties,
|
|
210
176
|
createdAt: nullable({ type: 'string', format: 'date-time' }),
|
|
211
177
|
updatedAt: nullable({ type: 'string', format: 'date-time' }),
|
|
212
178
|
data: {
|
|
@@ -231,12 +197,6 @@ const SCHEMAS = {
|
|
|
231
197
|
data: { type: 'object', additionalProperties: true, description: 'OAuth callback params or step form data' },
|
|
232
198
|
step: { type: 'integer', minimum: 1, default: 1 },
|
|
233
199
|
sessionId: { type: 'string', description: 'Required for step 2 and later' },
|
|
234
|
-
scope: {
|
|
235
|
-
type: 'string',
|
|
236
|
-
enum: ['individual', 'organization'],
|
|
237
|
-
description:
|
|
238
|
-
"The scope to connect at, from the entity type's allow list (ADR-056 §3.1). Omit to take the type's default. `global` is refused (403).",
|
|
239
|
-
},
|
|
240
200
|
},
|
|
241
201
|
},
|
|
242
202
|
ReauthorizeRequest: {
|
|
@@ -34,11 +34,6 @@ const stepQuery = [
|
|
|
34
34
|
query('step', 'Step number, 1-based (default 1)', { type: 'integer', minimum: 1 }),
|
|
35
35
|
query('sessionId', 'Session from step 1; required for later steps'),
|
|
36
36
|
];
|
|
37
|
-
// ADR-056 §3.4: the same two filters on every list that mixes ownership.
|
|
38
|
-
const listFilters = [
|
|
39
|
-
query('scope', 'Only records of this scope', { type: 'string', enum: ['individual', 'organization'] }),
|
|
40
|
-
query('owner', 'Only the caller\'s own (`me`) or the active organization\'s (`org`)', { type: 'string', enum: ['me', 'org'] }),
|
|
41
|
-
];
|
|
42
37
|
const anyJson = { description: 'Defined by the integration or module' };
|
|
43
38
|
|
|
44
39
|
const V2_OPERATIONS = {
|
|
@@ -46,8 +41,7 @@ const V2_OPERATIONS = {
|
|
|
46
41
|
listIntegrations: {
|
|
47
42
|
summary: "List the caller's integrations",
|
|
48
43
|
description:
|
|
49
|
-
'Returns only `{ integrations }
|
|
50
|
-
parameters: listFilters,
|
|
44
|
+
'Returns only `{ integrations }`. Integration types are at /api/v2/integrations/options and connected accounts at /api/v2/entities.',
|
|
51
45
|
responses: { 200: ok('Integrations', ref('ListIntegrationsResponse')), ...errors('Unauthorized') },
|
|
52
46
|
},
|
|
53
47
|
listIntegrationOptions: {
|
|
@@ -114,9 +108,6 @@ const V2_OPERATIONS = {
|
|
|
114
108
|
// Entities
|
|
115
109
|
listEntities: {
|
|
116
110
|
summary: "List the caller's connected entities",
|
|
117
|
-
description:
|
|
118
|
-
"The active organization's entities and the caller's individual ones (ADR-056 §3.6): an individual entity belongs to the organization it was connected in, unless `ownership.individualAcross` is `all`. Each carries `scope`, `owner`, `organizationId` and `access`.",
|
|
119
|
-
parameters: listFilters,
|
|
120
111
|
responses: { 200: ok('Entities', ref('ListEntitiesResponse')), ...errors('Unauthorized') },
|
|
121
112
|
},
|
|
122
113
|
listEntityTypes: {
|
|
@@ -165,14 +156,14 @@ const V2_OPERATIONS = {
|
|
|
165
156
|
proxyEntityRequest: {
|
|
166
157
|
summary: "Call the entity's API through Frigg (beta)",
|
|
167
158
|
description:
|
|
168
|
-
"ADR-052. Off unless the app sets managementApi.proxy.enable. Path-only input relative to the module's base URL; deny by default, only operations in the module's (or the app's) allow-list.
|
|
159
|
+
"ADR-052. Off unless the app sets managementApi.proxy.enable. Path-only input relative to the module's base URL; deny by default, only operations in the module's (or the app's) allow-list. The HTTP status follows the upstream status; upstream 5xx is 502 UPSTREAM_ERROR, 429 keeps Retry-After.",
|
|
169
160
|
requestBody: body(ref('ProxyRequest')),
|
|
170
161
|
responses: {
|
|
171
162
|
200: ok('Upstream answer (the status mirrors the upstream status)', ref('ProxyResponse')),
|
|
172
163
|
400: ok('INVALID_PROXY_REQUEST', ref('ProxyError')),
|
|
173
164
|
401: ok('INVALID_CREDENTIALS or unauthenticated', ref('ProxyError')),
|
|
174
|
-
403: ok(
|
|
175
|
-
404: ok('Entity not found
|
|
165
|
+
403: ok('PROXY_NOT_ALLOWED', ref('ProxyError')),
|
|
166
|
+
404: ok('Entity not found', ref('Error')),
|
|
176
167
|
413: ok('PAYLOAD_TOO_LARGE', ref('ProxyError')),
|
|
177
168
|
415: ok('UNSUPPORTED_MEDIA_TYPE', ref('ProxyError')),
|
|
178
169
|
429: ok('RATE_LIMITED (Retry-After passed through)', ref('ProxyError')),
|
|
@@ -184,9 +175,6 @@ const V2_OPERATIONS = {
|
|
|
184
175
|
// Credentials
|
|
185
176
|
listCredentials: {
|
|
186
177
|
summary: "List the caller's credentials (masked)",
|
|
187
|
-
description:
|
|
188
|
-
'A credential is listed through the entities that use it that the caller may see, so each organization lists only its own connections; one no entity uses is listed to its owner. Each carries `scope`, `owner`, `organizationId` and `access`, taken from who owns the credential.',
|
|
189
|
-
parameters: listFilters,
|
|
190
178
|
responses: { 200: ok('Credentials', ref('ListCredentialsResponse')), ...errors('Unauthorized') },
|
|
191
179
|
},
|
|
192
180
|
getCredential: {
|