@friggframework/core 2.0.0--canary.704.79d9a63.0 → 2.0.0--canary.686.e94de4d.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +28 -40
- package/README.md +105 -0
- package/application/commands/entity-commands.js +60 -18
- package/application/commands/integration-commands.js +20 -73
- 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-encryption-service.js +9 -22
- package/database/documentdb-utils.js +2 -0
- package/database/encryption/README.md +76 -14
- package/database/encryption/documentdb-encryption-service.md +78 -1
- package/database/encryption/encryption-config.js +219 -0
- package/database/encryption/encryption-schema-registry.js +93 -12
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/organization-where.js +18 -0
- package/database/prisma.js +15 -38
- package/database/unique-violation.js +25 -0
- package/database/use-cases/check-encryption-health-use-case.js +43 -52
- package/errors/index.js +0 -4
- 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/WEBHOOKS.md +0 -29
- 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 -3
- 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-base.js +0 -29
- package/integrations/integration-router.js +131 -96
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -35
- package/integrations/repositories/integration-repository-interface.js +17 -31
- package/integrations/repositories/integration-repository-mongo.js +60 -129
- package/integrations/repositories/integration-repository-postgres.js +62 -134
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +72 -75
- 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/modules/utils/map-module-dto.js +3 -5
- 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/tests/doubles/test-user-repository.js +1 -1
- 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-bearer-token.js +2 -7
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/errors/mapping-already-exists-error.js +0 -14
- package/integrations/use-cases/add-entities-to-integration.js +0 -92
- package/integrations/use-cases/remove-entities-from-integration.js +0 -70
- package/integrations/utils/assert-entity-ids-input.js +0 -42
- package/modules/utils/to-credential-id.js +0 -14
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response shapes for Management API v2. Use cases return domain records;
|
|
3
|
+
* these functions decide what leaves the server. Credential material never
|
|
4
|
+
* does (see maskCredential).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const idOf = (value) => {
|
|
8
|
+
if (value === undefined || value === null) return null;
|
|
9
|
+
if (typeof value === 'object') {
|
|
10
|
+
const id = value.id ?? value._id;
|
|
11
|
+
return id === undefined || id === null ? null : String(id);
|
|
12
|
+
}
|
|
13
|
+
return String(value);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `{ scope, owner, organizationId, access }` (ADR-056 §3.4): whose a record is
|
|
18
|
+
* and what the caller may do with it. Empty when the use case did not say.
|
|
19
|
+
*/
|
|
20
|
+
function tenancyFields(tenancy) {
|
|
21
|
+
if (!tenancy) return {};
|
|
22
|
+
return {
|
|
23
|
+
scope: tenancy.scope,
|
|
24
|
+
owner: tenancy.owner,
|
|
25
|
+
organizationId: tenancy.organizationId ?? null,
|
|
26
|
+
access: tenancy.access,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** An entity (connected account). */
|
|
31
|
+
function toEntityDto(entity) {
|
|
32
|
+
const credential =
|
|
33
|
+
entity.credential && typeof entity.credential === 'object'
|
|
34
|
+
? entity.credential
|
|
35
|
+
: null;
|
|
36
|
+
return {
|
|
37
|
+
id: String(entity.id),
|
|
38
|
+
type: entity.moduleName ?? null,
|
|
39
|
+
name: entity.name ?? null,
|
|
40
|
+
externalId: entity.externalId ?? null,
|
|
41
|
+
credentialId: idOf(entity.credential ?? entity.credentialId),
|
|
42
|
+
userId: idOf(entity.userId ?? entity.user),
|
|
43
|
+
authIsValid:
|
|
44
|
+
typeof credential?.authIsValid === 'boolean'
|
|
45
|
+
? credential.authIsValid
|
|
46
|
+
: null,
|
|
47
|
+
...tenancyFields(entity.tenancy),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An integration, from a hydrated instance or a stored DTO. */
|
|
52
|
+
function toIntegrationDto(integration) {
|
|
53
|
+
return {
|
|
54
|
+
id: String(integration.id),
|
|
55
|
+
entities: integration.entities ?? [],
|
|
56
|
+
status: integration.status ?? null,
|
|
57
|
+
config: integration.config ?? {},
|
|
58
|
+
version: integration.version ?? null,
|
|
59
|
+
messages: integration.messages ?? {},
|
|
60
|
+
...tenancyFields(integration.tenancy),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fields every repository adds around the stored data; everything else on a
|
|
65
|
+
// credential record came from the provider and is treated as secret.
|
|
66
|
+
const CREDENTIAL_METADATA = new Set([
|
|
67
|
+
'id',
|
|
68
|
+
'_id',
|
|
69
|
+
'userId',
|
|
70
|
+
'user',
|
|
71
|
+
'externalId',
|
|
72
|
+
'authIsValid',
|
|
73
|
+
'createdAt',
|
|
74
|
+
'updatedAt',
|
|
75
|
+
'type',
|
|
76
|
+
'entityIds',
|
|
77
|
+
'status',
|
|
78
|
+
// Added by the credential use cases (ADR-056 §3.4); never provider data.
|
|
79
|
+
'tenancy',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Masks one stored credential value. Strings keep at most their last four
|
|
84
|
+
* characters, and only when long enough that four characters reveal nothing
|
|
85
|
+
* useful; objects and arrays are hidden whole. Numbers, booleans and null
|
|
86
|
+
* (expiry times, flags) are not secret and pass through.
|
|
87
|
+
*/
|
|
88
|
+
function maskValue(value) {
|
|
89
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean') {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === 'string') {
|
|
93
|
+
return value.length >= 16 ? `****${value.slice(-4)}` : '****';
|
|
94
|
+
}
|
|
95
|
+
return '****';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const toIso = (value) => {
|
|
99
|
+
if (!value) return null;
|
|
100
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
101
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A credential with every provider field masked. The keys stay visible so a
|
|
106
|
+
* client can tell an OAuth credential from an API-key one; the values never
|
|
107
|
+
* leave the server.
|
|
108
|
+
*/
|
|
109
|
+
function maskCredential(credential) {
|
|
110
|
+
const data = {};
|
|
111
|
+
for (const [key, value] of Object.entries(credential)) {
|
|
112
|
+
if (CREDENTIAL_METADATA.has(key) || value === undefined) continue;
|
|
113
|
+
data[key] = maskValue(value);
|
|
114
|
+
}
|
|
115
|
+
const entityIds = Array.isArray(credential.entityIds)
|
|
116
|
+
? credential.entityIds.map(String)
|
|
117
|
+
: [];
|
|
118
|
+
return {
|
|
119
|
+
id: String(credential.id),
|
|
120
|
+
type: credential.type ?? null,
|
|
121
|
+
externalId: credential.externalId ?? null,
|
|
122
|
+
userId: idOf(credential.userId),
|
|
123
|
+
authIsValid:
|
|
124
|
+
typeof credential.authIsValid === 'boolean'
|
|
125
|
+
? credential.authIsValid
|
|
126
|
+
: null,
|
|
127
|
+
entityIds,
|
|
128
|
+
entityCount: entityIds.length,
|
|
129
|
+
createdAt: toIso(credential.createdAt),
|
|
130
|
+
updatedAt: toIso(credential.updatedAt),
|
|
131
|
+
...tenancyFields(credential.tenancy),
|
|
132
|
+
data,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
tenancyFields,
|
|
138
|
+
toEntityDto,
|
|
139
|
+
toIntegrationDto,
|
|
140
|
+
maskCredential,
|
|
141
|
+
maskValue,
|
|
142
|
+
idOf,
|
|
143
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const { toEntityDto, maskCredential } = require('../dto');
|
|
2
|
+
const { objectBody, requireFields, parseStep, optionalString } = require('../request');
|
|
3
|
+
|
|
4
|
+
/** The v2 body for a submitted step: masked records when complete. */
|
|
5
|
+
function toAuthorizationResponse(result) {
|
|
6
|
+
if (result.status !== 'complete') return result;
|
|
7
|
+
return {
|
|
8
|
+
status: 'complete',
|
|
9
|
+
entity: result.entity ? toEntityDto(result.entity) : null,
|
|
10
|
+
credential: result.credential ? maskCredential(result.credential) : null,
|
|
11
|
+
...(result.previousCredentialId && {
|
|
12
|
+
previousCredentialId: result.previousCredentialId,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Management API v2 authorization handlers (multi-step authorize and
|
|
19
|
+
* credential re-authorization), keyed by operationId.
|
|
20
|
+
*/
|
|
21
|
+
function createAuthorizationHandlers(useCases) {
|
|
22
|
+
const {
|
|
23
|
+
getAuthorizationStep,
|
|
24
|
+
submitAuthorizationStep,
|
|
25
|
+
getCredentialReauthorization,
|
|
26
|
+
reauthorizeCredentialForUser,
|
|
27
|
+
} = useCases;
|
|
28
|
+
|
|
29
|
+
const stepQuery = (req) => ({
|
|
30
|
+
step: parseStep(req.query.step),
|
|
31
|
+
sessionId: optionalString(req.query.sessionId, 'sessionId'),
|
|
32
|
+
state: optionalString(req.query.state, 'state'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const stepBody = (req) => {
|
|
36
|
+
const body = objectBody(req);
|
|
37
|
+
return {
|
|
38
|
+
body,
|
|
39
|
+
step: parseStep(body.step),
|
|
40
|
+
sessionId: optionalString(body.sessionId, 'sessionId'),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
async getAuthorizationRequirements(req, res, user) {
|
|
46
|
+
const entityType = optionalString(req.query.entityType, 'entityType');
|
|
47
|
+
requireFields({ entityType }, ['entityType']);
|
|
48
|
+
res.json(
|
|
49
|
+
await getAuthorizationStep.execute({
|
|
50
|
+
user,
|
|
51
|
+
entityType,
|
|
52
|
+
...stepQuery(req),
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async authorize(req, res, user) {
|
|
58
|
+
const { body, step, sessionId } = stepBody(req);
|
|
59
|
+
requireFields(body, ['entityType', 'data']);
|
|
60
|
+
const result = await submitAuthorizationStep.execute({
|
|
61
|
+
user,
|
|
62
|
+
entityType: optionalString(body.entityType, 'entityType'),
|
|
63
|
+
data: body.data,
|
|
64
|
+
step,
|
|
65
|
+
sessionId,
|
|
66
|
+
scope: optionalString(body.scope, 'scope'),
|
|
67
|
+
});
|
|
68
|
+
res.json(toAuthorizationResponse(result));
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async getCredentialReauthorizeRequirements(req, res, user) {
|
|
72
|
+
res.json(
|
|
73
|
+
await getCredentialReauthorization.execute(
|
|
74
|
+
req.params.credentialId,
|
|
75
|
+
user,
|
|
76
|
+
stepQuery(req)
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async reauthorizeCredential(req, res, user) {
|
|
82
|
+
const { body, step, sessionId } = stepBody(req);
|
|
83
|
+
requireFields(body, ['data']);
|
|
84
|
+
const result = await reauthorizeCredentialForUser.execute(
|
|
85
|
+
req.params.credentialId,
|
|
86
|
+
user,
|
|
87
|
+
{ data: body.data, step, sessionId }
|
|
88
|
+
);
|
|
89
|
+
res.json(toAuthorizationResponse(result));
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { createAuthorizationHandlers, toAuthorizationResponse };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { maskCredential } = require('../dto');
|
|
2
|
+
const { parseListFilters } = require('../request');
|
|
3
|
+
|
|
4
|
+
/** Management API v2 credential handlers, keyed by operationId. */
|
|
5
|
+
function createCredentialHandlers(useCases) {
|
|
6
|
+
const { listCredentialsForUser, getCredentialForUser, deleteCredentialForUser } =
|
|
7
|
+
useCases;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
async listCredentials(req, res, user) {
|
|
11
|
+
const credentials = await listCredentialsForUser.execute(
|
|
12
|
+
user,
|
|
13
|
+
parseListFilters(req.query)
|
|
14
|
+
);
|
|
15
|
+
res.json({ credentials: credentials.map(maskCredential) });
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
async getCredential(req, res, user) {
|
|
19
|
+
res.json(
|
|
20
|
+
maskCredential(
|
|
21
|
+
await getCredentialForUser.execute(req.params.credentialId, user)
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async deleteCredential(req, res, user) {
|
|
27
|
+
await deleteCredentialForUser.execute(req.params.credentialId, user);
|
|
28
|
+
res.status(204).end();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { createCredentialHandlers };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const { toEntityDto } = require('../dto');
|
|
2
|
+
const { objectBody, parseStep, parseListFilters } = require('../request');
|
|
3
|
+
|
|
4
|
+
/** Management API v2 entity and entity-type handlers, keyed by operationId. */
|
|
5
|
+
function createEntityHandlers(useCases) {
|
|
6
|
+
const {
|
|
7
|
+
listEntitiesForUser,
|
|
8
|
+
getEntityForUser,
|
|
9
|
+
deleteEntityForUser,
|
|
10
|
+
testEntityAuthForUser,
|
|
11
|
+
getEntityOptionsForUser,
|
|
12
|
+
refreshEntityOptionsForUser,
|
|
13
|
+
listEntityTypes,
|
|
14
|
+
getEntityType,
|
|
15
|
+
getAuthorizationRequirements,
|
|
16
|
+
} = useCases;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
async listEntities(req, res, user) {
|
|
20
|
+
const entities = await listEntitiesForUser.execute(
|
|
21
|
+
user,
|
|
22
|
+
parseListFilters(req.query)
|
|
23
|
+
);
|
|
24
|
+
res.json({ entities: entities.map(toEntityDto) });
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async listEntityTypes(_req, res, user) {
|
|
28
|
+
res.json({ types: await listEntityTypes.execute({ user }) });
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async getEntityType(req, res, user) {
|
|
32
|
+
res.json(await getEntityType.execute(req.params.entityType, { user }));
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Describes a step's requirements without starting an authorization
|
|
36
|
+
// session; GET /api/v2/authorize starts one.
|
|
37
|
+
async getEntityTypeRequirements(req, res, user) {
|
|
38
|
+
res.json(
|
|
39
|
+
await getAuthorizationRequirements.execute({
|
|
40
|
+
entityType: req.params.entityType,
|
|
41
|
+
step: parseStep(req.query.step),
|
|
42
|
+
userId: user.getId(),
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async getEntity(req, res, user) {
|
|
48
|
+
res.json(toEntityDto(await getEntityForUser.execute(req.params.entityId, user)));
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async deleteEntity(req, res, user) {
|
|
52
|
+
await deleteEntityForUser.execute(req.params.entityId, user);
|
|
53
|
+
res.status(204).end();
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async testEntityAuth(req, res, user) {
|
|
57
|
+
const ok = await testEntityAuthForUser.execute(req.params.entityId, user);
|
|
58
|
+
res.json({ status: ok ? 'ok' : 'failed' });
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async getEntityOptions(req, res, user) {
|
|
62
|
+
res.json(await getEntityOptionsForUser.execute(req.params.entityId, user));
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async refreshEntityOptions(req, res, user) {
|
|
66
|
+
res.json(
|
|
67
|
+
await refreshEntityOptionsForUser.execute(
|
|
68
|
+
req.params.entityId,
|
|
69
|
+
user,
|
|
70
|
+
objectBody(req)
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { createEntityHandlers };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { toIntegrationDto } = require('../dto');
|
|
2
|
+
const { objectBody, optionalString, parseListFilters } = require('../request');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Management API v2 integration handlers, keyed by registry operationId.
|
|
6
|
+
* Each takes (req, res, user) after authentication and only calls use cases.
|
|
7
|
+
*/
|
|
8
|
+
function createIntegrationHandlers(useCases) {
|
|
9
|
+
const {
|
|
10
|
+
getIntegrationsForUser,
|
|
11
|
+
getPossibleIntegrations,
|
|
12
|
+
createIntegrationForUser,
|
|
13
|
+
getIntegrationInstanceForUser,
|
|
14
|
+
updateIntegrationForUser,
|
|
15
|
+
removeIntegrationForUser,
|
|
16
|
+
sendIntegrationEventForUser,
|
|
17
|
+
runIntegrationActionForUser,
|
|
18
|
+
testIntegrationAuthForUser,
|
|
19
|
+
} = useCases;
|
|
20
|
+
|
|
21
|
+
const send = (req, user, event, payload) =>
|
|
22
|
+
sendIntegrationEventForUser.execute(
|
|
23
|
+
req.params.integrationId,
|
|
24
|
+
user,
|
|
25
|
+
event,
|
|
26
|
+
payload
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
async listIntegrations(req, res, user) {
|
|
31
|
+
const integrations = await getIntegrationsForUser.execute(
|
|
32
|
+
user,
|
|
33
|
+
parseListFilters(req.query)
|
|
34
|
+
);
|
|
35
|
+
res.json({ integrations: integrations.map(toIntegrationDto) });
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async listIntegrationOptions(_req, res) {
|
|
39
|
+
res.json({ integrations: await getPossibleIntegrations.execute() });
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async createIntegration(req, res, user) {
|
|
43
|
+
const { entities, config } = objectBody(req);
|
|
44
|
+
const integration = await createIntegrationForUser.execute(
|
|
45
|
+
{ entities, config },
|
|
46
|
+
user
|
|
47
|
+
);
|
|
48
|
+
res.status(201).json(toIntegrationDto(integration));
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async getIntegration(req, res, user) {
|
|
52
|
+
const instance = await getIntegrationInstanceForUser.execute(
|
|
53
|
+
req.params.integrationId,
|
|
54
|
+
user
|
|
55
|
+
);
|
|
56
|
+
res.json(toIntegrationDto(instance));
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async updateIntegration(req, res, user) {
|
|
60
|
+
const { config } = objectBody(req);
|
|
61
|
+
const integration = await updateIntegrationForUser.execute(
|
|
62
|
+
req.params.integrationId,
|
|
63
|
+
user,
|
|
64
|
+
config
|
|
65
|
+
);
|
|
66
|
+
res.json(toIntegrationDto(integration));
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async deleteIntegration(req, res, user) {
|
|
70
|
+
await removeIntegrationForUser.execute(req.params.integrationId, user);
|
|
71
|
+
res.status(204).end();
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async getIntegrationConfigOptions(req, res, user) {
|
|
75
|
+
res.json(await send(req, user, 'GET_CONFIG_OPTIONS'));
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async refreshIntegrationConfigOptions(req, res, user) {
|
|
79
|
+
res.json(await send(req, user, 'REFRESH_CONFIG_OPTIONS', objectBody(req)));
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async listIntegrationActions(req, res, user) {
|
|
83
|
+
const actionType = optionalString(req.query.actionType, 'actionType');
|
|
84
|
+
res.json(
|
|
85
|
+
await send(req, user, 'GET_USER_ACTIONS', actionType ? { actionType } : {})
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async getIntegrationActionOptions(req, res, user) {
|
|
90
|
+
res.json(
|
|
91
|
+
await send(req, user, 'GET_USER_ACTION_OPTIONS', {
|
|
92
|
+
actionId: req.params.actionId,
|
|
93
|
+
data: objectBody(req),
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async refreshIntegrationActionOptions(req, res, user) {
|
|
99
|
+
res.json(
|
|
100
|
+
await send(req, user, 'REFRESH_USER_ACTION_OPTIONS', {
|
|
101
|
+
actionId: req.params.actionId,
|
|
102
|
+
data: objectBody(req),
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async runIntegrationAction(req, res, user) {
|
|
108
|
+
res.json(
|
|
109
|
+
await runIntegrationActionForUser.execute(
|
|
110
|
+
req.params.integrationId,
|
|
111
|
+
user,
|
|
112
|
+
req.params.actionId,
|
|
113
|
+
objectBody(req)
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async testIntegrationAuth(req, res, user) {
|
|
119
|
+
const { ok, errors } = await testIntegrationAuthForUser.execute(
|
|
120
|
+
req.params.integrationId,
|
|
121
|
+
user
|
|
122
|
+
);
|
|
123
|
+
res.json(ok ? { status: 'ok' } : { status: 'failed', errors });
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { createIntegrationHandlers };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { toV2ErrorBody } = require('../../error-response');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* POST /api/v2/entities/:entityId/proxy (ADR-052). The HTTP status follows the
|
|
5
|
+
* upstream status. Proxy failures answer in the api-proxy.schema.json error
|
|
6
|
+
* shape, which is also the v2 error shape plus `success` and `status`.
|
|
7
|
+
*/
|
|
8
|
+
function createProxyHandlers({ executeEntityProxyRequest }) {
|
|
9
|
+
return {
|
|
10
|
+
async proxyEntityRequest(req, res, user) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await executeEntityProxyRequest.execute(
|
|
13
|
+
req.params.entityId,
|
|
14
|
+
user,
|
|
15
|
+
req.body
|
|
16
|
+
);
|
|
17
|
+
res.status(result.status).json(result.body);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (!error?.isBoom || !error.data?.proxy) throw error;
|
|
20
|
+
const status = error.output.statusCode;
|
|
21
|
+
if (error.data.retryAfter) {
|
|
22
|
+
res.set('Retry-After', String(error.data.retryAfter));
|
|
23
|
+
}
|
|
24
|
+
res.status(status).json({
|
|
25
|
+
success: false,
|
|
26
|
+
status,
|
|
27
|
+
...toV2ErrorBody(error),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { createProxyHandlers };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
const { LIST_OWNERS } = require('../../modules/scope-visibility');
|
|
3
|
+
|
|
4
|
+
/** The JSON body as an object, or a 400 VALIDATION_ERROR. */
|
|
5
|
+
function objectBody(req) {
|
|
6
|
+
const body = req.body;
|
|
7
|
+
if (body === undefined || body === null || body === '') return {};
|
|
8
|
+
if (typeof body !== 'object' || Array.isArray(body)) {
|
|
9
|
+
throw Boom.badRequest('Request body must be a JSON object', {
|
|
10
|
+
code: 'VALIDATION_ERROR',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fields that must be present (non-empty) in the body. */
|
|
17
|
+
function requireFields(body, fields) {
|
|
18
|
+
const missing = fields.filter(
|
|
19
|
+
(field) =>
|
|
20
|
+
body[field] === undefined || body[field] === null || body[field] === ''
|
|
21
|
+
);
|
|
22
|
+
if (missing.length) {
|
|
23
|
+
throw Boom.badRequest(
|
|
24
|
+
`Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
|
|
25
|
+
{ code: 'VALIDATION_ERROR', details: { missing } }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return body;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A 1-based step number from a query or body value; 1 when absent. */
|
|
32
|
+
function parseStep(value) {
|
|
33
|
+
if (value === undefined || value === null || value === '') return 1;
|
|
34
|
+
const step = Number(value);
|
|
35
|
+
if (!Number.isInteger(step) || step < 1) {
|
|
36
|
+
throw Boom.badRequest('step must be a positive integer', {
|
|
37
|
+
code: 'INVALID_STEP',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return step;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** An optional string query/body value. */
|
|
44
|
+
function optionalString(value, name) {
|
|
45
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
46
|
+
if (typeof value !== 'string') {
|
|
47
|
+
throw Boom.badRequest(`${name} must be a string`, {
|
|
48
|
+
code: 'VALIDATION_ERROR',
|
|
49
|
+
details: { field: name },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const LIST_SCOPES = ['individual', 'organization'];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `?scope=individual|organization` and `?owner=me|org` on a list (ADR-056
|
|
59
|
+
* §3.4). Anything else is a 400.
|
|
60
|
+
*/
|
|
61
|
+
function parseListFilters(query = {}) {
|
|
62
|
+
const scope = optionalString(query.scope, 'scope');
|
|
63
|
+
const owner = optionalString(query.owner, 'owner');
|
|
64
|
+
if (scope !== undefined && !LIST_SCOPES.includes(scope)) {
|
|
65
|
+
throw Boom.badRequest(`scope must be one of ${LIST_SCOPES.join(', ')}`, {
|
|
66
|
+
code: 'VALIDATION_ERROR',
|
|
67
|
+
details: { field: 'scope' },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (owner !== undefined && !Object.hasOwn(LIST_OWNERS, owner)) {
|
|
71
|
+
throw Boom.badRequest('owner must be me or org', {
|
|
72
|
+
code: 'VALIDATION_ERROR',
|
|
73
|
+
details: { field: 'owner' },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { scope, owner };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
objectBody,
|
|
81
|
+
requireFields,
|
|
82
|
+
parseStep,
|
|
83
|
+
optionalString,
|
|
84
|
+
parseListFilters,
|
|
85
|
+
};
|