@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.
Files changed (161) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/CLAUDE.md +0 -28
  3. package/README.md +0 -105
  4. package/application/commands/integration-commands.js +2 -2
  5. package/application/commands/scheduler-commands.js +7 -11
  6. package/application/commands/user-commands.js +5 -17
  7. package/core/Worker.js +7 -31
  8. package/core/create-handler.js +0 -9
  9. package/credential/use-cases/credential-operations-for-user.js +25 -110
  10. package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
  11. package/database/config.js +24 -48
  12. package/database/documentdb-encryption-service.js +0 -1
  13. package/database/documentdb-utils.js +1 -6
  14. package/database/encryption/encryption-config.js +44 -13
  15. package/database/encryption/encryption-schema-registry.js +31 -88
  16. package/database/prisma.js +2 -4
  17. package/generated/prisma-mongodb/edge.js +4 -27
  18. package/generated/prisma-mongodb/index-browser.js +1 -24
  19. package/generated/prisma-mongodb/index.d.ts +48 -3228
  20. package/generated/prisma-mongodb/index.js +4 -27
  21. package/generated/prisma-mongodb/package.json +1 -1
  22. package/generated/prisma-mongodb/schema.prisma +0 -65
  23. package/generated/prisma-mongodb/wasm.js +4 -27
  24. package/generated/prisma-postgresql/edge.js +4 -27
  25. package/generated/prisma-postgresql/index-browser.js +1 -24
  26. package/generated/prisma-postgresql/index.d.ts +51 -3503
  27. package/generated/prisma-postgresql/index.js +4 -27
  28. package/generated/prisma-postgresql/package.json +1 -1
  29. package/generated/prisma-postgresql/schema.prisma +0 -61
  30. package/generated/prisma-postgresql/wasm.js +4 -27
  31. package/handlers/app-definition-loader.js +12 -110
  32. package/handlers/routers/auth.js +12 -40
  33. package/handlers/routers/health.js +2 -4
  34. package/handlers/routers/integration-defined-routers.js +92 -144
  35. package/handlers/routers/integration-webhook-routers.js +64 -102
  36. package/index.js +1 -64
  37. package/infrastructure/scheduler/index.js +6 -19
  38. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  39. package/integrations/index.js +0 -5
  40. package/integrations/integration-router.js +73 -115
  41. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  42. package/integrations/repositories/integration-repository-interface.js +1 -17
  43. package/integrations/repositories/integration-repository-mongo.js +76 -59
  44. package/integrations/repositories/integration-repository-postgres.js +76 -62
  45. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  46. package/integrations/use-cases/create-integration.js +20 -55
  47. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  48. package/integrations/use-cases/index.js +1 -7
  49. package/integrations/use-cases/integration-operations-for-user.js +24 -55
  50. package/integrations/utils/map-integration-dto.js +1 -44
  51. package/management-api/management-api-config.js +2 -71
  52. package/management-api/meta-router.js +2 -21
  53. package/management-api/openapi/components.js +3 -50
  54. package/management-api/openapi/v2-operations.js +5 -17
  55. package/management-api/route-registry.js +6 -123
  56. package/management-api/v2/dependencies.js +15 -40
  57. package/management-api/v2/dto.js +0 -20
  58. package/management-api/v2/handlers/authorization.js +0 -1
  59. package/management-api/v2/handlers/credentials.js +2 -6
  60. package/management-api/v2/handlers/entities.js +7 -10
  61. package/management-api/v2/handlers/integrations.js +3 -6
  62. package/management-api/v2/request.js +1 -32
  63. package/modules/index.js +0 -8
  64. package/modules/module-factory.js +0 -12
  65. package/modules/module.js +2 -53
  66. package/modules/proxy/proxy-errors.js +1 -12
  67. package/modules/proxy/proxy-path.js +3 -7
  68. package/modules/proxy/proxy-policy.js +5 -34
  69. package/modules/repositories/module-repository-documentdb.js +6 -51
  70. package/modules/repositories/module-repository-interface.js +2 -19
  71. package/modules/repositories/module-repository-mongo.js +74 -67
  72. package/modules/repositories/module-repository-postgres.js +74 -66
  73. package/modules/repositories/module-repository.js +1 -2
  74. package/modules/requester/proxy-request.js +1 -2
  75. package/modules/requester/requester.js +3 -8
  76. package/modules/use-cases/entity-operations-for-user.js +13 -50
  77. package/modules/use-cases/entity-types.js +7 -31
  78. package/modules/use-cases/execute-entity-proxy-request.js +15 -107
  79. package/modules/use-cases/get-authorization-requirements.js +2 -2
  80. package/modules/use-cases/get-entities-for-user.js +13 -26
  81. package/modules/use-cases/get-entity-module-for-user.js +7 -34
  82. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  83. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  84. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  85. package/modules/use-cases/get-module.js +4 -10
  86. package/modules/use-cases/multi-step-authorization.js +13 -40
  87. package/modules/use-cases/process-authorization-callback.js +19 -53
  88. package/modules/use-cases/refresh-entity-options.js +4 -10
  89. package/modules/use-cases/test-module-auth.js +4 -10
  90. package/package.json +5 -6
  91. package/prisma-mongodb/schema.prisma +0 -65
  92. package/prisma-postgresql/schema.prisma +0 -61
  93. package/queues/index.js +0 -25
  94. package/types/index.d.ts +0 -1
  95. package/types/modules/index.d.ts +1 -157
  96. package/user/repositories/user-repository-documentdb.js +2 -27
  97. package/user/repositories/user-repository-interface.js +0 -18
  98. package/user/repositories/user-repository-mongo.js +0 -24
  99. package/user/repositories/user-repository-postgres.js +0 -18
  100. package/user/use-cases/authenticate-user.js +1 -7
  101. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  102. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  103. package/user/user.js +1 -64
  104. package/database/organization-where.js +0 -18
  105. package/database/unique-violation.js +0 -25
  106. package/extensions/bootstrap-runner.js +0 -43
  107. package/extensions/extension-loader.js +0 -230
  108. package/extensions/index.js +0 -44
  109. package/extensions/initialize-app.js +0 -123
  110. package/extensions/prisma-composer.js +0 -270
  111. package/extensions/route-mounter.js +0 -103
  112. package/extensions/schema-composer.js +0 -188
  113. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  114. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  115. package/integrations/repositories/integration-not-found.js +0 -14
  116. package/integrations/schedule-config.js +0 -80
  117. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  118. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  119. package/modules/entity-scope.js +0 -15
  120. package/modules/proxy/proxy-bases.js +0 -169
  121. package/modules/repositories/entity-not-found.js +0 -18
  122. package/modules/scope-config.js +0 -192
  123. package/modules/scope-policy.js +0 -112
  124. package/modules/scope-resolution.js +0 -288
  125. package/modules/scope-visibility.js +0 -247
  126. package/modules/tests/doubles/ownership-matching.js +0 -18
  127. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  128. package/modules/use-cases/create-entity-from-credential.js +0 -113
  129. package/modules/use-cases/manage-access.js +0 -21
  130. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  131. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  132. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  133. package/providers/index.js +0 -23
  134. package/providers/resolve-provider.js +0 -93
  135. package/providers/runtime-detection.js +0 -124
  136. package/queues/providers/index.js +0 -11
  137. package/queues/providers/netlify-background-provider.js +0 -150
  138. package/queues/providers/qstash-queue-provider.js +0 -219
  139. package/queues/providers/sqs-queue-provider.js +0 -70
  140. package/queues/queue-client-interface.js +0 -55
  141. package/queues/queue-message-auth.js +0 -193
  142. package/queues/queue-provider-factory.js +0 -101
  143. package/queues/queue-provider.js +0 -65
  144. package/queues/sqs-queue-client.js +0 -81
  145. package/types/user/index.d.ts +0 -134
  146. package/user/adopter-jwt-config.js +0 -177
  147. package/user/adopter-jwt-errors.js +0 -59
  148. package/user/adopter-token-verifier.js +0 -122
  149. package/user/app-user-helpers.js +0 -97
  150. package/user/create-user-authentication.js +0 -64
  151. package/user/membership.js +0 -51
  152. package/user/repositories/organization-membership-record.js +0 -19
  153. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  154. package/user/repositories/organization-membership-repository-factory.js +0 -42
  155. package/user/repositories/organization-membership-repository-interface.js +0 -57
  156. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  157. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  158. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  159. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  160. package/user/use-cases/ensure-organization-membership.js +0 -92
  161. 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 entityNotFoundError(entityId);
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 this._mapEntity(entity, credential);
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) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
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) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
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) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
218
- }
219
-
220
- /**
221
- * Find entities matching any of the owner/organization pairs, in one query.
222
- * `organizationId: null` matches rows with no organization. The caller
223
- * (a use case) decides which pairs; this only runs them.
224
- *
225
- * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
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 this._mapEntity(entity, credential);
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) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
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, tenancy = {}) {
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 this._mapEntity(entity, credentialObj);
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 this._mapEntity(entity, credentialObj);
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 entityNotFoundError(entityId);
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] - the module-declared URL of the base the
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
- const value = options.query[key];
223
- // `fields[]: ['a', 'b']` -> `fields%5B%5D=a&fields%5B%5D=b`.
224
- // Any other array keeps String(value), i.e. comma-joined.
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, scopePolicy }) {
11
+ constructor({ moduleRepository }) {
24
12
  this.moduleRepository = moduleRepository;
25
- this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
26
13
  }
27
14
 
28
- /**
29
- * @param {Object} user - authenticated User
30
- * @param {{scope?: string, owner?: string}} [filters] - `?scope=` and `?owner=`
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
- const listed = [];
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 may use, with its `tenancy`. */
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
- const entity = await this.getEntityModuleForUser.loadOwnedEntity(
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 may manage (the owner; for an organization
116
- * entity, an organization admin). Refuses with 409 while integrations still
117
- * use it, so an integration never points at a missing entity. The credential
118
- * is left alone (others may share it); delete it through /api/v2/credentials.
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, scopePolicy }) {
81
+ constructor({ entityTypeRegistry }) {
103
82
  this.registry = requireRegistry(entityTypeRegistry);
104
- this.scopePolicy = scopePolicy;
105
83
  }
106
84
 
107
- /** @param {{user?: Object}} [params] - the caller, for the level-6 scope default */
108
- async execute({ user } = {}) {
85
+ async execute() {
109
86
  return [...this.registry]
110
- .map(([name, entry]) => describeEntry(name, entry, this.scopePolicy, user))
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, scopePolicy }) {
94
+ constructor({ entityTypeRegistry }) {
118
95
  this.registry = requireRegistry(entityTypeRegistry);
119
- this.scopePolicy = scopePolicy;
120
96
  }
121
97
 
122
- async execute(entityType, { user } = {}) {
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, this.scopePolicy, user);
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, ProxyConfigError } = require('../proxy/proxy-errors');
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 visibility in
63
- * the active organization (404), `use` access (403), the entity's scope
64
- * against `managementApi.proxy.scopes` (403), the module's allow-list (403,
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, base);
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
- // Single-base modules keep the message they always had.
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(bases, module.api, base),
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, base)),
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
- * ADR-056 §3.5: the caller needs `use` access to the entity in the active
233
- * organization, and the entity's scope must be one the app lets the proxy
234
- * accept. `access` and `scope` come from the description the v2 lists
235
- * return (GetEntityModuleForUser.describe), never from a second rule.
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.create(params);
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 = await this.createModule({
83
+ const module = this.createModule({
84
84
  userId,
85
85
  definition,
86
86
  ...(state ? { state } : {}),