@friggframework/core 2.0.0--canary.680.6bacd26.0 → 2.0.0--canary.663.8de669c.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 +0 -11
- package/application/commands/integration-commands.js +8 -18
- package/application/index.js +2 -6
- package/credential/repositories/credential-repository-documentdb.js +0 -34
- package/credential/repositories/credential-repository-interface.js +0 -15
- package/credential/repositories/credential-repository-mongo.js +0 -23
- package/credential/repositories/credential-repository-postgres.js +0 -28
- package/database/encryption/README.md +0 -2
- package/database/encryption/encryption-schema-registry.js +12 -41
- package/database/encryption/field-encryption-service.js +5 -69
- package/generated/prisma-mongodb/edge.js +4 -20
- package/generated/prisma-mongodb/index-browser.js +1 -17
- package/generated/prisma-mongodb/index.d.ts +95 -1543
- package/generated/prisma-mongodb/index.js +4 -20
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -27
- package/generated/prisma-mongodb/wasm.js +4 -20
- package/generated/prisma-postgresql/edge.js +4 -20
- package/generated/prisma-postgresql/index-browser.js +1 -17
- package/generated/prisma-postgresql/index.d.ts +84 -1565
- package/generated/prisma-postgresql/index.js +4 -20
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -26
- package/generated/prisma-postgresql/wasm.js +4 -20
- package/handlers/app-definition-loader.js +8 -19
- package/handlers/app-handler-helpers.js +3 -25
- package/handlers/backend-utils.js +25 -31
- package/handlers/middleware/admin-auth.js +1 -14
- package/handlers/routers/auth.js +3 -11
- package/handlers/routers/health.js +4 -11
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +2 -4
- package/integrations/index.js +4 -0
- package/integrations/integration-router.js +23 -16
- package/integrations/utils/map-integration-dto.js +15 -1
- package/modules/index.js +0 -6
- package/modules/requester/requester.js +2 -27
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +0 -27
- package/prisma-postgresql/schema.prisma +0 -26
- package/types/index.d.ts +0 -1
- package/credential/use-cases/credential-operations-for-user.js +0 -117
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -66
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
- package/integrations/use-cases/integration-operations-for-user.js +0 -225
- package/management-api/create-management-api-router.js +0 -49
- package/management-api/error-response.js +0 -116
- package/management-api/management-api-config.js +0 -82
- package/management-api/meta-router.js +0 -57
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -368
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -252
- package/management-api/route-registry.js +0 -294
- package/management-api/test-support/http-client.js +0 -39
- package/management-api/use-cases/get-management-api-meta.js +0 -57
- package/management-api/v1-deprecation.js +0 -120
- package/management-api/v2/create-v2-router.js +0 -85
- package/management-api/v2/dependencies.js +0 -256
- package/management-api/v2/dto.js +0 -123
- package/management-api/v2/handlers/authorization.js +0 -93
- package/management-api/v2/handlers/credentials.js +0 -29
- package/management-api/v2/handlers/entities.js +0 -74
- package/management-api/v2/handlers/integrations.js +0 -125
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -54
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-errors.js +0 -20
- package/modules/proxy/proxy-path.js +0 -179
- package/modules/proxy/proxy-policy.js +0 -248
- package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
- package/modules/repositories/authorization-session-repository-factory.js +0 -31
- package/modules/repositories/authorization-session-repository-interface.js +0 -47
- package/modules/repositories/authorization-session-repository-prisma.js +0 -87
- package/modules/requester/proxy-request.js +0 -244
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/use-cases/entity-operations-for-user.js +0 -125
- package/modules/use-cases/entity-types.js +0 -115
- package/modules/use-cases/execute-entity-proxy-request.js +0 -239
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -86
- package/modules/use-cases/multi-step-authorization.js +0 -302
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/types/modules/index.d.ts +0 -58
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
const { FetchError } = require('../../errors/fetch-error');
|
|
2
|
-
const { ProxyRequestError } = require('../proxy/proxy-errors');
|
|
3
|
-
const {
|
|
4
|
-
normalizeProxyPath,
|
|
5
|
-
joinProxyUrl,
|
|
6
|
-
} = require('../proxy/proxy-path');
|
|
7
|
-
|
|
8
|
-
const MAX_REDIRECTS = 3;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The generic, path-checked request behind the Management API entity proxy
|
|
12
|
-
* (ADR-052 §8). It runs through the requester's own `_request`, so auth
|
|
13
|
-
* headers, token refresh (single-flight), the per-attempt timeout, telemetry
|
|
14
|
-
* and log redaction all apply. What it adds:
|
|
15
|
-
*
|
|
16
|
-
* - the URL is always `module base URL + normalised path`; the caller cannot
|
|
17
|
-
* name a host, scheme or port;
|
|
18
|
-
* - no backoff retries (the caller decides; a 429 comes back with its
|
|
19
|
-
* Retry-After), and upstream errors are returned, not thrown;
|
|
20
|
-
* - redirects are handled manually: same origin and under the base path only,
|
|
21
|
-
* at most three hops, each re-checked with `isAllowedRedirect`;
|
|
22
|
-
* - one deadline for the whole call and a cap on the response body.
|
|
23
|
-
*
|
|
24
|
-
* @param {import('./requester').Requester} requester
|
|
25
|
-
* @param {Object} params
|
|
26
|
-
* @param {string} params.method - upper case
|
|
27
|
-
* @param {string} params.path - caller path, validated here
|
|
28
|
-
* @param {Object} [params.query]
|
|
29
|
-
* @param {Object} [params.headers] - already filtered by the caller's policy
|
|
30
|
-
* @param {string|Buffer} [params.body] - already serialised
|
|
31
|
-
* @param {string} [params.baseUrl] - module-declared proxy base URL; falls
|
|
32
|
-
* back to the requester's `proxyBaseUrl`, `baseUrl` or `baseURL`
|
|
33
|
-
* @param {number} params.timeoutMs - budget for the whole call
|
|
34
|
-
* @param {number} params.maxResponseBytes
|
|
35
|
-
* @param {(path: string, method: string) => boolean} [params.isAllowedRedirect]
|
|
36
|
-
* @returns {Promise<{ status: number, headers: Object, body: Buffer, contentType: string, redirectBlocked: boolean }>}
|
|
37
|
-
* @throws {ProxyRequestError} INVALID_PROXY_REQUEST, PROXY_NOT_CONFIGURED,
|
|
38
|
-
* INVALID_CREDENTIALS, TIMEOUT, NETWORK_ERROR, RESPONSE_TOO_LARGE
|
|
39
|
-
*/
|
|
40
|
-
async function proxyRequest(requester, params) {
|
|
41
|
-
const {
|
|
42
|
-
method,
|
|
43
|
-
path,
|
|
44
|
-
query,
|
|
45
|
-
headers = {},
|
|
46
|
-
body,
|
|
47
|
-
baseUrl,
|
|
48
|
-
timeoutMs,
|
|
49
|
-
maxResponseBytes,
|
|
50
|
-
isAllowedRedirect,
|
|
51
|
-
} = params;
|
|
52
|
-
|
|
53
|
-
const normalizedPath = normalizeProxyPath(path);
|
|
54
|
-
const base =
|
|
55
|
-
baseUrl ??
|
|
56
|
-
requester.proxyBaseUrl ??
|
|
57
|
-
requester.baseUrl ??
|
|
58
|
-
requester.baseURL;
|
|
59
|
-
if (!base) {
|
|
60
|
-
throw new ProxyRequestError(
|
|
61
|
-
'PROXY_NOT_CONFIGURED',
|
|
62
|
-
'The module declares no base URL to proxy to'
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
const target = joinProxyUrl(base, normalizedPath);
|
|
66
|
-
const deadline = Date.now() + timeoutMs;
|
|
67
|
-
|
|
68
|
-
const run = async () => {
|
|
69
|
-
let url = target.url;
|
|
70
|
-
let currentMethod = method;
|
|
71
|
-
let currentBody = body;
|
|
72
|
-
let currentQuery = query;
|
|
73
|
-
for (let hop = 0; ; hop++) {
|
|
74
|
-
const remaining = deadline - Date.now();
|
|
75
|
-
if (remaining <= 0) throw timeoutError();
|
|
76
|
-
|
|
77
|
-
const response = await requester._request(url, {
|
|
78
|
-
method: currentMethod,
|
|
79
|
-
headers: { ...headers },
|
|
80
|
-
query: currentQuery,
|
|
81
|
-
body: currentBody,
|
|
82
|
-
returnFullRes: true,
|
|
83
|
-
returnErrorResponses: true,
|
|
84
|
-
noRetry: true,
|
|
85
|
-
redirect: 'manual',
|
|
86
|
-
size: maxResponseBytes,
|
|
87
|
-
timeoutMs: remaining,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const location = response.headers.get('location');
|
|
91
|
-
if (response.status >= 300 && response.status < 400 && location) {
|
|
92
|
-
const next = sameApiRedirect(location, url, target);
|
|
93
|
-
const nextMethod = redirectMethod(response.status, currentMethod);
|
|
94
|
-
if (
|
|
95
|
-
next &&
|
|
96
|
-
hop < MAX_REDIRECTS &&
|
|
97
|
-
(!isAllowedRedirect || isAllowedRedirect(next.path, nextMethod))
|
|
98
|
-
) {
|
|
99
|
-
if (nextMethod !== currentMethod) currentBody = undefined;
|
|
100
|
-
currentMethod = nextMethod;
|
|
101
|
-
url = next.url;
|
|
102
|
-
currentQuery = undefined;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
return readResponse(response, maxResponseBytes, {
|
|
106
|
-
redirectBlocked: !next,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
return readResponse(response, maxResponseBytes, {
|
|
110
|
-
redirectBlocked: false,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
let timer;
|
|
116
|
-
const timeout = new Promise((_, reject) => {
|
|
117
|
-
timer = setTimeout(() => reject(timeoutError()), timeoutMs);
|
|
118
|
-
});
|
|
119
|
-
const work = run();
|
|
120
|
-
// If the deadline wins, the request still settles later; nothing awaits it.
|
|
121
|
-
work.catch(() => {});
|
|
122
|
-
try {
|
|
123
|
-
return await Promise.race([work, timeout]);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
throw toProxyError(error);
|
|
126
|
-
} finally {
|
|
127
|
-
clearTimeout(timer);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function timeoutError() {
|
|
132
|
-
return new ProxyRequestError('TIMEOUT', 'The upstream API did not answer in time');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* A redirect target the proxy may follow: same origin as the module base URL
|
|
137
|
-
* and under its base path. Returns null for anything else.
|
|
138
|
-
*/
|
|
139
|
-
function sameApiRedirect(location, currentUrl, target) {
|
|
140
|
-
let next;
|
|
141
|
-
try {
|
|
142
|
-
next = new URL(location, currentUrl);
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
if (next.origin !== target.origin || next.protocol !== 'https:') return null;
|
|
147
|
-
let decodedPath;
|
|
148
|
-
try {
|
|
149
|
-
decodedPath = decodeURIComponent(next.pathname);
|
|
150
|
-
} catch {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
if (
|
|
154
|
-
!(decodedPath === target.basePath || decodedPath.startsWith(`${target.basePath}/`))
|
|
155
|
-
) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
const relativePath = decodedPath.slice(target.basePath.length) || '/';
|
|
159
|
-
let checkedPath;
|
|
160
|
-
try {
|
|
161
|
-
checkedPath = normalizeProxyPath(relativePath);
|
|
162
|
-
} catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
// _rawRequest percent-encodes the URL once, so hand it the decoded form.
|
|
166
|
-
return {
|
|
167
|
-
url: `${target.origin}${target.basePath}${checkedPath}${next.search ? decodeURI(next.search) : ''}`,
|
|
168
|
-
path: checkedPath,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Method for the next hop, as fetch does it. */
|
|
173
|
-
function redirectMethod(status, method) {
|
|
174
|
-
if (status === 303 && method !== 'HEAD') return 'GET';
|
|
175
|
-
if ((status === 301 || status === 302) && method === 'POST') return 'GET';
|
|
176
|
-
return method;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function readResponse(response, maxResponseBytes, { redirectBlocked }) {
|
|
180
|
-
const declaredLength = Number(response.headers.get('content-length'));
|
|
181
|
-
if (Number.isFinite(declaredLength) && declaredLength > maxResponseBytes) {
|
|
182
|
-
destroyBody(response);
|
|
183
|
-
throw tooLarge();
|
|
184
|
-
}
|
|
185
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
186
|
-
if (buffer.length > maxResponseBytes) throw tooLarge();
|
|
187
|
-
const headers = {};
|
|
188
|
-
response.headers.forEach((value, name) => {
|
|
189
|
-
headers[name.toLowerCase()] = value;
|
|
190
|
-
});
|
|
191
|
-
return {
|
|
192
|
-
status: response.status,
|
|
193
|
-
headers,
|
|
194
|
-
body: buffer,
|
|
195
|
-
contentType: headers['content-type'] || '',
|
|
196
|
-
redirectBlocked,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function destroyBody(response) {
|
|
201
|
-
try {
|
|
202
|
-
response.body?.destroy?.();
|
|
203
|
-
} catch {
|
|
204
|
-
// nothing to clean up
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function tooLarge() {
|
|
209
|
-
return new ProxyRequestError(
|
|
210
|
-
'RESPONSE_TOO_LARGE',
|
|
211
|
-
'The upstream response is larger than the proxy allows'
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Maps requester and fetch failures to the proxy's error codes. */
|
|
216
|
-
function toProxyError(error) {
|
|
217
|
-
if (error instanceof ProxyRequestError) return error;
|
|
218
|
-
const causeType = error?.cause?.type ?? error?.type;
|
|
219
|
-
if (causeType === 'max-size') return tooLarge();
|
|
220
|
-
if (error?.isTimeout) return timeoutError();
|
|
221
|
-
if (error instanceof FetchError || error?.name === 'FetchError') {
|
|
222
|
-
if (error.statusCode === 401) {
|
|
223
|
-
return new ProxyRequestError(
|
|
224
|
-
'INVALID_CREDENTIALS',
|
|
225
|
-
'The upstream API rejected the stored credentials; re-authorize the entity',
|
|
226
|
-
{ upstreamStatus: 401 }
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
if (!error.statusCode) {
|
|
230
|
-
return new ProxyRequestError(
|
|
231
|
-
'NETWORK_ERROR',
|
|
232
|
-
'Could not reach the upstream API'
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
return new ProxyRequestError(
|
|
236
|
-
'UPSTREAM_ERROR',
|
|
237
|
-
'The upstream API returned an error',
|
|
238
|
-
{ upstreamStatus: error.statusCode }
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
return error;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
module.exports = { proxyRequest, MAX_REDIRECTS };
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
const { AuthorizationSession } = require('../../domain/authorization-session');
|
|
2
|
-
|
|
3
|
-
/** In-memory AuthorizationSession repository for tests. */
|
|
4
|
-
class InMemoryAuthorizationSessionRepository {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.rows = new Map();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
_copy(session) {
|
|
10
|
-
return new AuthorizationSession({ ...session, stepData: { ...session.stepData } });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async create(session) {
|
|
14
|
-
this.rows.set(session.sessionId, this._copy(session));
|
|
15
|
-
return this._copy(session);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async findBySessionId(sessionId) {
|
|
19
|
-
const row = this.rows.get(sessionId);
|
|
20
|
-
if (!row || row.isExpired()) return null;
|
|
21
|
-
return this._copy(row);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async update(session) {
|
|
25
|
-
this.rows.set(session.sessionId, this._copy(session));
|
|
26
|
-
return this._copy(session);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async deleteBySessionId(sessionId) {
|
|
30
|
-
return this.rows.delete(sessionId) ? 1 : 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async deleteExpired(now = new Date()) {
|
|
34
|
-
let removed = 0;
|
|
35
|
-
for (const [id, row] of this.rows) {
|
|
36
|
-
if (row.isExpired(now)) {
|
|
37
|
-
this.rows.delete(id);
|
|
38
|
-
removed++;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return removed;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
module.exports = { InMemoryAuthorizationSessionRepository };
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
const Boom = require('@hapi/boom');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Management API v2 entity operations. Each one starts from
|
|
5
|
-
* GetEntityModuleForUser, so ownership is checked the same way everywhere
|
|
6
|
-
* (missing or foreign entity = 404).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** The caller's connected entities. */
|
|
10
|
-
class ListEntitiesForUser {
|
|
11
|
-
constructor({ moduleRepository }) {
|
|
12
|
-
this.moduleRepository = moduleRepository;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** @param {Object} user - authenticated User */
|
|
16
|
-
async execute(user) {
|
|
17
|
-
const entities = await this.moduleRepository.findEntitiesByUserId(
|
|
18
|
-
user.getId()
|
|
19
|
-
);
|
|
20
|
-
return Array.isArray(entities) ? entities : [];
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** One entity the caller owns. */
|
|
25
|
-
class GetEntityForUser {
|
|
26
|
-
constructor({ getEntityModuleForUser }) {
|
|
27
|
-
this.getEntityModuleForUser = getEntityModuleForUser;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async execute(entityId, user) {
|
|
31
|
-
return this.getEntityModuleForUser.loadOwnedEntity(entityId, user);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Runs the module's testAuthRequest against the stored credential. */
|
|
36
|
-
class TestEntityAuthForUser {
|
|
37
|
-
constructor({ getEntityModuleForUser }) {
|
|
38
|
-
this.getEntityModuleForUser = getEntityModuleForUser;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** @returns {Promise<boolean>} */
|
|
42
|
-
async execute(entityId, user) {
|
|
43
|
-
const { module } = await this.getEntityModuleForUser.execute(
|
|
44
|
-
entityId,
|
|
45
|
-
user
|
|
46
|
-
);
|
|
47
|
-
return Boolean(await module.testAuth());
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The module's entity options (e.g. objects a user can pick). */
|
|
52
|
-
class GetEntityOptionsForUser {
|
|
53
|
-
constructor({ getEntityModuleForUser }) {
|
|
54
|
-
this.getEntityModuleForUser = getEntityModuleForUser;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async execute(entityId, user) {
|
|
58
|
-
const { module } = await this.getEntityModuleForUser.execute(
|
|
59
|
-
entityId,
|
|
60
|
-
user
|
|
61
|
-
);
|
|
62
|
-
return module.getEntityOptions();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Refreshes the module's entity options with caller input, then returns them. */
|
|
67
|
-
class RefreshEntityOptionsForUser {
|
|
68
|
-
constructor({ getEntityModuleForUser }) {
|
|
69
|
-
this.getEntityModuleForUser = getEntityModuleForUser;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async execute(entityId, user, options = {}) {
|
|
73
|
-
const { module } = await this.getEntityModuleForUser.execute(
|
|
74
|
-
entityId,
|
|
75
|
-
user
|
|
76
|
-
);
|
|
77
|
-
return module.refreshEntityOptions(options);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Deletes an entity the caller owns. Refuses with 409 while integrations
|
|
83
|
-
* still use it, so an integration never points at a missing entity. The
|
|
84
|
-
* credential is left alone (others may share it); delete it through
|
|
85
|
-
* /api/v2/credentials.
|
|
86
|
-
*/
|
|
87
|
-
class DeleteEntityForUser {
|
|
88
|
-
constructor({ getEntityModuleForUser, moduleRepository, integrationRepository }) {
|
|
89
|
-
this.getEntityModuleForUser = getEntityModuleForUser;
|
|
90
|
-
this.moduleRepository = moduleRepository;
|
|
91
|
-
this.integrationRepository = integrationRepository;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async execute(entityId, user) {
|
|
95
|
-
const entity = await this.getEntityModuleForUser.loadOwnedEntity(
|
|
96
|
-
entityId,
|
|
97
|
-
user
|
|
98
|
-
);
|
|
99
|
-
const integrations =
|
|
100
|
-
(await this.integrationRepository.findIntegrationsByEntityId(
|
|
101
|
-
entity.id
|
|
102
|
-
)) || [];
|
|
103
|
-
if (integrations.length > 0) {
|
|
104
|
-
throw Boom.conflict(
|
|
105
|
-
`Entity ${entity.id} is used by ${integrations.length} integration(s); delete them first`,
|
|
106
|
-
{
|
|
107
|
-
code: 'ENTITY_IN_USE',
|
|
108
|
-
details: {
|
|
109
|
-
integrationIds: integrations.map((i) => String(i.id)),
|
|
110
|
-
},
|
|
111
|
-
}
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
await this.moduleRepository.deleteEntity(entity.id);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
module.exports = {
|
|
119
|
-
ListEntitiesForUser,
|
|
120
|
-
GetEntityForUser,
|
|
121
|
-
TestEntityAuthForUser,
|
|
122
|
-
GetEntityOptionsForUser,
|
|
123
|
-
RefreshEntityOptionsForUser,
|
|
124
|
-
DeleteEntityForUser,
|
|
125
|
-
};
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
const Boom = require('@hapi/boom');
|
|
2
|
-
const { ModuleConstants } = require('../ModuleConstants');
|
|
3
|
-
const { usedByOf } = require('../entity-type-registry');
|
|
4
|
-
|
|
5
|
-
const AUTH_TYPE_BY_REQUESTER = {
|
|
6
|
-
[ModuleConstants.authType.oauth2]: 'oauth2',
|
|
7
|
-
[ModuleConstants.authType.oauth1]: 'oauth1',
|
|
8
|
-
[ModuleConstants.authType.apiKey]: 'api-key',
|
|
9
|
-
[ModuleConstants.authType.basic]: 'basic',
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const callIfFunction = (target, name) =>
|
|
13
|
-
typeof target?.[name] === 'function' ? target[name]() : undefined;
|
|
14
|
-
|
|
15
|
-
/** Number of authorization steps a module declares (1 unless multi-step). */
|
|
16
|
-
function getAuthStepCount(definition) {
|
|
17
|
-
const count = callIfFunction(definition, 'getAuthStepCount');
|
|
18
|
-
return Number.isInteger(count) && count > 0 ? count : 1;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Public description of an API module (an "entity type"). Reads only the
|
|
23
|
-
* definition: display metadata, auth type and step count. Never credentials
|
|
24
|
-
* or env values.
|
|
25
|
-
*/
|
|
26
|
-
function describeEntityType(definition, typeName = definition.moduleName) {
|
|
27
|
-
const stepCount = getAuthStepCount(definition);
|
|
28
|
-
const display = definition.display || {};
|
|
29
|
-
const description = {
|
|
30
|
-
type: typeName,
|
|
31
|
-
name:
|
|
32
|
-
callIfFunction(definition, 'getDisplayName') ??
|
|
33
|
-
display.label ??
|
|
34
|
-
display.name ??
|
|
35
|
-
typeName,
|
|
36
|
-
authType:
|
|
37
|
-
callIfFunction(definition, 'getAuthType') ??
|
|
38
|
-
AUTH_TYPE_BY_REQUESTER[definition.API?.requesterType] ??
|
|
39
|
-
'custom',
|
|
40
|
-
isMultiStep: stepCount > 1,
|
|
41
|
-
stepCount,
|
|
42
|
-
};
|
|
43
|
-
const text = callIfFunction(definition, 'getDescription') ?? display.description;
|
|
44
|
-
if (text) description.description = text;
|
|
45
|
-
const capabilities = callIfFunction(definition, 'getCapabilities');
|
|
46
|
-
if (capabilities !== undefined) description.capabilities = capabilities;
|
|
47
|
-
return description;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function findDefinition(moduleDefinitions, entityType) {
|
|
51
|
-
const definition = moduleDefinitions.find(
|
|
52
|
-
(def) => def.moduleName === entityType
|
|
53
|
-
);
|
|
54
|
-
if (!definition) {
|
|
55
|
-
throw Boom.notFound(`Entity type '${entityType}' not found`, {
|
|
56
|
-
code: 'ENTITY_TYPE_NOT_FOUND',
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return definition;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const describeEntry = (name, entry) => ({
|
|
63
|
-
// The registry key is the name the API uses, whatever the definition spells.
|
|
64
|
-
...describeEntityType(entry.definition, name),
|
|
65
|
-
usedBy: usedByOf(entry),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
function requireRegistry(entityTypeRegistry) {
|
|
69
|
-
if (!(entityTypeRegistry instanceof Map)) {
|
|
70
|
-
throw new TypeError('entityTypeRegistry is required (see getEntityTypeRegistry)');
|
|
71
|
-
}
|
|
72
|
-
return entityTypeRegistry;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Lists the entity types this app can authorize: registry entries plus every
|
|
77
|
-
* module an integration binds (ADR-059). `usedBy` names the integrations that
|
|
78
|
-
* bind each type; it is empty for a registry-only type.
|
|
79
|
-
*/
|
|
80
|
-
class ListEntityTypes {
|
|
81
|
-
constructor({ entityTypeRegistry }) {
|
|
82
|
-
this.registry = requireRegistry(entityTypeRegistry);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async execute() {
|
|
86
|
-
return [...this.registry]
|
|
87
|
-
.map(([name, entry]) => describeEntry(name, entry))
|
|
88
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Describes one entity type, looked up by its registry key; 404 when absent. */
|
|
93
|
-
class GetEntityType {
|
|
94
|
-
constructor({ entityTypeRegistry }) {
|
|
95
|
-
this.registry = requireRegistry(entityTypeRegistry);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async execute(entityType) {
|
|
99
|
-
const entry = this.registry.get(entityType);
|
|
100
|
-
if (!entry) {
|
|
101
|
-
throw Boom.notFound(`Entity type '${entityType}' not found`, {
|
|
102
|
-
code: 'ENTITY_TYPE_NOT_FOUND',
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return describeEntry(entityType, entry);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
module.exports = {
|
|
110
|
-
ListEntityTypes,
|
|
111
|
-
GetEntityType,
|
|
112
|
-
describeEntityType,
|
|
113
|
-
getAuthStepCount,
|
|
114
|
-
findDefinition,
|
|
115
|
-
};
|