@friggframework/core 2.0.0--canary.678.58db4b2.0 → 2.0.0--canary.680.b3ca7f1.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 (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +0 -2
  3. package/README.md +0 -57
  4. package/application/commands/integration-commands.js +18 -8
  5. package/application/index.js +6 -2
  6. package/database/encryption/README.md +2 -0
  7. package/database/encryption/encryption-schema-registry.js +35 -12
  8. package/database/encryption/field-encryption-service.js +30 -3
  9. package/database/encryption/test-support/fake-ciphertext.js +15 -0
  10. package/handlers/app-definition-loader.js +16 -13
  11. package/handlers/backend-utils.js +31 -25
  12. package/handlers/routers/health.js +1 -4
  13. package/handlers/workers/integration-defined-workers.js +5 -2
  14. package/index.js +4 -2
  15. package/integrations/index.js +0 -4
  16. package/integrations/integration-router.js +16 -29
  17. package/integrations/utils/map-integration-dto.js +1 -15
  18. package/management-api/openapi/components.js +7 -1
  19. package/management-api/openapi/v2-operations.js +4 -0
  20. package/management-api/v2/dependencies.js +23 -13
  21. package/modules/entity-type-registry.js +311 -0
  22. package/modules/index.js +6 -0
  23. package/modules/use-cases/entity-types.js +36 -12
  24. package/package.json +5 -6
  25. package/types/index.d.ts +1 -1
  26. package/types/modules/index.d.ts +58 -0
  27. package/user/use-cases/get-user-from-adopter-jwt.js +122 -109
  28. package/user/use-cases/get-user-from-x-frigg-headers.js +51 -29
  29. package/user/user.js +0 -11
  30. package/types/user/index.d.ts +0 -67
  31. package/user/adopter-jwt-config.js +0 -177
  32. package/user/adopter-jwt-errors.js +0 -65
  33. package/user/adopter-token-verifier.js +0 -122
  34. package/user/app-user-helpers.js +0 -73
  35. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -130
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # Unreleased
2
+
3
+ #### Behaviour change: module credential fields are now encrypted
4
+
5
+ - `credentialFields` declared in an API module's `encryption` config (for example `Definition.encryption.credentialFields`) were never registered for encryption: `loadModuleEncryptionSchemas` required a path that does not exist and the error was swallowed. They are now registered, for modules bound by integrations and for modules registered in `appDefinition.entities` (ADR-059).
6
+ - Existing rows are not rewritten. A credential saved before this release stays plaintext until it is next written, and is read as stored.
7
+ - Reads now tolerate plaintext: a value is only treated as ciphertext when it has the exact `keyId:iv:ciphertext:encryptedKey` shape (previously any string with three or more colons, such as `https://user:pw@host:443`), and a value that fails to decrypt is returned as stored with a warning instead of failing the whole read.
8
+ - An app's own `encryption.schema` is no longer replaced when module fields are added to it.
9
+
10
+ ---
11
+
1
12
  # v1.2.2 (Fri Aug 09 2024)
2
13
 
3
14
  #### 🐛 Bug Fix
package/CLAUDE.md CHANGED
@@ -332,14 +332,12 @@ class MyIntegration extends IntegrationBase {
332
332
  - Password-based (bcrypt hashed)
333
333
  - Token-based (Bearer tokens)
334
334
  - App-based (external app user IDs)
335
- - Adopter JWT (`authModes.adopterJwt`, `jwtConfig`; `jose`, JWKS or HMAC; see `README.md`)
336
335
 
337
336
  **Use Cases**:
338
337
  - `login-user.js` - User authentication
339
338
  - `create-individual-user.js` - Create personal account
340
339
  - `create-organization-user.js` - Create business account
341
340
  - `get-user-from-bearer-token.js` - Token authentication
342
- - `get-user-from-adopter-jwt.js` - Adopter JWT verification, find-or-create (ADR-056 §2); `adopter-jwt-config.js` validates `jwtConfig` at load
343
341
 
344
342
  **Repositories**:
345
343
  - `user-repository-factory.js` - Creates database-specific repositories
package/README.md CHANGED
@@ -305,7 +305,6 @@ Comprehensive user authentication and authorization system supporting both indiv
305
305
  - **Password-based**: Traditional username/password authentication
306
306
  - **Token-based**: Bearer token authentication with session management
307
307
  - **App-based**: External app user ID authentication (passwordless)
308
- - **Adopter token (JWT)**: A token issued by your own identity system, verified by Frigg and safe to send from a browser (see below)
309
308
 
310
309
  **Usage:**
311
310
  ```javascript
@@ -353,62 +352,6 @@ console.log('Individual user:', user.getIndividualUser());
353
352
  console.log('Organization user:', user.getOrganizationUser());
354
353
  ```
355
354
 
356
- #### Adopter token (JWT) authentication
357
-
358
- For a multi-tenant product (ADR-056 §2), let the browser send the token your identity system
359
- already issues. Frigg verifies the signature and claims, then finds or creates the individual
360
- user and the organization user the token names. Unlike `sharedSecret` (`x-frigg-*` headers), a
361
- token cannot name a user other than the one it was issued for, so it is safe in a browser.
362
-
363
- ```javascript
364
- // backend/index.js
365
- const appDefinition = {
366
- user: {
367
- primary: 'organization',
368
- individualUserRequired: true,
369
- organizationUserRequired: true,
370
- authModes: { adopterJwt: true, friggToken: false, sharedSecret: false },
371
- jwtConfig: {
372
- jwksUri: 'https://login.example.com/.well-known/jwks.json',
373
- issuer: 'https://login.example.com/',
374
- audience: 'my-frigg-api', // required with jwksUri
375
- },
376
- },
377
- };
378
- ```
379
-
380
- The client sends `Authorization: Bearer <token>`. `x-frigg-appuserid` / `x-frigg-apporgid`, if
381
- also sent, must match the token or the request is refused with 403. That match rule lives in
382
- `user/use-cases/authenticate-user.js` (`validateUserMatch`), not in the token verifier. A bearer
383
- value that is not a three-part JWT is not offered to the verifier at all; it falls through to
384
- `friggToken` (or is a 401 when that mode is off).
385
-
386
- | `jwtConfig` field | Default | Meaning |
387
- |---|---|---|
388
- | `jwksUri` | | Remote JWKS (rotating keys, one per verifier, cached by jose). Must be `https`; `http` only for `localhost`, `127.0.0.0/8` or `::1`. Set exactly one of this and `secret`. |
389
- | `secret` | | Shared HMAC secret. Read it from an environment variable. |
390
- | `issuer` | not checked | Required `iss`. With `jwksUri`, a startup warning is logged when it is unset. |
391
- | `audience` | not checked with `secret`; **required with `jwksUri`** | Accepted `aud` (a string or a list). |
392
- | `algorithms` | `['ES256','RS256']` with `jwksUri`, `['HS256']` with `secret` | Allow-list. HS* is only accepted with `secret`, never with `jwksUri`. The older single `algorithm` is a deprecated alias; setting both is an error. |
393
- | `clockToleranceSec` | `30` | Skew allowed on `exp` and `nbf`. |
394
- | `userIdClaim` | `sub` | App user id (required in the token). |
395
- | `orgIdClaim` | `org_id` | App organization id (optional). |
396
- | `roleClaim` | `org_role` | Read and exposed as `user.getOrgRole()`; not stored yet. |
397
-
398
- Rules worth knowing:
399
-
400
- - Tokens must carry `exp`. `alg: none` and any algorithm outside the allow-list are refused.
401
- - Startup fails (`loadAppDefinition` throws) when `adopterJwt` is on and the config is unusable:
402
- neither or both of `jwksUri` and `secret`, a `jwksUri` that is not https (or loopback http),
403
- no `audience` with `jwksUri`, or an algorithm list that does not fit the key type.
404
- - Failures are `401` with a stable `data.reason`: `token_expired`, `token_not_yet_valid`,
405
- `token_invalid_signature`, `token_wrong_issuer`, `token_wrong_audience`,
406
- `token_missing_user_claim`, `token_malformed`. The message never contains the token or its claims.
407
- Any other verification failure, such as an unreachable or unresolvable JWKS, is `503`
408
- `jwks_unavailable`, not a 401, so clients do not drop good sessions.
409
- - A person belongs to one organization for now: a token naming a second one gets `403`
410
- `multi_org_unsupported` and nothing is moved (ADR-056 §3.6 replaces this with memberships).
411
-
412
355
  ### 8. Lambda Utilities (`/lambda`)
413
356
 
414
357
  AWS Lambda-specific utilities and helpers.
@@ -30,8 +30,9 @@ const {
30
30
  PatchIntegrationConfig,
31
31
  } = require('../../integrations/use-cases/patch-integration-config');
32
32
  const {
33
- getModulesDefinitionFromIntegrationClasses,
34
- } = require('../../integrations/utils/map-integration-dto');
33
+ getEntityTypeRegistry,
34
+ getEntityTypeDefinitions,
35
+ } = require('../../modules/entity-type-registry');
35
36
 
36
37
  const ERROR_CODE_MAP = {
37
38
  ENTITY_NOT_FOUND: 401,
@@ -51,7 +52,16 @@ function mapErrorToResponse(error) {
51
52
  };
52
53
  }
53
54
 
54
- function createIntegrationCommands({ integrationClass } = {}) {
55
+ /**
56
+ * @param {Object} [params]
57
+ * @param {Function} [params.integrationClass]
58
+ * @param {Array<object>} [params.moduleDefinitions] - the app's offered module
59
+ * definitions (`loadAppDefinition().moduleDefinitions`). Integration code
60
+ * calls this from a constructor with no app definition in hand, so when it
61
+ * is omitted the commands hydrate with this integration's own modules only
62
+ * (a registry built from `{ integrations: [integrationClass] }`).
63
+ */
64
+ function createIntegrationCommands({ integrationClass, moduleDefinitions } = {}) {
55
65
  // Always use Frigg's default repositories and use cases
56
66
  const integrationRepository = createIntegrationRepository();
57
67
 
@@ -108,13 +118,13 @@ function createIntegrationCommands({ integrationClass } = {}) {
108
118
 
109
119
  const moduleRepository = createModuleRepository();
110
120
 
111
- const moduleDefinitions = getModulesDefinitionFromIntegrationClasses([
112
- integrationClass,
113
- ]);
114
-
115
121
  const moduleFactory = new ModuleFactory({
116
122
  moduleRepository,
117
- moduleDefinitions,
123
+ moduleDefinitions:
124
+ moduleDefinitions ??
125
+ getEntityTypeDefinitions(
126
+ getEntityTypeRegistry({ integrations: [integrationClass] })
127
+ ),
118
128
  });
119
129
 
120
130
  const loadIntegrationContextUseCase = new LoadIntegrationContextUseCase({
@@ -17,6 +17,7 @@ const { createUsageCommands } = require('./commands/usage-commands');
17
17
  *
18
18
  * @param {Object} params
19
19
  * @param {Object} params.integrationClass - Integration class (required)
20
+ * @param {Array<object>} [params.moduleDefinitions] - the app's offered module definitions; defaults to the integration's own modules
20
21
  * @returns {Object} Unified commands object with all CRUD operations
21
22
  *
22
23
  * @example
@@ -24,9 +25,12 @@ const { createUsageCommands } = require('./commands/usage-commands');
24
25
  * const user = await commands.createUser({ username: 'user@example.com' });
25
26
  * const credential = await commands.createCredential({ userId: user.id, ... });
26
27
  */
27
- function createFriggCommands({ integrationClass }) {
28
+ function createFriggCommands({ integrationClass, moduleDefinitions }) {
28
29
  // All commands use Frigg's default repositories and use cases
29
- const integrationCommands = createIntegrationCommands({ integrationClass });
30
+ const integrationCommands = createIntegrationCommands({
31
+ integrationClass,
32
+ moduleDefinitions,
33
+ });
30
34
 
31
35
  const userCommands = createUserCommands();
32
36
 
@@ -428,6 +428,8 @@ Format: "keyId:encryptedText:encryptedKey"
428
428
  Example: "base64KeyId:iv:ciphertext:base64EncryptedDataKey"
429
429
  ```
430
430
 
431
+ **Recognising ciphertext on read.** A value is decrypted only when it has exactly four colon-separated parts: base64 `keyId`, 32-hex-character `iv`, hex ciphertext, base64 `encryptedKey`. Plaintext that merely contains colons (`https://user:pw@host:443`) is left alone. A value with that shape that still fails to decrypt is returned as stored and logged as a warning, so one bad field never fails a whole read. A field that becomes encrypted later (for example a module's `credentialFields`) stays plaintext in existing rows until they are next written.
432
+
431
433
  **Why Envelope Encryption?**
432
434
 
433
435
  - Reduces KMS API calls (one DEK per field, cached)
@@ -10,6 +10,11 @@
10
10
 
11
11
  const { logger } = require('./logger');
12
12
  const { registerDeniedKeys } = require('../../logs/denied-keys');
13
+ const {
14
+ getEntityTypeRegistry,
15
+ getEntityTypeDefinitions,
16
+ EntityRegistryError,
17
+ } = require('../../modules/entity-type-registry');
13
18
 
14
19
  /**
15
20
  * Core encryption schema (immutable - cannot be overridden by custom schemas)
@@ -166,8 +171,9 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
166
171
  * Each module can declare credentialFields to encrypt in its encryption config.
167
172
  *
168
173
  * @param {Array} integrations - Array of integration classes with modules
174
+ * @param {object} [entities] - `appDefinition.entities` (ADR-059 registry)
169
175
  */
170
- function loadModuleEncryptionSchemas(integrations) {
176
+ function loadModuleEncryptionSchemas(integrations, entities) {
171
177
  if (!integrations) {
172
178
  throw new Error('integrations parameter is required');
173
179
  }
@@ -176,27 +182,39 @@ function loadModuleEncryptionSchemas(integrations) {
176
182
  throw new Error('integrations must be an array');
177
183
  }
178
184
 
179
- if (integrations.length === 0) {
185
+ // `entities` is the app definition's registry (ADR-059): a module an app
186
+ // registers without any integration still carries credentials to encrypt.
187
+ if (integrations.length === 0 && !entities) {
180
188
  return;
181
189
  }
182
190
 
183
- const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
184
-
185
- const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
191
+ const moduleDefinitions = getEntityTypeDefinitions(
192
+ getEntityTypeRegistry({ integrations, entities })
193
+ );
186
194
  const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
187
195
 
188
- if (credentialFields.length === 0) {
196
+ // Core fields are encrypted already and a custom schema may not repeat
197
+ // them. Everything the app registered itself stays: module fields are added
198
+ // to the custom schema, never substituted for it (other models included).
199
+ const coreFields = new Set(CORE_ENCRYPTION_SCHEMA.Credential?.fields || []);
200
+ const fields = [
201
+ ...new Set([
202
+ ...(customSchema.Credential?.fields || []),
203
+ ...credentialFields.filter((field) => !coreFields.has(field)),
204
+ ]),
205
+ ];
206
+
207
+ if (fields.length === 0) {
189
208
  return;
190
209
  }
191
210
 
192
211
  const moduleSchema = {
193
- Credential: {
194
- fields: credentialFields
195
- }
212
+ ...customSchema,
213
+ Credential: { ...customSchema.Credential, fields },
196
214
  };
197
215
 
198
216
  logger.info(
199
- `Registering module-level encryption for ${credentialFields.length} credential fields`
217
+ `Registering module-level encryption for ${fields.length} credential fields`
200
218
  );
201
219
 
202
220
  registerCustomSchema(moduleSchema);
@@ -248,10 +266,15 @@ function loadCustomEncryptionSchema() {
248
266
 
249
267
  // Load module-level encryption schemas from integrations
250
268
  const integrations = appDefinition.integrations;
251
- if (integrations && Array.isArray(integrations)) {
252
- loadModuleEncryptionSchemas(integrations);
269
+ if (Array.isArray(integrations) || appDefinition.entities) {
270
+ loadModuleEncryptionSchemas(
271
+ Array.isArray(integrations) ? integrations : [],
272
+ appDefinition.entities
273
+ );
253
274
  }
254
275
  } catch (error) {
276
+ // A bad entity registry is a configuration error, not "no custom schema".
277
+ if (error instanceof EntityRegistryError) throw error;
255
278
  // Silently ignore errors - custom schema is optional
256
279
  // This handles cases like:
257
280
  // - Backend package.json not found (tests, standalone usage)
@@ -4,6 +4,16 @@
4
4
  * Infrastructure layer service that orchestrates field-level encryption/decryption.
5
5
  * Handles nested JSON paths (e.g., 'data.access_token') and bulk operations.
6
6
  */
7
+ const { logger } = require('./logger');
8
+
9
+ // Cryptor.encrypt() returns `${keyId}:${iv}:${ciphertext}:${encryptedKey}`:
10
+ // keyId and encryptedKey are base64, iv is 16 bytes of hex, ciphertext is hex.
11
+ // Plaintext with three colons (`https://user:pw@host:443`, `a:b:c:d`) must not
12
+ // be mistaken for it.
13
+ const BASE64_PART = /^[A-Za-z0-9+/]+={0,2}$/;
14
+ const IV_PART = /^[0-9a-f]{32}$/i;
15
+ const CIPHERTEXT_PART = /^(?:[0-9a-f]{2})+$/i;
16
+
7
17
  class FieldEncryptionService {
8
18
  constructor({ cryptor, schema }) {
9
19
  if (!cryptor) {
@@ -97,12 +107,23 @@ class FieldEncryptionService {
97
107
  const decryptionPromises = fields.map(async (fieldPath) => {
98
108
  const value = this._getNestedValue(decrypted, fieldPath);
99
109
 
100
- if (this._isEncrypted(value)) {
110
+ if (!this._isEncrypted(value)) {
111
+ return null;
112
+ }
113
+ try {
101
114
  const decryptedValue = await this.cryptor.decrypt(value);
102
115
  const deserializedValue = this._deserializeAfterDecryption(decryptedValue);
103
116
  return { fieldPath, decryptedValue: deserializedValue };
117
+ } catch (error) {
118
+ // A value that looks like ciphertext but is not (or was written
119
+ // before the field was encrypted) is read as it is stored. One
120
+ // such field must not fail the whole read, findMany included.
121
+ logger.warn(
122
+ `Could not decrypt ${modelName}.${fieldPath}; returning the stored value`,
123
+ { errorName: error?.name }
124
+ );
125
+ return null;
104
126
  }
105
- return null;
106
127
  });
107
128
 
108
129
  const results = await Promise.all(decryptionPromises);
@@ -152,7 +173,13 @@ class FieldEncryptionService {
152
173
  }
153
174
 
154
175
  const parts = value.split(':');
155
- return parts.length >= 4;
176
+ return (
177
+ parts.length === 4 &&
178
+ BASE64_PART.test(parts[0]) &&
179
+ IV_PART.test(parts[1]) &&
180
+ CIPHERTEXT_PART.test(parts[2]) &&
181
+ BASE64_PART.test(parts[3])
182
+ );
156
183
  }
157
184
 
158
185
  _getNestedValue(obj, path) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Test double for Cryptor output. Same shape as the real thing
3
+ * (`keyId:iv:ciphertext:encryptedKey`: base64, 16-byte hex, hex, base64) so
4
+ * FieldEncryptionService._isEncrypted accepts it, with the plaintext
5
+ * recoverable for the fake decrypt.
6
+ */
7
+ const fakeCiphertext = (plain) =>
8
+ `a2V5SWQ=:${'0'.repeat(32)}:${Buffer.from(String(plain)).toString(
9
+ 'hex'
10
+ )}:ZW5jS2V5`;
11
+
12
+ const fakePlaintext = (ciphertext) =>
13
+ Buffer.from(String(ciphertext).split(':')[2], 'hex').toString();
14
+
15
+ module.exports = { fakeCiphertext, fakePlaintext };
@@ -2,22 +2,20 @@ const { findNearestBackendPackageJson } = require('@friggframework/core/utils');
2
2
  const path = require('node:path');
3
3
  const fs = require('fs-extra');
4
4
  const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
5
- const { assertAdopterJwtConfig } = require('../user/adopter-jwt-config');
6
5
  const { registerDeniedKeys } = require('../logs/denied-keys');
7
6
  const {
8
7
  extractCredentialFieldsFromModules,
9
8
  } = require('../database/encryption/encryption-schema-registry');
10
9
  const {
11
- getModulesDefinitionFromIntegrationClasses,
12
- } = require('../integrations/utils/map-integration-dto');
10
+ getEntityTypeRegistry,
11
+ getEntityTypeDefinitions,
12
+ } = require('../modules/entity-type-registry');
13
13
 
14
14
  // Handlers without a database never load the encryption registry, so the
15
15
  // credential leaf keys are registered for redaction here too.
16
- function registerCredentialLogKeys(appDefinition, integrations) {
16
+ function registerCredentialLogKeys(appDefinition, moduleDefinitions) {
17
17
  try {
18
- extractCredentialFieldsFromModules(
19
- getModulesDefinitionFromIntegrationClasses(integrations)
20
- );
18
+ extractCredentialFieldsFromModules(moduleDefinitions);
21
19
  } catch {
22
20
  // An odd integration shape must not stop the app from loading.
23
21
  }
@@ -34,10 +32,9 @@ function registerCredentialLogKeys(appDefinition, integrations) {
34
32
  * @function loadAppDefinition
35
33
  * @description Searches for the nearest backend package.json, loads the corresponding index.js file,
36
34
  * and extracts the application definition containing integrations and user configuration.
37
- * @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null, managementApi: object}} An object containing the application definition.
35
+ * @returns {{integrations: Array<object>, entities: object | null, entityTypeRegistry: Map<string, object>, moduleDefinitions: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null, managementApi: object}} An object containing the application definition.
38
36
  * @throws {Error} Throws error if backend package.json cannot be found.
39
37
  * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
40
- * @throws {Error} Throws error if user.authModes.adopterJwt is enabled without a usable user.jwtConfig.
41
38
  * @example
42
39
  * const { integrations, userConfig, telemetry } = loadAppDefinition();
43
40
  * console.log(`Found ${integrations.length} integrations`);
@@ -59,6 +56,7 @@ function loadAppDefinition() {
59
56
 
60
57
  const {
61
58
  integrations = [],
59
+ entities = null,
62
60
  user: userConfig = null,
63
61
  adminScripts = [],
64
62
  reports = [],
@@ -67,11 +65,13 @@ function loadAppDefinition() {
67
65
  managementApi = {},
68
66
  } = appDefinition;
69
67
 
70
- // Fail at load, not on the first request: an adopter-token mode with no
71
- // key source would otherwise start cleanly and answer every login with 500.
72
- assertAdopterJwtConfig(userConfig);
68
+ // Fail fast: a bad `entities` registry (key/name mismatch, conflicting
69
+ // definitions, `$infer: false` with an unregistered module) must stop the
70
+ // app at load, not on the first request that needs a module.
71
+ const entityTypeRegistry = getEntityTypeRegistry(appDefinition);
72
+ const moduleDefinitions = getEntityTypeDefinitions(entityTypeRegistry);
73
73
 
74
- registerCredentialLogKeys(appDefinition, integrations);
74
+ registerCredentialLogKeys(appDefinition, moduleDefinitions);
75
75
 
76
76
  // Degrade consistently: an invalid telemetry block must never take down a
77
77
  // router bundle that loads the app definition at module scope (telemetry is
@@ -96,6 +96,9 @@ function loadAppDefinition() {
96
96
 
97
97
  return {
98
98
  integrations,
99
+ entities,
100
+ entityTypeRegistry,
101
+ moduleDefinitions,
99
102
  userConfig,
100
103
  adminScripts,
101
104
  reports,
@@ -18,8 +18,9 @@ const {
18
18
  createModuleRepository,
19
19
  } = require('../modules/repositories/module-repository-factory');
20
20
  const {
21
- getModulesDefinitionFromIntegrationClasses,
22
- } = require('../integrations/utils/map-integration-dto');
21
+ getEntityTypeRegistry,
22
+ getEntityTypeDefinitions,
23
+ } = require('../modules/entity-type-registry');
23
24
  const { processNotFound } = require('../integrations/use-cases/process-errors');
24
25
 
25
26
  const loadRouterFromObject = (IntegrationClass, routerObject) => {
@@ -61,30 +62,18 @@ const initializeRepositories = () => {
61
62
  return { processRepository, integrationRepository, moduleRepository };
62
63
  };
63
64
 
64
- const createModuleFactoryWithDefinitions = (
65
- moduleRepository,
66
- integrationClasses
67
- ) => {
68
- const moduleDefinitions =
69
- getModulesDefinitionFromIntegrationClasses(integrationClasses);
70
-
71
- return new ModuleFactory({
72
- moduleRepository,
73
- moduleDefinitions,
74
- });
75
- };
76
-
77
65
  const loadIntegrationForWebhook = async (integrationId) => {
78
66
  const { loadAppDefinition } = require('./app-definition-loader');
79
- const { integrations: integrationClasses } = loadAppDefinition();
67
+ const { integrations: integrationClasses, moduleDefinitions } =
68
+ loadAppDefinition();
80
69
 
81
70
  const { integrationRepository, moduleRepository } =
82
71
  initializeRepositories();
83
72
 
84
- const moduleFactory = createModuleFactoryWithDefinitions(
73
+ const moduleFactory = new ModuleFactory({
85
74
  moduleRepository,
86
- integrationClasses
87
- );
75
+ moduleDefinitions,
76
+ });
88
77
 
89
78
  const getIntegrationInstance = new GetIntegrationInstance({
90
79
  integrationRepository,
@@ -129,13 +118,23 @@ const integrationExists = async (integrationId) => {
129
118
  }
130
119
  };
131
120
 
132
- const loadIntegrationForProcess = async (processId, integrationClass) => {
121
+ const loadIntegrationForProcess = async (
122
+ processId,
123
+ integrationClass,
124
+ moduleDefinitions
125
+ ) => {
133
126
  const { processRepository, integrationRepository, moduleRepository } =
134
127
  initializeRepositories();
135
128
 
136
- const moduleFactory = createModuleFactoryWithDefinitions(moduleRepository, [
137
- integrationClass,
138
- ]);
129
+ const moduleFactory = new ModuleFactory({
130
+ moduleRepository,
131
+ moduleDefinitions:
132
+ moduleDefinitions ??
133
+ // No app definition handed in: this integration's own modules only.
134
+ getEntityTypeDefinitions(
135
+ getEntityTypeRegistry({ integrations: [integrationClass] })
136
+ ),
137
+ });
139
138
 
140
139
  const getIntegrationInstance = new GetIntegrationInstance({
141
140
  integrationRepository,
@@ -161,7 +160,13 @@ const loadIntegrationForProcess = async (processId, integrationClass) => {
161
160
  return instance;
162
161
  };
163
162
 
164
- const createQueueWorker = (integrationClass) => {
163
+ /**
164
+ * @param {Function} integrationClass
165
+ * @param {{moduleDefinitions?: Array<object>}} [options] - the app's offered
166
+ * module definitions (`loadAppDefinition().moduleDefinitions`). When omitted
167
+ * the worker hydrates with this integration's own modules only.
168
+ */
169
+ const createQueueWorker = (integrationClass, { moduleDefinitions } = {}) => {
165
170
  const integrationName = integrationClass.Definition.name;
166
171
 
167
172
  class QueueWorker extends Worker {
@@ -183,7 +188,8 @@ const createQueueWorker = (integrationClass) => {
183
188
  console.log(`[QueueWorker] hydrating by processId`, logCtx);
184
189
  integrationInstance = await loadIntegrationForProcess(
185
190
  params.data.processId,
186
- integrationClass
191
+ integrationClass,
192
+ moduleDefinitions
187
193
  );
188
194
  console.log(`[QueueWorker] hydrated`, {
189
195
  ...logCtx,
@@ -2,9 +2,6 @@ const { Router } = require('express');
2
2
  const { createAppHandler } = require('./../app-handler-helpers');
3
3
  const { loadAppDefinition } = require('./../app-definition-loader');
4
4
  const { ModuleFactory } = require('../../modules/module-factory');
5
- const {
6
- getModulesDefinitionFromIntegrationClasses,
7
- } = require('../../integrations/utils/map-integration-dto');
8
5
  const {
9
6
  createModuleRepository,
10
7
  } = require('../../modules/repositories/module-repository-factory');
@@ -45,7 +42,7 @@ try {
45
42
  integrationClasses = appDef.integrations || [];
46
43
 
47
44
  const moduleRepository = createModuleRepository();
48
- const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrationClasses);
45
+ const { moduleDefinitions } = appDef;
49
46
 
50
47
  moduleFactory = new ModuleFactory({
51
48
  moduleRepository,
@@ -10,10 +10,13 @@ const { createQueueWorker } = require('../backend-utils');
10
10
  // const { getExtensionWorkers } = require('../../integrations/extension');
11
11
 
12
12
  const handlers = {};
13
- const { integrations: integrationClasses } = loadAppDefinition();
13
+ const { integrations: integrationClasses, moduleDefinitions } =
14
+ loadAppDefinition();
14
15
 
15
16
  integrationClasses.forEach((IntegrationClass) => {
16
- const defaultQueueWorker = createQueueWorker(IntegrationClass);
17
+ const defaultQueueWorker = createQueueWorker(IntegrationClass, {
18
+ moduleDefinitions,
19
+ });
17
20
 
18
21
  handlers[`${IntegrationClass.Definition.name}`] = {
19
22
  queueWorker: createHandler({
package/index.js CHANGED
@@ -63,7 +63,6 @@ const {
63
63
  Options,
64
64
  createIntegrationRouter,
65
65
  checkRequiredParams,
66
- getModulesDefinitionFromIntegrationClasses,
67
66
  LoadIntegrationContextUseCase,
68
67
  } = require('./integrations/index');
69
68
  const {
@@ -99,6 +98,8 @@ const {
99
98
  Requester,
100
99
  ModuleConstants,
101
100
  ModuleFactory,
101
+ getEntityTypeRegistry,
102
+ EntityRegistryError,
102
103
  } = require('./modules/index');
103
104
  const application = require('./application');
104
105
  const utils = require('./utils');
@@ -150,7 +151,6 @@ module.exports = {
150
151
  Options,
151
152
  checkRequiredParams,
152
153
  createIntegrationRouter,
153
- getModulesDefinitionFromIntegrationClasses,
154
154
  LoadIntegrationContextUseCase,
155
155
  CreateProcess,
156
156
  UpdateProcessState,
@@ -206,6 +206,8 @@ module.exports = {
206
206
  Requester,
207
207
  ModuleConstants,
208
208
  ModuleFactory,
209
+ getEntityTypeRegistry,
210
+ EntityRegistryError,
209
211
  // queues
210
212
  QueuerUtil,
211
213
 
@@ -4,9 +4,6 @@ const {
4
4
  createIntegrationRouter,
5
5
  checkRequiredParams,
6
6
  } = require('./integration-router');
7
- const {
8
- getModulesDefinitionFromIntegrationClasses,
9
- } = require('./utils/map-integration-dto');
10
7
  const {
11
8
  LoadIntegrationContextUseCase,
12
9
  } = require('./use-cases/load-integration-context');
@@ -21,7 +18,6 @@ module.exports = {
21
18
  Options,
22
19
  createIntegrationRouter,
23
20
  checkRequiredParams,
24
- getModulesDefinitionFromIntegrationClasses,
25
21
  LoadIntegrationContextUseCase,
26
22
  validateExtensionBinding,
27
23
  getExtensionRoutes,