@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.
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +0 -2
- package/README.md +0 -57
- package/application/commands/integration-commands.js +18 -8
- package/application/index.js +6 -2
- package/database/encryption/README.md +2 -0
- package/database/encryption/encryption-schema-registry.js +35 -12
- package/database/encryption/field-encryption-service.js +30 -3
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- package/handlers/app-definition-loader.js +16 -13
- package/handlers/backend-utils.js +31 -25
- package/handlers/routers/health.js +1 -4
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +4 -2
- package/integrations/index.js +0 -4
- package/integrations/integration-router.js +16 -29
- package/integrations/utils/map-integration-dto.js +1 -15
- package/management-api/openapi/components.js +7 -1
- package/management-api/openapi/v2-operations.js +4 -0
- package/management-api/v2/dependencies.js +23 -13
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +6 -0
- package/modules/use-cases/entity-types.js +36 -12
- package/package.json +5 -6
- package/types/index.d.ts +1 -1
- package/types/modules/index.d.ts +58 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +122 -109
- package/user/use-cases/get-user-from-x-frigg-headers.js +51 -29
- package/user/user.js +0 -11
- package/types/user/index.d.ts +0 -67
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -65
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -73
- 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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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({
|
package/application/index.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
const moduleDefinitions = getEntityTypeDefinitions(
|
|
192
|
+
getEntityTypeRegistry({ integrations, entities })
|
|
193
|
+
);
|
|
186
194
|
const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
|
|
187
195
|
|
|
188
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
212
|
+
...customSchema,
|
|
213
|
+
Credential: { ...customSchema.Credential, fields },
|
|
196
214
|
};
|
|
197
215
|
|
|
198
216
|
logger.info(
|
|
199
|
-
`Registering module-level encryption for ${
|
|
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 (
|
|
252
|
-
loadModuleEncryptionSchemas(
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
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,
|
|
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
|
|
71
|
-
//
|
|
72
|
-
|
|
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,
|
|
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
|
-
|
|
22
|
-
|
|
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 } =
|
|
67
|
+
const { integrations: integrationClasses, moduleDefinitions } =
|
|
68
|
+
loadAppDefinition();
|
|
80
69
|
|
|
81
70
|
const { integrationRepository, moduleRepository } =
|
|
82
71
|
initializeRepositories();
|
|
83
72
|
|
|
84
|
-
const moduleFactory =
|
|
73
|
+
const moduleFactory = new ModuleFactory({
|
|
85
74
|
moduleRepository,
|
|
86
|
-
|
|
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 (
|
|
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 =
|
|
137
|
-
|
|
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
|
-
|
|
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 =
|
|
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 } =
|
|
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
|
|
package/integrations/index.js
CHANGED
|
@@ -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,
|