@friggframework/core 2.0.0--canary.685.022c3c3.0 → 2.0.0--canary.682.d8626c4.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 (42) hide show
  1. package/CLAUDE.md +0 -23
  2. package/README.md +1 -1
  3. package/credential/use-cases/credential-operations-for-user.js +25 -110
  4. package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
  5. package/handlers/app-definition-loader.js +0 -9
  6. package/index.js +3 -4
  7. package/integrations/index.js +5 -0
  8. package/integrations/integration-router.js +31 -35
  9. package/integrations/repositories/integration-repository-documentdb.js +2 -1
  10. package/integrations/use-cases/integration-operations-for-user.js +6 -16
  11. package/integrations/utils/map-integration-dto.js +44 -1
  12. package/management-api/management-api-config.js +4 -70
  13. package/management-api/openapi/components.js +0 -1
  14. package/management-api/openapi/v2-operations.js +3 -6
  15. package/management-api/v2/dependencies.js +0 -2
  16. package/management-api/v2/dto.js +0 -3
  17. package/management-api/v2/handlers/credentials.js +2 -6
  18. package/modules/repositories/module-repository-documentdb.js +8 -6
  19. package/modules/repositories/module-repository-mongo.js +1 -2
  20. package/modules/repositories/module-repository-postgres.js +1 -2
  21. package/modules/repositories/module-repository.js +1 -2
  22. package/modules/scope-config.js +1 -23
  23. package/modules/scope-policy.js +3 -34
  24. package/modules/scope-resolution.js +0 -11
  25. package/modules/scope-visibility.js +8 -63
  26. package/modules/use-cases/authorize-entity-for-user.js +2 -4
  27. package/modules/use-cases/create-entity-from-credential.js +113 -0
  28. package/modules/use-cases/entity-operations-for-user.js +4 -9
  29. package/modules/use-cases/execute-entity-proxy-request.js +18 -50
  30. package/modules/use-cases/get-entity-module-for-user.js +2 -9
  31. package/modules/use-cases/get-entity-options-by-id.js +4 -1
  32. package/modules/use-cases/get-module.js +4 -1
  33. package/modules/use-cases/multi-step-authorization.js +1 -4
  34. package/modules/use-cases/refresh-entity-options.js +4 -1
  35. package/modules/use-cases/test-module-auth.js +4 -1
  36. package/package.json +5 -5
  37. package/types/modules/index.d.ts +9 -41
  38. package/types/user/index.d.ts +0 -7
  39. package/user/use-cases/authenticate-user.js +1 -7
  40. package/user/user.js +1 -24
  41. package/modules/repositories/entity-not-found.js +0 -18
  42. package/modules/use-cases/manage-access.js +0 -21
package/CLAUDE.md CHANGED
@@ -502,29 +502,6 @@ The single source of the max receive count is
502
502
  devtools integration builder uses it for the queue's `RedrivePolicy` and sets
503
503
  it as `FRIGG_QUEUE_MAX_RECEIVE_COUNT` on the queue worker function.
504
504
 
