@friggframework/core 2.0.0--canary.707.df80bfd.0 → 2.0.0--canary.680.d670696.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 +3 -0
- package/CLAUDE.md +0 -28
- package/README.md +0 -105
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- package/credential/use-cases/credential-operations-for-user.js +25 -110
- package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
- package/database/config.js +24 -48
- package/database/documentdb-encryption-service.js +0 -1
- package/database/documentdb-utils.js +1 -6
- package/database/encryption/encryption-config.js +44 -13
- package/database/encryption/encryption-schema-registry.js +31 -88
- package/database/prisma.js +2 -4
- package/generated/prisma-mongodb/edge.js +4 -27
- package/generated/prisma-mongodb/index-browser.js +1 -24
- package/generated/prisma-mongodb/index.d.ts +48 -3228
- package/generated/prisma-mongodb/index.js +4 -27
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -65
- package/generated/prisma-mongodb/wasm.js +4 -27
- package/generated/prisma-postgresql/edge.js +4 -27
- package/generated/prisma-postgresql/index-browser.js +1 -24
- package/generated/prisma-postgresql/index.d.ts +51 -3503
- package/generated/prisma-postgresql/index.js +4 -27
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -61
- package/generated/prisma-postgresql/wasm.js +4 -27
- package/handlers/app-definition-loader.js +12 -110
- package/handlers/routers/auth.js +12 -40
- package/handlers/routers/health.js +2 -4
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/index.js +1 -64
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/index.js +0 -5
- 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 -7
- package/integrations/use-cases/integration-operations-for-user.js +24 -55
- package/integrations/utils/map-integration-dto.js +1 -44
- package/management-api/management-api-config.js +2 -71
- package/management-api/meta-router.js +2 -21
- package/management-api/openapi/components.js +3 -50
- package/management-api/openapi/v2-operations.js +5 -17
- package/management-api/route-registry.js +6 -123
- 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/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/proxy/proxy-errors.js +1 -12
- package/modules/proxy/proxy-path.js +3 -7
- package/modules/proxy/proxy-policy.js +5 -34
- 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/requester/proxy-request.js +1 -2
- package/modules/requester/requester.js +3 -8
- 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 +15 -107
- package/modules/use-cases/get-authorization-requirements.js +2 -2
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-module-for-user.js +7 -34
- package/modules/use-cases/get-entity-options-by-id.js +4 -10
- 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 +4 -10
- package/modules/use-cases/multi-step-authorization.js +13 -40
- package/modules/use-cases/process-authorization-callback.js +19 -53
- package/modules/use-cases/refresh-entity-options.js +4 -10
- package/modules/use-cases/test-module-auth.js +4 -10
- package/package.json +5 -6
- package/prisma-mongodb/schema.prisma +0 -65
- package/prisma-postgresql/schema.prisma +0 -61
- package/queues/index.js +0 -25
- package/types/index.d.ts +0 -1
- package/types/modules/index.d.ts +1 -157
- 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/extensions/bootstrap-runner.js +0 -43
- package/extensions/extension-loader.js +0 -230
- package/extensions/index.js +0 -44
- package/extensions/initialize-app.js +0 -123
- package/extensions/prisma-composer.js +0 -270
- package/extensions/route-mounter.js +0 -103
- package/extensions/schema-composer.js +0 -188
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
- package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/schedule-config.js +0 -80
- package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/modules/entity-scope.js +0 -15
- package/modules/proxy/proxy-bases.js +0 -169
- 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/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
- package/providers/index.js +0 -23
- package/providers/resolve-provider.js +0 -93
- package/providers/runtime-detection.js +0 -124
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -150
- package/queues/providers/qstash-queue-provider.js +0 -219
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -193
- package/queues/queue-provider-factory.js +0 -101
- package/queues/queue-provider.js +0 -65
- package/queues/sqs-queue-client.js +0 -81
- 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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
2
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
3
|
-
const { entityNotFoundError } = require('./entity-not-found');
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* PostgreSQL Module Repository Adapter
|
|
@@ -109,26 +108,6 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
109
108
|
return credentialMap;
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
/**
|
|
113
|
-
* The one place a stored entity row becomes the entity the application sees.
|
|
114
|
-
* The provider's `data` is spread FIRST and every column is pinned after it:
|
|
115
|
-
* a stored `id`, `userId` or `organizationId` in `data` must not decide who
|
|
116
|
-
* owns the entity or who sees it.
|
|
117
|
-
* @private
|
|
118
|
-
*/
|
|
119
|
-
_mapEntity(row, credential) {
|
|
120
|
-
return {
|
|
121
|
-
...(row.data || {}),
|
|
122
|
-
id: row.id.toString(),
|
|
123
|
-
credential,
|
|
124
|
-
userId: row.userId?.toString(),
|
|
125
|
-
name: row.name,
|
|
126
|
-
externalId: row.externalId,
|
|
127
|
-
moduleName: row.moduleName,
|
|
128
|
-
organizationId: row.organizationId?.toString() ?? null,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
111
|
/**
|
|
133
112
|
* Find entity by ID with credential
|
|
134
113
|
* Replaces: Entity.findById(entityId).populate('credential')
|
|
@@ -145,12 +124,20 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
145
124
|
});
|
|
146
125
|
|
|
147
126
|
if (!entity) {
|
|
148
|
-
throw
|
|
127
|
+
throw new Error(`Entity ${entityId} not found`);
|
|
149
128
|
}
|
|
150
129
|
|
|
151
130
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
152
131
|
|
|
153
|
-
return
|
|
132
|
+
return {
|
|
133
|
+
id: entity.id.toString(),
|
|
134
|
+
credential,
|
|
135
|
+
userId: entity.userId?.toString(),
|
|
136
|
+
name: entity.name,
|
|
137
|
+
externalId: entity.externalId,
|
|
138
|
+
moduleName: entity.moduleName,
|
|
139
|
+
...(entity.data || {}),
|
|
140
|
+
};
|
|
154
141
|
}
|
|
155
142
|
|
|
156
143
|
/**
|
|
@@ -170,7 +157,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
170
157
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
171
158
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
172
159
|
|
|
173
|
-
return entities.map((e) => (
|
|
160
|
+
return entities.map((e) => ({
|
|
161
|
+
id: e.id.toString(),
|
|
162
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
163
|
+
userId: e.userId?.toString(),
|
|
164
|
+
name: e.name,
|
|
165
|
+
externalId: e.externalId,
|
|
166
|
+
moduleName: e.moduleName,
|
|
167
|
+
...(e.data || {}),
|
|
168
|
+
}));
|
|
174
169
|
}
|
|
175
170
|
|
|
176
171
|
/**
|
|
@@ -190,7 +185,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
190
185
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
191
186
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
192
187
|
|
|
193
|
-
return entities.map((e) => (
|
|
188
|
+
return entities.map((e) => ({
|
|
189
|
+
id: e.id.toString(),
|
|
190
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
191
|
+
userId: e.userId?.toString(),
|
|
192
|
+
name: e.name,
|
|
193
|
+
externalId: e.externalId,
|
|
194
|
+
moduleName: e.moduleName,
|
|
195
|
+
...(e.data || {}),
|
|
196
|
+
}));
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
/**
|
|
@@ -214,32 +217,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
214
217
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
215
218
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
216
219
|
|
|
217
|
-
return entities.map((e) => (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
* @returns {Promise<Array>} Array of entity objects with string IDs
|
|
227
|
-
*/
|
|
228
|
-
async findEntitiesByOwnership(clauses) {
|
|
229
|
-
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
230
|
-
const entities = await this.prisma.entity.findMany({
|
|
231
|
-
where: {
|
|
232
|
-
OR: clauses.map(({ userId, organizationId }) => ({
|
|
233
|
-
userId: this._convertId(userId),
|
|
234
|
-
organizationId: this._convertId(organizationId),
|
|
235
|
-
})),
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
240
|
-
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
241
|
-
|
|
242
|
-
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
220
|
+
return entities.map((e) => ({
|
|
221
|
+
id: e.id.toString(),
|
|
222
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
223
|
+
userId: e.userId?.toString(),
|
|
224
|
+
name: e.name,
|
|
225
|
+
externalId: e.externalId,
|
|
226
|
+
moduleName: e.moduleName,
|
|
227
|
+
...(e.data || {}),
|
|
228
|
+
}));
|
|
243
229
|
}
|
|
244
230
|
|
|
245
231
|
/**
|
|
@@ -278,7 +264,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
278
264
|
|
|
279
265
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
280
266
|
|
|
281
|
-
return
|
|
267
|
+
return {
|
|
268
|
+
id: entity.id.toString(),
|
|
269
|
+
credential,
|
|
270
|
+
userId: entity.userId?.toString(),
|
|
271
|
+
name: entity.name,
|
|
272
|
+
externalId: entity.externalId,
|
|
273
|
+
moduleName: entity.moduleName,
|
|
274
|
+
...(entity.data || {}),
|
|
275
|
+
};
|
|
282
276
|
}
|
|
283
277
|
|
|
284
278
|
/**
|
|
@@ -298,7 +292,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
298
292
|
.filter(Boolean);
|
|
299
293
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
300
294
|
|
|
301
|
-
return entities.map((e) => (
|
|
295
|
+
return entities.map((e) => ({
|
|
296
|
+
id: e.id.toString(),
|
|
297
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
298
|
+
userId: e.userId?.toString(),
|
|
299
|
+
name: e.name,
|
|
300
|
+
externalId: e.externalId,
|
|
301
|
+
moduleName: e.moduleName,
|
|
302
|
+
...(e.data || {}),
|
|
303
|
+
}));
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
/**
|
|
@@ -306,13 +308,9 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
306
308
|
* Replaces: Entity.create(entityData)
|
|
307
309
|
*
|
|
308
310
|
* @param {Object} entityData - Entity data (with string IDs from application layer)
|
|
309
|
-
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
|
|
310
|
-
* belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
|
|
311
|
-
* because `entityData` carries provider fields and one named `organizationId`
|
|
312
|
-
* must not be taken for ours.
|
|
313
311
|
* @returns {Promise<Object>} Created entity object with string IDs
|
|
314
312
|
*/
|
|
315
|
-
async createEntity(entityData
|
|
313
|
+
async createEntity(entityData) {
|
|
316
314
|
const {
|
|
317
315
|
user,
|
|
318
316
|
userId,
|
|
@@ -330,7 +328,6 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
330
328
|
name,
|
|
331
329
|
moduleName,
|
|
332
330
|
externalId,
|
|
333
|
-
organizationId: this._convertId(tenancy.organizationId ?? null),
|
|
334
331
|
data: dynamicData,
|
|
335
332
|
};
|
|
336
333
|
|
|
@@ -340,7 +337,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
340
337
|
|
|
341
338
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
342
339
|
|
|
343
|
-
return
|
|
340
|
+
return {
|
|
341
|
+
id: entity.id.toString(),
|
|
342
|
+
credential: credentialObj,
|
|
343
|
+
userId: entity.userId?.toString(),
|
|
344
|
+
name: entity.name,
|
|
345
|
+
externalId: entity.externalId,
|
|
346
|
+
moduleName: entity.moduleName,
|
|
347
|
+
...(entity.data || {}),
|
|
348
|
+
};
|
|
344
349
|
}
|
|
345
350
|
|
|
346
351
|
/**
|
|
@@ -396,7 +401,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
396
401
|
|
|
397
402
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
398
403
|
|
|
399
|
-
return
|
|
404
|
+
return {
|
|
405
|
+
id: entity.id.toString(),
|
|
406
|
+
credential: credentialObj,
|
|
407
|
+
userId: entity.userId?.toString(),
|
|
408
|
+
name: entity.name,
|
|
409
|
+
externalId: entity.externalId,
|
|
410
|
+
moduleName: entity.moduleName,
|
|
411
|
+
...(entity.data || {}),
|
|
412
|
+
};
|
|
400
413
|
} catch (error) {
|
|
401
414
|
if (error.code === 'P2025') {
|
|
402
415
|
return null;
|
|
@@ -461,11 +474,6 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
461
474
|
if (filter.moduleName) where.moduleName = filter.moduleName;
|
|
462
475
|
if (filter.externalId) where.externalId = this._toString(filter.externalId);
|
|
463
476
|
|
|
464
|
-
// `null` means "no organization"; absent means "any".
|
|
465
|
-
if (filter.organizationId !== undefined) {
|
|
466
|
-
where.organizationId = this._convertId(filter.organizationId);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
477
|
return where;
|
|
470
478
|
}
|
|
471
479
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
2
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
3
|
-
const { entityNotFoundError } = require('./entity-not-found');
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Prisma-based Module Repository
|
|
@@ -38,7 +37,7 @@ class ModuleRepository extends ModuleRepositoryInterface {
|
|
|
38
37
|
});
|
|
39
38
|
|
|
40
39
|
if (!entity) {
|
|
41
|
-
throw
|
|
40
|
+
throw new Error(`Entity ${entityId} not found`);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
return {
|
|
@@ -28,8 +28,7 @@ const MAX_REDIRECTS = 3;
|
|
|
28
28
|
* @param {Object} [params.query]
|
|
29
29
|
* @param {Object} [params.headers] - already filtered by the caller's policy
|
|
30
30
|
* @param {string|Buffer} [params.body] - already serialised
|
|
31
|
-
* @param {string} [params.baseUrl] -
|
|
32
|
-
* caller named (ADR-052 amendment; resolved by the use case); falls
|
|
31
|
+
* @param {string} [params.baseUrl] - module-declared proxy base URL; falls
|
|
33
32
|
* back to the requester's `proxyBaseUrl`, `baseUrl` or `baseURL`
|
|
34
33
|
* @param {number} params.timeoutMs - budget for the whole call
|
|
35
34
|
* @param {number} params.maxResponseBytes
|
|
@@ -219,14 +219,9 @@ class Requester extends Delegate {
|
|
|
219
219
|
if (options.query) {
|
|
220
220
|
let queryBuild = '?';
|
|
221
221
|
for (const key in options.query) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const values =
|
|
226
|
-
Array.isArray(value) && key.endsWith('[]') ? value : [value];
|
|
227
|
-
for (const v of values) {
|
|
228
|
-
queryBuild += `${encodeURIComponent(key)}=${encodeURIComponent(v)}&`;
|
|
229
|
-
}
|
|
222
|
+
queryBuild += `${encodeURIComponent(key)}=${encodeURIComponent(
|
|
223
|
+
options.query[key]
|
|
224
|
+
)}&`;
|
|
230
225
|
}
|
|
231
226
|
encodedUrl += queryBuild.slice(0, -1);
|
|
232
227
|
}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
-
const {
|
|
3
|
-
describeRecord,
|
|
4
|
-
visibilityClauses,
|
|
5
|
-
matchesListFilters,
|
|
6
|
-
} = require('../scope-visibility');
|
|
7
|
-
const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
|
|
8
|
-
const { assertCanManage } = require('./manage-access');
|
|
9
2
|
|
|
10
3
|
/**
|
|
11
4
|
* Management API v2 entity operations. Each one starts from
|
|
@@ -13,55 +6,29 @@ const { assertCanManage } = require('./manage-access');
|
|
|
13
6
|
* (missing or foreign entity = 404).
|
|
14
7
|
*/
|
|
15
8
|
|
|
16
|
-
/**
|
|
17
|
-
* The entities the caller may see in the active organization (ADR-056 §3.4,
|
|
18
|
-
* §3.6): the organization's own, and the caller's individual ones by the
|
|
19
|
-
* visibility rule. Each carries `tenancy` ({scope, owner, organizationId,
|
|
20
|
-
* access}); the stored entity is otherwise untouched.
|
|
21
|
-
*/
|
|
9
|
+
/** The caller's connected entities. */
|
|
22
10
|
class ListEntitiesForUser {
|
|
23
|
-
constructor({ moduleRepository
|
|
11
|
+
constructor({ moduleRepository }) {
|
|
24
12
|
this.moduleRepository = moduleRepository;
|
|
25
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
26
13
|
}
|
|
27
14
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
async execute(user, filters = {}) {
|
|
33
|
-
const ctx = this.scopePolicy.accessContext(user);
|
|
34
|
-
const entities = await this.moduleRepository.findEntitiesByOwnership(
|
|
35
|
-
visibilityClauses(ctx, 'entity')
|
|
15
|
+
/** @param {Object} user - authenticated User */
|
|
16
|
+
async execute(user) {
|
|
17
|
+
const entities = await this.moduleRepository.findEntitiesByUserId(
|
|
18
|
+
user.getId()
|
|
36
19
|
);
|
|
37
|
-
|
|
38
|
-
for (const entity of Array.isArray(entities) ? entities : []) {
|
|
39
|
-
// The query is the filter; this is the same rule applied again,
|
|
40
|
-
// so a repository that returns too much cannot widen the list.
|
|
41
|
-
const tenancy = describeRecord(entity, ctx, 'entity');
|
|
42
|
-
if (tenancy && matchesListFilters(tenancy, filters)) {
|
|
43
|
-
listed.push({ ...entity, tenancy });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return listed;
|
|
20
|
+
return Array.isArray(entities) ? entities : [];
|
|
47
21
|
}
|
|
48
22
|
}
|
|
49
23
|
|
|
50
|
-
/** One entity the caller
|
|
24
|
+
/** One entity the caller owns. */
|
|
51
25
|
class GetEntityForUser {
|
|
52
26
|
constructor({ getEntityModuleForUser }) {
|
|
53
27
|
this.getEntityModuleForUser = getEntityModuleForUser;
|
|
54
28
|
}
|
|
55
29
|
|
|
56
30
|
async execute(entityId, user) {
|
|
57
|
-
|
|
58
|
-
entityId,
|
|
59
|
-
user
|
|
60
|
-
);
|
|
61
|
-
return {
|
|
62
|
-
...entity,
|
|
63
|
-
tenancy: this.getEntityModuleForUser.describe(entity, user),
|
|
64
|
-
};
|
|
31
|
+
return this.getEntityModuleForUser.loadOwnedEntity(entityId, user);
|
|
65
32
|
}
|
|
66
33
|
}
|
|
67
34
|
|
|
@@ -112,10 +79,10 @@ class RefreshEntityOptionsForUser {
|
|
|
112
79
|
}
|
|
113
80
|
|
|
114
81
|
/**
|
|
115
|
-
* Deletes an entity the caller
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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.
|
|
119
86
|
*/
|
|
120
87
|
class DeleteEntityForUser {
|
|
121
88
|
constructor({ getEntityModuleForUser, moduleRepository, integrationRepository }) {
|
|
@@ -129,10 +96,6 @@ class DeleteEntityForUser {
|
|
|
129
96
|
entityId,
|
|
130
97
|
user
|
|
131
98
|
);
|
|
132
|
-
assertCanManage(
|
|
133
|
-
this.getEntityModuleForUser.describe(entity, user)?.access,
|
|
134
|
-
`entity ${entity.id}`
|
|
135
|
-
);
|
|
136
99
|
const integrations =
|
|
137
100
|
(await this.integrationRepository.findIntegrationsByEntityId(
|
|
138
101
|
entity.id
|
|
@@ -59,31 +59,10 @@ function findDefinition(moduleDefinitions, entityType) {
|
|
|
59
59
|
return definition;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
* With a scope policy the entry also says which scope a new connection gets
|
|
64
|
-
* and which a request may choose (ADR-056 §3.1), so a UI can offer the choice.
|
|
65
|
-
* `global` is never offered: a tenant request cannot create one.
|
|
66
|
-
*/
|
|
67
|
-
const scopeOf = (scopePolicy, name, user) => {
|
|
68
|
-
if (!scopePolicy) return {};
|
|
69
|
-
const { scope, source, allow } = scopePolicy.resolveScope({
|
|
70
|
-
entityType: name,
|
|
71
|
-
user,
|
|
72
|
-
});
|
|
73
|
-
return {
|
|
74
|
-
scope: {
|
|
75
|
-
default: scope,
|
|
76
|
-
allow: allow.filter((value) => value !== 'global'),
|
|
77
|
-
source,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const describeEntry = (name, entry, scopePolicy, user) => ({
|
|
62
|
+
const describeEntry = (name, entry) => ({
|
|
83
63
|
// The registry key is the name the API uses, whatever the definition spells.
|
|
84
64
|
...describeEntityType(entry.definition, name),
|
|
85
65
|
usedBy: usedByOf(entry),
|
|
86
|
-
...scopeOf(scopePolicy, name, user),
|
|
87
66
|
});
|
|
88
67
|
|
|
89
68
|
function requireRegistry(entityTypeRegistry) {
|
|
@@ -99,34 +78,31 @@ function requireRegistry(entityTypeRegistry) {
|
|
|
99
78
|
* bind each type; it is empty for a registry-only type.
|
|
100
79
|
*/
|
|
101
80
|
class ListEntityTypes {
|
|
102
|
-
constructor({ entityTypeRegistry
|
|
81
|
+
constructor({ entityTypeRegistry }) {
|
|
103
82
|
this.registry = requireRegistry(entityTypeRegistry);
|
|
104
|
-
this.scopePolicy = scopePolicy;
|
|
105
83
|
}
|
|
106
84
|
|
|
107
|
-
|
|
108
|
-
async execute({ user } = {}) {
|
|
85
|
+
async execute() {
|
|
109
86
|
return [...this.registry]
|
|
110
|
-
.map(([name, entry]) => describeEntry(name, entry
|
|
87
|
+
.map(([name, entry]) => describeEntry(name, entry))
|
|
111
88
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
112
89
|
}
|
|
113
90
|
}
|
|
114
91
|
|
|
115
92
|
/** Describes one entity type, looked up by its registry key; 404 when absent. */
|
|
116
93
|
class GetEntityType {
|
|
117
|
-
constructor({ entityTypeRegistry
|
|
94
|
+
constructor({ entityTypeRegistry }) {
|
|
118
95
|
this.registry = requireRegistry(entityTypeRegistry);
|
|
119
|
-
this.scopePolicy = scopePolicy;
|
|
120
96
|
}
|
|
121
97
|
|
|
122
|
-
async execute(entityType
|
|
98
|
+
async execute(entityType) {
|
|
123
99
|
const entry = this.registry.get(entityType);
|
|
124
100
|
if (!entry) {
|
|
125
101
|
throw Boom.notFound(`Entity type '${entityType}' not found`, {
|
|
126
102
|
code: 'ENTITY_TYPE_NOT_FOUND',
|
|
127
103
|
});
|
|
128
104
|
}
|
|
129
|
-
return describeEntry(entityType, entry
|
|
105
|
+
return describeEntry(entityType, entry);
|
|
130
106
|
}
|
|
131
107
|
}
|
|
132
108
|
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
2
|
const { getLogger } = require('../../logs');
|
|
3
3
|
const { getTelemetry } = require('../../telemetry/telemetry-runtime');
|
|
4
|
-
const { ProxyRequestError
|
|
5
|
-
const { SCOPES } = require('../scope-resolution');
|
|
6
|
-
const { grantsUse } = require('../scope-visibility');
|
|
4
|
+
const { ProxyRequestError } = require('../proxy/proxy-errors');
|
|
7
5
|
const { normalizeProxyPath, matchAllowRule } = require('../proxy/proxy-path');
|
|
8
|
-
const { DEFAULT_BASE, resolveModuleBases } = require('../proxy/proxy-bases');
|
|
9
6
|
const {
|
|
10
7
|
validateProxyRequest,
|
|
11
8
|
resolveAllowRules,
|
|
@@ -19,7 +16,6 @@ const log = getLogger('frigg.api.proxy');
|
|
|
19
16
|
/** HTTP status and audit outcome for each proxy error code (ADR-052 §10). */
|
|
20
17
|
const ERROR_MAP = {
|
|
21
18
|
INVALID_PROXY_REQUEST: { status: 400, outcome: 'denied' },
|
|
22
|
-
PROXY_UNKNOWN_BASE: { status: 400, outcome: 'denied' },
|
|
23
19
|
PROXY_NOT_ALLOWED: { status: 403, outcome: 'denied' },
|
|
24
20
|
PAYLOAD_TOO_LARGE: { status: 413, outcome: 'too_large' },
|
|
25
21
|
UNSUPPORTED_MEDIA_TYPE: { status: 415, outcome: 'denied' },
|
|
@@ -59,11 +55,9 @@ const authHeaderNamesOf = (api) =>
|
|
|
59
55
|
* through its module with the stored credential, inside the limits the app
|
|
60
56
|
* and the module set.
|
|
61
57
|
*
|
|
62
|
-
* Order of checks: request shape and size, path rules, entity
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* deny by default), credential readiness (401), then the call through
|
|
66
|
-
* Requester._proxyRequest. Every call,
|
|
58
|
+
* Order of checks: request shape and size, path rules, entity ownership
|
|
59
|
+
* (404), the module's allow-list (403, deny by default), credential
|
|
60
|
+
* readiness (401), then the call through Requester._proxyRequest. Every call,
|
|
67
61
|
* allowed or denied, writes one audit record and one usage count. Bodies,
|
|
68
62
|
* headers, query values and the raw path are never logged.
|
|
69
63
|
*/
|
|
@@ -104,27 +98,9 @@ class ExecuteEntityProxyRequest {
|
|
|
104
98
|
audit.requestBytes = validated.requestBytes;
|
|
105
99
|
const path = normalizeProxyPath(validated.path);
|
|
106
100
|
|
|
107
|
-
// Visible in the active organization, or 404 (ADR-056 §3.6): the
|
|
108
|
-
// same clauses the v2 lists use, so a missing entity, another
|
|
109
|
-
// person's and one paired with another organization look alike.
|
|
110
101
|
const { entity, definition, module } =
|
|
111
102
|
await this.getEntityModuleForUser.execute(entityId, user);
|
|
112
103
|
audit.module = definition.moduleName;
|
|
113
|
-
this.assertProxyable(entityId, entity, definition, user);
|
|
114
|
-
|
|
115
|
-
// The base is a name the module declared; the host comes from the
|
|
116
|
-
// module's declaration, never from the request.
|
|
117
|
-
const base = validated.base ?? DEFAULT_BASE;
|
|
118
|
-
audit.base = base;
|
|
119
|
-
const bases = this.basesOf(definition);
|
|
120
|
-
if (!bases.names.includes(base)) {
|
|
121
|
-
throw new ProxyRequestError(
|
|
122
|
-
'PROXY_UNKNOWN_BASE',
|
|
123
|
-
`Module ${definition.moduleName} has no base named ${base}; use one of: ${bases.names.join(', ')}`,
|
|
124
|
-
{ module: definition.moduleName, base, bases: bases.names }
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
104
|
const credentialRef = entity.credential;
|
|
129
105
|
const credentialId =
|
|
130
106
|
credentialRef && typeof credentialRef === 'object'
|
|
@@ -135,14 +111,12 @@ class ExecuteEntityProxyRequest {
|
|
|
135
111
|
}
|
|
136
112
|
|
|
137
113
|
const { rules } = resolveAllowRules(definition, this.proxyConfig.modules);
|
|
138
|
-
const rule = matchAllowRule(rules, validated.method, path
|
|
114
|
+
const rule = matchAllowRule(rules, validated.method, path);
|
|
139
115
|
if (!rule) {
|
|
140
116
|
throw new ProxyRequestError(
|
|
141
117
|
'PROXY_NOT_ALLOWED',
|
|
142
|
-
`Module ${definition.moduleName} does not allow ${validated.method} on this path
|
|
143
|
-
|
|
144
|
-
(bases.names.length > 1 ? ` of base ${base}` : ''),
|
|
145
|
-
{ module: definition.moduleName, method: validated.method, base }
|
|
118
|
+
`Module ${definition.moduleName} does not allow ${validated.method} on this path`,
|
|
119
|
+
{ module: definition.moduleName, method: validated.method }
|
|
146
120
|
);
|
|
147
121
|
}
|
|
148
122
|
audit.pattern = rule.path;
|
|
@@ -170,11 +144,11 @@ class ExecuteEntityProxyRequest {
|
|
|
170
144
|
query: validated.query,
|
|
171
145
|
headers,
|
|
172
146
|
body: validated.body,
|
|
173
|
-
baseUrl: this.resolveBaseUrl(
|
|
147
|
+
baseUrl: this.resolveBaseUrl(definition, module.api),
|
|
174
148
|
timeoutMs: this.proxyConfig.timeoutMs,
|
|
175
149
|
maxResponseBytes: this.proxyConfig.maxResponseBytes,
|
|
176
150
|
isAllowedRedirect: (nextPath, nextMethod) =>
|
|
177
|
-
Boolean(matchAllowRule(rules, nextMethod, nextPath
|
|
151
|
+
Boolean(matchAllowRule(rules, nextMethod, nextPath)),
|
|
178
152
|
});
|
|
179
153
|
audit.upstreamStatus = upstream.status;
|
|
180
154
|
audit.responseBytes = upstream.body?.length ?? 0;
|
|
@@ -228,78 +202,12 @@ class ExecuteEntityProxyRequest {
|
|
|
228
202
|
}
|
|
229
203
|
}
|
|
230
204
|
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
* Both refusals are 403 PROXY_NOT_ALLOWED: the caller can already see the
|
|
238
|
-
* entity, so saying it exists leaks nothing. `details.reason` tells them
|
|
239
|
-
* apart ('access' or 'scope').
|
|
240
|
-
*/
|
|
241
|
-
assertProxyable(entityId, entity, definition, user) {
|
|
242
|
-
const tenancy = this.getEntityModuleForUser.describe(entity, user);
|
|
243
|
-
if (!tenancy) {
|
|
244
|
-
// Not visible after all: the same 404 as a missing entity.
|
|
245
|
-
throw Boom.notFound(`Entity ${entityId} not found`, {
|
|
246
|
-
code: 'ENTITY_NOT_FOUND',
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
if (!grantsUse(tenancy.access)) {
|
|
250
|
-
throw new ProxyRequestError(
|
|
251
|
-
'PROXY_NOT_ALLOWED',
|
|
252
|
-
'Proxying through an entity needs use access to it; you can only view this one',
|
|
253
|
-
{ module: definition.moduleName, reason: 'access' }
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
// Narrowest by default (§3.5): with no setting only individual
|
|
257
|
-
// entities are proxied, because proxying through a shared credential
|
|
258
|
-
// lets any member act on the organization's account.
|
|
259
|
-
const acceptedScopes = this.proxyConfig.scopes ?? [SCOPES.INDIVIDUAL];
|
|
260
|
-
if (!acceptedScopes.includes(tenancy.scope)) {
|
|
261
|
-
throw new ProxyRequestError(
|
|
262
|
-
'PROXY_NOT_ALLOWED',
|
|
263
|
-
`The entity proxy does not accept ${tenancy.scope} entities in this app`,
|
|
264
|
-
{ module: definition.moduleName, reason: 'scope', scope: tenancy.scope }
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* The module's bases. Load already validated the definition
|
|
271
|
-
* (assertProxyConfig), so this only fails for a definition that skipped
|
|
272
|
-
* load; that is a configuration fault (501), never an unmapped 500.
|
|
273
|
-
*/
|
|
274
|
-
basesOf(definition) {
|
|
275
|
-
try {
|
|
276
|
-
return resolveModuleBases(definition);
|
|
277
|
-
} catch (error) {
|
|
278
|
-
if (!(error instanceof ProxyConfigError)) throw error;
|
|
279
|
-
throw new ProxyRequestError(
|
|
280
|
-
'PROXY_NOT_CONFIGURED',
|
|
281
|
-
`Module ${definition.moduleName} has invalid proxy settings`
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* The URL of the named base. A base the module declared never falls back
|
|
288
|
-
* to the requester's own host (that would send the module's token to a
|
|
289
|
-
* host the module did not name); only a module that declares nothing at
|
|
290
|
-
* all uses the requester's base URL, signalled by undefined.
|
|
291
|
-
*/
|
|
292
|
-
resolveBaseUrl(bases, api, name) {
|
|
293
|
-
const declared = bases.values.get(name);
|
|
294
|
-
if (!bases.declared) return undefined;
|
|
295
|
-
const url = typeof declared === 'function' ? declared(api) : declared;
|
|
296
|
-
if (typeof url !== 'string' || url === '') {
|
|
297
|
-
throw new ProxyRequestError(
|
|
298
|
-
'PROXY_NOT_CONFIGURED',
|
|
299
|
-
`The module declares no usable URL for base ${name}`
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
return url;
|
|
205
|
+
/** A module may pin its proxy base URL (string, or a function of the api). */
|
|
206
|
+
resolveBaseUrl(definition, api) {
|
|
207
|
+
const declared = definition.proxy?.baseUrl;
|
|
208
|
+
if (typeof declared === 'function') return declared(api);
|
|
209
|
+
if (typeof declared === 'string') return declared;
|
|
210
|
+
return undefined;
|
|
303
211
|
}
|
|
304
212
|
|
|
305
213
|
/** One audit record (ADR-048) and one usage count (ADR-011) per call. */
|
|
@@ -3,7 +3,7 @@ const { findDefinition, getAuthStepCount } = require('./entity-types');
|
|
|
3
3
|
|
|
4
4
|
const defaultCreateModule = (params) => {
|
|
5
5
|
const { Module } = require('../module');
|
|
6
|
-
return Module
|
|
6
|
+
return new Module(params);
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -80,7 +80,7 @@ class GetAuthorizationRequirements {
|
|
|
80
80
|
if (typeof definition.getAuthRequirementsForStep === 'function') {
|
|
81
81
|
raw = await definition.getAuthRequirementsForStep(step);
|
|
82
82
|
} else {
|
|
83
|
-
const module =
|
|
83
|
+
const module = this.createModule({
|
|
84
84
|
userId,
|
|
85
85
|
definition,
|
|
86
86
|
...(state ? { state } : {}),
|