505
- ### Management API v2 notes (`/management-api`)
506
-
507
- - Handlers in `management-api/v2/handlers/` call use cases only; dependencies are wired in
508
- `management-api/v2/dependencies.js`. Errors are `{ error: { code, message, details? } }`.
509
- - `appDefinition.managementApi` is resolved by `resolveManagementApiConfig`, which `loadAppDefinition`
510
- also calls so a bad value stops the app at load. Numeric proxy limits fall back to their defaults;
511
- `proxy.scopes` (ADR-056 §3.5, default `['individual']`) never does: an unknown scope, `'global'`, a
512
- non-list, an empty list or a repeated scope throws `ManagementApiConfigError`
513
- (`MANAGEMENT_API_CONFIG_INVALID`).
514
- - **Entity proxy** (`ExecuteEntityProxyRequest`): visibility in the active organization comes from
515
- `GetEntityModuleForUser` (404), then `assertProxyable` reads the same `{ scope, access }` description
516
- the v2 lists return: `grantsUse(access)` (`use` or `manage`, from `modules/scope-visibility.js`) or 403
517
- `PROXY_NOT_ALLOWED` `reason: 'access'`, and the scope must be in `proxy.scopes` or 403 `reason: 'scope'`.
518
- Do not add a second ownership rule for the proxy; change the description instead.
519
- - **Not visible is 404, and so is missing.** Module repositories throw `entityNotFoundError(id)`
520
- (`modules/repositories/entity-not-found.js`) for a missing row and use cases match it with
521
- `isEntityNotFound`. A 500 for a missing id next to a 404 for another tenant's id tells a caller which
522
- ids exist. A test double that resolves `findEntityById` to `null` hides this: no adapter does that.
523
- - **Tenant isolation on real PostgreSQL**: `management-api/v2/tenant-isolation.postgres.test.js`, run with
524
- `FRIGG_TEST_POSTGRES_URL=<a database it may wipe>`. It applies the migrations, truncates every table
525
- before each test, and drives real adopter-JWT auth through the real router and Prisma repositories.
526
- Without the variable it is skipped (and says so), not passed. See `docs/guides/entity-scopes.md`.
527
-
528
505
  ### 8. Error Handling (`/errors`)
529
506
 
530
507
  **Purpose**: Standardized error types with proper HTTP semantics.
package/README.md CHANGED
@@ -393,7 +393,7 @@ value that is not a three-part JWT is not offered to the verifier at all; it fal
393
393
  | `clockToleranceSec` | `30` | Skew allowed on `exp` and `nbf`. |
394
394
  | `userIdClaim` | `sub` | App user id (required in the token). |
395
395
  | `orgIdClaim` | `org_id` | App organization id (optional). |
396
- | `roleClaim` | `org_role` | Exposed as `user.getOrgRole()` for the request, and stored as the `orgRole` of a person's membership the first time they join that organization (`MEMBER` when the token names none). A token with no role is a member for permissions too: it cannot create or change organization-level entities or integrations; `user.orgAdminRoles` (default `['ADMIN']`) names the roles that can. |
396
+ | `roleClaim` | `org_role` | Exposed as `user.getOrgRole()` for the request, and stored as the `orgRole` of a person's membership the first time they join that organization (`MEMBER` when the token names none). |
397
397
 
398
398
  Rules worth knowing:
399
399
 
@@ -1,28 +1,10 @@
1
1
  const Boom = require('@hapi/boom');
2
- const {
3
- createScopePolicy,
4
- describeRecord,
5
- visibilityClauses,
6
- matchesListFilters,
7
- SCOPES,
8
- } = require('../../modules/entity-scope');
9
- const { assertCanManage } = require('../../modules/use-cases/manage-access');
2
+ const { userOwns } = require('../../modules/use-cases/get-entity-module-for-user');
10
3
 
11
4
  /**
12
- * Management API v2 credential operations (ADR-056 §3.4, §3.6). A credential
13
- * has no module name and no organization of its own:
14
- *
15
- * - its `type` and `entityIds` come from the entities that use it that the
16
- * caller may see, so the same person's account connected in two
17
- * organizations (one credential, two entities) lists in each organization
18
- * with that organization's entity only;
19
- * - who it belongs to is its OWNER (`credential.userId`), never the entities:
20
- * a credential an individual entity and an organization entity both point at
21
- * (possible only through legacy data) is described by whoever owns the
22
- * credential, so the answer cannot change with the order the entities load;
23
- * - a credential whose entities are all out of sight (another organization's)
24
- * is a 404, and one with no entities at all (its entity was deleted) is
25
- * visible to its owner only, as it was before scoping.
5
+ * Management API v2 credential operations. A credential has no module name
6
+ * of its own; its `type` and the entities that use it come from the
7
+ * caller's entities. A missing or foreign credential is a 404.
26
8
  */
27
9
 
28
10
  const credentialIdOf = (entity) => {
@@ -55,82 +37,31 @@ function withEntityUsage(credentials, entities) {
55
37
  });
56
38
  }
57
39
 
58
- /**
59
- * `{scope, owner, organizationId, access}` of a credential for this caller, or
60
- * null when its owner is neither the caller nor the active organization. Built
61
- * by `describeRecord` over the row a connection by this owner has: the
62
- * organization of a visible entity, else the one a new entity would get.
63
- */
64
- function credentialTenancy(credential, visibleEntities, ctx) {
65
- const entityOrg = visibleEntities.find(
66
- (entity) => entity.organizationId !== undefined && entity.organizationId !== null
67
- )?.organizationId;
68
- const organizationId =
69
- entityOrg ?? (ctx.individualAcross === 'all' ? null : ctx.activeOrgId);
70
- return describeRecord({ userId: credential.userId, organizationId }, ctx, 'entity');
71
- }
72
-
73
- /** The entities of `entities` the caller may see, each with its `tenancy`. */
74
- function visibleOf(entities, ctx) {
75
- const visible = [];
76
- for (const entity of entities || []) {
77
- const tenancy = describeRecord(entity, ctx, 'entity');
78
- if (tenancy) visible.push({ ...entity, tenancy });
79
- }
80
- return visible;
81
- }
82
-
83
- /**
84
- * The caller's credentials with their type, the entities of them they may see
85
- * and `tenancy`. Credentials with no entity at all are listed too, for the
86
- * person and the active organization that own them.
87
- */
40
+ /** The caller's credentials with their type and entity usage. */
88
41
  class ListCredentialsForUser {
89
- constructor({ credentialRepository, moduleRepository, scopePolicy }) {
42
+ constructor({ credentialRepository, moduleRepository }) {
90
43
  this.credentialRepository = credentialRepository;
91
44
  this.moduleRepository = moduleRepository;
92
- this.scopePolicy = scopePolicy || createScopePolicy();
93
45
  }
94
46
 
95
- /** @param {{scope?: string, owner?: string}} [filters] */
96
- async execute(user, filters = {}) {
97
- const ctx = this.scopePolicy.accessContext(user);
98
- const owners = [...new Set([ctx.callerId, ctx.activeOrgId].filter(Boolean))];
99
- const [found, perOwner, owned] = await Promise.all([
100
- this.moduleRepository.findEntitiesByOwnership(visibilityClauses(ctx, 'entity')),
101
- Promise.all(owners.map((id) => this.credentialRepository.findCredentialsByUserId(id))),
102
- // Every entity of those owners, in any organization: only to tell a credential
103
- // with no entity at all from one used elsewhere.
104
- Promise.all(owners.map((id) => this.moduleRepository.findEntitiesByUserId(id))),
47
+ async execute(user) {
48
+ const userId = user.getId();
49
+ const [credentials, entities] = await Promise.all([
50
+ this.credentialRepository.findCredentialsByUserId(userId),
51
+ this.moduleRepository.findEntitiesByUserId(userId),
105
52
  ]);
106
- const visible = visibleOf(found, ctx);
107
- const usedAnywhere = new Set(owned.flat().map(credentialIdOf).filter(Boolean));
108
- const listed = [];
109
- for (const credential of perOwner.flat().filter(Boolean)) {
110
- const id = String(credential.id);
111
- const referencing = visible.filter((entity) => credentialIdOf(entity) === id);
112
- if (referencing.length === 0 && usedAnywhere.has(id)) continue;
113
- const tenancy = credentialTenancy(credential, referencing, ctx);
114
- if (!tenancy || !matchesListFilters(tenancy, filters)) continue;
115
- listed.push({ ...withEntityUsage([credential], referencing)[0], tenancy });
116
- }
117
- return listed;
53
+ return withEntityUsage(credentials || [], entities);
118
54
  }
119
55
  }
120
56
 
121
- /** One credential the caller may see: see the header. 404 otherwise. */
57
+ /** One credential the caller owns, with its type and entity usage. */
122
58
  class GetCredentialForUserV2 {
123
- constructor({ credentialRepository, moduleRepository, scopePolicy }) {
59
+ constructor({ credentialRepository, moduleRepository }) {
124
60
  this.credentialRepository = credentialRepository;
125
61
  this.moduleRepository = moduleRepository;
126
- this.scopePolicy = scopePolicy || createScopePolicy();
127
62
  }
128
63
 
129
64
  async execute(credentialId, user) {
130
- const notFound = () =>
131
- Boom.notFound(`Credential ${credentialId} not found`, {
132
- code: 'CREDENTIAL_NOT_FOUND',
133
- });
134
65
  let credential = null;
135
66
  try {
136
67
  credential = await this.credentialRepository.findCredentialById(
@@ -141,29 +72,22 @@ class GetCredentialForUserV2 {
141
72
  throw error;
142
73
  }
143
74
  }
144
- if (!credential) throw notFound();
145
- const ctx = this.scopePolicy.accessContext(user);
146
-
147
- const referencing =
148
- (await this.moduleRepository.findEntities({ credentialId: credential.id })) || [];
149
- const visible = visibleOf(referencing, ctx);
150
- if (referencing.length > 0 && visible.length === 0) throw notFound();
151
- const tenancy = credentialTenancy(credential, visible, ctx);
152
- if (!tenancy) throw notFound();
153
- return { ...withEntityUsage([credential], visible)[0], tenancy };
75
+ if (!credential || !userOwns(user, credential.userId)) {
76
+ throw Boom.notFound(`Credential ${credentialId} not found`, {
77
+ code: 'CREDENTIAL_NOT_FOUND',
78
+ });
79
+ }
80
+ const entities = await this.moduleRepository.findEntitiesByUserId(
81
+ credential.userId
82
+ );
83
+ return withEntityUsage([credential], entities)[0];
154
84
  }
155
85
  }
156
86
 
157
87
  /**
158
- * Deletes a credential the caller may manage. The entities that used it stay,
159
- * with their credential unset, so they show as needing re-authorization rather
88
+ * Deletes a credential the caller owns. The entities that used it stay, with
89
+ * their credential unset, so they show as needing re-authorization rather
160
90
  * than disappearing from integrations.
161
- *
162
- * Refuses with 409 `CREDENTIAL_SHARED` while an entity the caller cannot see
163
- * still uses it: one credential row serves the same provider account in every
164
- * organization, and deleting it from one would break the other. The message
165
- * says nothing about the other connections (not even how many): remove the
166
- * entities that use it, and it goes with the last one.
167
91
  */
168
92
  class DeleteCredentialForUser {
169
93
  constructor({ credentialRepository, moduleRepository, getCredentialForUser }) {
@@ -177,15 +101,6 @@ class DeleteCredentialForUser {
177
101
  credentialId,
178
102
  user
179
103
  );
180
- assertCanManage(credential.tenancy.access, `credential ${credential.id}`);
181
- const usedBy =
182
- (await this.moduleRepository.findEntities({ credentialId: credential.id })) || [];
183
- if (usedBy.length > credential.entityIds.length) {
184
- throw Boom.conflict(
185
- `Credential ${credential.id} is still in use by other connections and cannot be deleted from here`,
186
- { code: 'CREDENTIAL_SHARED' }
187
- );
188
- }
189
104
  for (const entityId of credential.entityIds) {
190
105
  await this.moduleRepository.unsetCredential(entityId);
191
106
  }
@@ -1,5 +1,4 @@
1
1
  const Boom = require('@hapi/boom');
2
- const { assertCanManage } = require('../../modules/use-cases/manage-access');
3
2
 
4
3
  /**
5
4
  * Re-authorization of a credential the caller owns (Management API v2). It
@@ -15,13 +14,7 @@ async function credentialEntityType(getCredentialForUser, credentialId, user) {
15
14
  { code: 'CREDENTIAL_TYPE_UNKNOWN' }
16
15
  );
17
16
  }
18
- return {
19
- entityType: credential.type,
20
- credentialId: String(credential.id),
21
- // A re-authorization keeps the scope the connection already has.
22
- fixedScope: credential.tenancy?.scope,
23
- access: credential.tenancy?.access,
24
- };
17
+ return { entityType: credential.type, credentialId: String(credential.id) };
25
18
  }
26
19
 
27
20
  /** GET /api/v2/credentials/:credentialId/reauthorize */
@@ -32,12 +25,14 @@ class GetCredentialReauthorization {
32
25
  }
33
26
 
34
27
  async execute(credentialId, user, { step = 1, sessionId, state } = {}) {
35
- const { entityType, credentialId: boundCredentialId } =
36
- await credentialEntityType(this.getCredentialForUser, credentialId, user);
28
+ const binding = await credentialEntityType(
29
+ this.getCredentialForUser,
30
+ credentialId,
31
+ user
32
+ );
37
33
  return this.getAuthorizationStep.execute({
38
34
  user,
39
- entityType,
40
- credentialId: boundCredentialId,
35
+ ...binding,
41
36
  step,
42
37
  sessionId,
43
38
  state,
@@ -53,13 +48,11 @@ class ReauthorizeCredentialForUser {
53
48
  }
54
49
 
55
50
  async execute(credentialId, user, { data, step = 1, sessionId } = {}) {
56
- const { access, ...binding } = await credentialEntityType(
51
+ const binding = await credentialEntityType(
57
52
  this.getCredentialForUser,
58
53
  credentialId,
59
54
  user
60
55
  );
61
- // A credential an organization owns: the organization's admins only.
62
- assertCanManage(access, `credential ${credentialId}`);
63
56
  return this.submitAuthorizationStep.execute({
64
57
  user,
65
58
  ...binding,
@@ -12,9 +12,6 @@ const {
12
12
  getEntityTypeDefinitions,
13
13
  } = require('../modules/entity-type-registry');
14
14
  const { resolveOwnershipConfig } = require('../modules/entity-scope');
15
- const {
16
- resolveManagementApiConfig,
17
- } = require('../management-api/management-api-config');
18
15
 
19
16
  // Handlers without a database never load the encryption registry, so the
20
17
  // credential leaf keys are registered for redaction here too.
@@ -42,7 +39,6 @@ function registerCredentialLogKeys(appDefinition, moduleDefinitions) {
42
39
  * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
43
40
  * @throws {Error} Throws error if user.authModes.adopterJwt is enabled without a usable user.jwtConfig.
44
41
  * @throws {OwnershipConfigError} Throws if an ownership or scope setting is invalid (ADR-056 §3.1).
45
- * @throws {ManagementApiConfigError} Throws if `managementApi.proxy.scopes` is invalid (ADR-056 §3.5).
46
42
  * @example
47
43
  * const { integrations, userConfig, telemetry } = loadAppDefinition();
48
44
  * console.log(`Found ${integrations.length} integrations`);
@@ -88,11 +84,6 @@ function loadAppDefinition() {
88
84
  // individual entity stops the app at load.
89
85
  const ownership = resolveOwnershipConfig(appDefinition, entityTypeRegistry);
90
86
 
91
- // And for the Management API: `managementApi.proxy.scopes` decides whose
92
- // credentials the entity proxy may act through (ADR-056 §3.5), so an
93
- // unknown scope stops the app here rather than being read as a default.
94
- resolveManagementApiConfig(managementApi);
95
-
96
87
  registerCredentialLogKeys(appDefinition, moduleDefinitions);
97
88
 
98
89
  // Degrade consistently: an invalid telemetry block must never take down a
package/index.js CHANGED
@@ -71,6 +71,7 @@ const {
71
71
  Options,
72
72
  createIntegrationRouter,
73
73
  checkRequiredParams,
74
+ getModulesDefinitionFromIntegrationClasses,
74
75
  LoadIntegrationContextUseCase,
75
76
  } = require('./integrations/index');
76
77
  const {
@@ -112,9 +113,6 @@ const {
112
113
  listBindingScopes,
113
114
  OwnershipConfigError,
114
115
  } = require('./modules/index');
115
- const {
116
- ManagementApiConfigError,
117
- } = require('./management-api/management-api-config');
118
116
  const application = require('./application');
119
117
  const utils = require('./utils');
120
118
 
@@ -169,6 +167,8 @@ module.exports = {
169
167
  Options,
170
168
  checkRequiredParams,
171
169
  createIntegrationRouter,
170
+ // Deprecated (ADR-059): use getEntityTypeRegistry.
171
+ getModulesDefinitionFromIntegrationClasses,
172
172
  LoadIntegrationContextUseCase,
173
173
  CreateProcess,
174
174
  UpdateProcessState,
@@ -229,7 +229,6 @@ module.exports = {
229
229
  resolveScope,
230
230
  listBindingScopes,
231
231
  OwnershipConfigError,
232
- ManagementApiConfigError,
233
232
  // queues
234
233
  QueuerUtil,
235
234
 
@@ -4,6 +4,9 @@ const {
4
4
  createIntegrationRouter,
5
5
  checkRequiredParams,
6
6
  } = require('./integration-router');
7
+ const {
8
+ getModulesDefinitionFromIntegrationClasses,
9
+ } = require('./utils/map-integration-dto');
7
10
  const {
8
11
  LoadIntegrationContextUseCase,
9
12
  } = require('./use-cases/load-integration-context');
@@ -18,6 +21,8 @@ module.exports = {
18
21
  Options,
19
22
  createIntegrationRouter,
20
23
  checkRequiredParams,
24
+ // Deprecated (ADR-059): a wrapper over getEntityTypeRegistry.
25
+ getModulesDefinitionFromIntegrationClasses,
21
26
  LoadIntegrationContextUseCase,
22
27
  validateExtensionBinding,
23
28
  getExtensionRoutes,
@@ -65,6 +65,9 @@ const { createScopePolicy } = require('../modules/scope-policy');
65
65
  const {
66
66
  AuthorizeEntityForUser,
67
67
  } = require('../modules/use-cases/authorize-entity-for-user');
68
+ const {
69
+ CreateEntityFromCredential,
70
+ } = require('../modules/use-cases/create-entity-from-credential');
68
71
  const {
69
72
  ResolveIntegrationOwnership,
70
73
  } = require('./use-cases/resolve-integration-ownership');
@@ -199,14 +202,22 @@ function createIntegrationRouter() {
199
202
  });
200
203
 
201
204
  // The owner of what an authorization creates follows its scope (ADR-056 §3.2).
205
+ const processAuthorizationCallback = new ProcessAuthorizationCallback({
206
+ moduleRepository,
207
+ credentialRepository,
208
+ integrationRepository,
209
+ moduleDefinitions,
210
+ });
202
211
  const authorizeEntityForUser = new AuthorizeEntityForUser({
203
212
  scopePolicy,
204
- processAuthorizationCallback: new ProcessAuthorizationCallback({
205
- moduleRepository,
206
- credentialRepository,
207
- integrationRepository,
208
- moduleDefinitions,
209
- }),
213
+ processAuthorizationCallback,
214
+ });
215
+ // v1 POST /api/entity: same owner resolution as /api/authorize.
216
+ const createEntityFromCredential = new CreateEntityFromCredential({
217
+ authorizeEntityForUser,
218
+ processAuthorizationCallback,
219
+ credentialRepository,
220
+ moduleDefinitions,
210
221
  });
211
222
 
212
223
  const router = express();
@@ -231,6 +242,7 @@ function createIntegrationRouter() {
231
242
  getEntityOptionsById,
232
243
  refreshEntityOptions,
233
244
  authorizeEntityForUser,
245
+ createEntityFromCredential,
234
246
  });
235
247
  return router;
236
248
  }
@@ -279,11 +291,9 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
279
291
  } = useCases;
280
292
 
281
293
  // The owner of an integration the caller may reach (404 otherwise), which is
282
- // what the id-and-owner use cases below expect. `manage: true` is for
283
- // changing or deleting: an organization's integration needs an admin (403).
284
- const ownerOf = async (integrationId, user, options) =>
285
- (await getOwnedIntegrationRecord.execute(integrationId, user, options))
286
- .userId;
294
+ // what the id-and-owner use cases below expect.
295
+ const ownerOf = async (integrationId, user) =>
296
+ (await getOwnedIntegrationRecord.execute(integrationId, user)).userId;
287
297
 
288
298
  router.route('/api/integrations').get(
289
299
  catchAsyncError(async (req, res) => {
@@ -335,7 +345,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
335
345
 
336
346
  const integration = await updateIntegration.execute(
337
347
  req.params.integrationId,
338
- await ownerOf(req.params.integrationId, user, { manage: true }),
348
+ await ownerOf(req.params.integrationId, user),
339
349
  params.config
340
350
  );
341
351
  res.json(integration);
@@ -348,7 +358,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
348
358
  const params = checkRequiredParams(req.params, ['integrationId']);
349
359
  await deleteIntegrationForUser.execute(
350
360
  params.integrationId,
351
- await ownerOf(params.integrationId, user, { manage: true })
361
+ await ownerOf(params.integrationId, user)
352
362
  );
353
363
  res.status(204).json({});
354
364
  })
@@ -529,6 +539,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
529
539
  getEntityOptionsById,
530
540
  refreshEntityOptions,
531
541
  authorizeEntityForUser,
542
+ createEntityFromCredential,
532
543
  } = useCases;
533
544
 
534
545
  router.route('/api/authorize').get(
@@ -603,35 +614,20 @@ function setEntityRoutes(router, authenticateUser, useCases) {
603
614
  router.route('/api/entity').post(
604
615
  catchAsyncError(async (req, res) => {
605
616
  const user = await authenticateUser.execute(req);
606
- const userId = user.getId();
607
617
  const params = checkRequiredParams(req.body, [
608
618
  'entityType',
609
619
  'data',
610
620
  ]);
611
621
  checkRequiredParams(req.body.data, ['credential_id']);
612
622
 
613
- // May want to pass along the user ID as well so credential ID's can't be fished???
614
- const credential = await getCredentialForUser.execute(
615
- params.data.credential_id,
616
- userId
617
- );
618
-
619
- if (!credential) {
620
- throw Boom.badRequest('Invalid credential ID');
621
- }
622
-
623
- const module = await getModuleInstanceFromType.execute(
624
- userId,
625
- params.entityType
626
- );
627
- const entityDetails = await module.getEntityDetails(
628
- module.api,
629
- null,
630
- null,
631
- userId
623
+ // The owner follows the entity type's scope, as /api/authorize.
624
+ res.json(
625
+ await createEntityFromCredential.execute({
626
+ user,
627
+ entityType: params.entityType,
628
+ credentialId: params.data.credential_id,
629
+ })
632
630
  );
633
-
634
- res.json(await module.findOrCreateEntity(entityDetails));
635
631
  })
636
632
  );
637
633
 
@@ -40,7 +40,8 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
40
40
  */
41
41
  async findIntegrationsByOwnership(clauses) {
42
42
  if (!Array.isArray(clauses) || clauses.length === 0) return [];
43
- const records = await findMany(this.prisma, 'Integration', {
43
+ // Drained: a list can hold more than the first cursor batch (~101).
44
+ const records = await findManyDrained(this.prisma, 'Integration', {
44
45
  $or: clauses.map(({ userId, organizationId }) => ({
45
46
  userId: toObjectId(userId),
46
47
  organizationId:
@@ -1,7 +1,6 @@
1
1
  const Boom = require('@hapi/boom');
2
2
  const { describeRecord } = require('../../modules/scope-visibility');
3
3
  const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
4
- const { assertCanManage } = require('../../modules/use-cases/manage-access');
5
4
  const { isIntegrationNotFound } = require('../repositories/integration-not-found');
6
5
 
7
6
  /**
@@ -33,14 +32,7 @@ class GetOwnedIntegrationRecord {
33
32
  return describeRecord(record, this.scopePolicy.accessContext(user), 'integration');
34
33
  }
35
34
 
36
- /**
37
- * @param {string} integrationId
38
- * @param {Object} user - authenticated User
39
- * @param {{manage?: boolean}} [options] - `manage: true` for changing or
40
- * deleting: a person may use an organization's integration but only an
41
- * admin may change it (403 ORG_ADMIN_REQUIRED)
42
- */
43
- async execute(integrationId, user, { manage = false } = {}) {
35
+ async execute(integrationId, user) {
44
36
  let record = null;
45
37
  try {
46
38
  record = await this.integrationRepository.findIntegrationById(
@@ -55,9 +47,9 @@ class GetOwnedIntegrationRecord {
55
47
  }
56
48
  throw error;
57
49
  }
58
- const tenancy = record && this.describe(record, user);
59
- if (!tenancy) throw notFound(integrationId);
60
- if (manage) assertCanManage(tenancy.access, `integration ${integrationId}`);
50
+ if (!record || !this.describe(record, user)) {
51
+ throw notFound(integrationId);
52
+ }
61
53
  return record;
62
54
  }
63
55
  }
@@ -146,8 +138,7 @@ class UpdateIntegrationForUser {
146
138
  }
147
139
  const record = await this.getOwnedIntegrationRecord.execute(
148
140
  integrationId,
149
- user,
150
- { manage: true }
141
+ user
151
142
  );
152
143
  return this.updateIntegration.execute(record.id, record.userId, config);
153
144
  }
@@ -163,8 +154,7 @@ class RemoveIntegrationForUser {
163
154
  async execute(integrationId, user) {
164
155
  const record = await this.getOwnedIntegrationRecord.execute(
165
156
  integrationId,
166
- user,
167
- { manage: true }
157
+ user
168
158
  );
169
159
  await this.deleteIntegrationForUser.execute(record.id, record.userId);
170
160
  }
@@ -19,5 +19,48 @@ function mapIntegrationClassToIntegrationDTO(integration) {
19
19
  };
20
20
  }
21
21
 
22
+ let deprecationLogged = false;
22
23
 
23
- module.exports = { mapIntegrationClassToIntegrationDTO };
24
+ /**
25
+ * @deprecated Since ADR-059. Read `entityTypeRegistry` / `moduleDefinitions`
26
+ * from `loadAppDefinition()`, or call `getEntityTypeRegistry(appDefinition)`
27
+ * and read each entry's `definition`.
28
+ *
29
+ * Kept as a wrapper over the entity type registry so callers that imported it
30
+ * keep working. It returns the registry's definitions, so it follows the
31
+ * registry's rules: one definition per entity type name (the first wins when
32
+ * two integrations bind different definitions under one name, as
33
+ * ModuleFactory already picked), and, when `entities` is passed, registry-only
34
+ * types are included and conflicts throw `EntityRegistryError`.
35
+ *
36
+ * @param {Array<Function>} integrationClasses - integration classes
37
+ * @param {Object} [entities] - `appDefinition.entities` (ADR-059)
38
+ * @returns {Array<Object>} API module definitions
39
+ */
40
+ function getModulesDefinitionFromIntegrationClasses(
41
+ integrationClasses,
42
+ entities
43
+ ) {
44
+ // Required here, not at the top: modules/ must not load at require time
45
+ // of every integration use case that only wants the DTO mapper.
46
+ const {
47
+ getEntityTypeRegistry,
48
+ getEntityTypeDefinitions,
49
+ } = require('../../modules/entity-type-registry');
50
+ if (!deprecationLogged) {
51
+ deprecationLogged = true;
52
+ const { getLogger } = require('../../logs');
53
+ getLogger('frigg.integrations').warn(
54
+ 'getModulesDefinitionFromIntegrationClasses is deprecated; use getEntityTypeRegistry (ADR-059)',
55
+ { eventName: 'frigg.integrations.deprecated_module_definitions' }
56
+ );
57
+ }
58
+ return getEntityTypeDefinitions(
59
+ getEntityTypeRegistry({ integrations: integrationClasses, entities })
60
+ );
61
+ }
62
+
63
+ module.exports = {
64
+ mapIntegrationClassToIntegrationDTO,
65
+ getModulesDefinitionFromIntegrationClasses,
66
+ };