@friggframework/core 2.0.0-next.111 → 2.0.0-next.113
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/.eslintrc.json +18 -1
- package/CLAUDE.md +101 -13
- package/README.md +19 -22
- package/core/CLAUDE.md +28 -13
- package/core/Worker.js +32 -37
- package/core/create-handler.js +97 -197
- package/core/index.js +8 -1
- package/core/invocation-scope.js +225 -0
- package/core/secrets-to-env.js +7 -6
- package/database/config.js +0 -7
- package/database/documentdb-encryption-service.js +6 -2
- package/database/documentdb-utils.js +2 -1
- package/database/encryption/README.md +25 -1
- package/database/encryption/encryption-schema-registry.js +3 -0
- package/database/encryption/integration-mapping-encryption.js +97 -0
- package/database/encryption/logger.js +5 -2
- package/database/encryption/prisma-encryption-extension.js +20 -9
- package/errors/fetch-error.js +48 -65
- package/handlers/app-definition-loader.js +38 -2
- package/handlers/app-handler-helpers.js +18 -4
- package/handlers/backend-utils.js +10 -13
- package/handlers/database-migration-handler.js +10 -1
- package/handlers/integration-event-dispatcher.js +2 -2
- package/handlers/routers/integration-webhook-routers.js +13 -6
- package/handlers/routers/websocket.js +27 -7
- package/handlers/workers/db-migration.js +12 -1
- package/handlers/workers/dlq-processor.js +64 -38
- package/index.js +19 -1
- package/integrations/integration-base.js +56 -38
- package/integrations/integration-router.js +21 -20
- package/integrations/repositories/integration-mapping-query-pipeline.js +152 -0
- package/integrations/repositories/integration-mapping-query.js +193 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +48 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +72 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +118 -0
- package/integrations/use-cases/delete-integration-for-user.js +24 -13
- package/jest.config.js +2 -0
- package/logs/.eslintrc.json +26 -1
- package/logs/__fixtures__/events.js +82 -0
- package/logs/__fixtures__/matchers.js +45 -0
- package/logs/__fixtures__/secrets.js +59 -0
- package/logs/__fixtures__/vectors.js +329 -0
- package/logs/__fixtures__/with-env.js +45 -0
- package/logs/context.js +109 -0
- package/logs/contract-keys.js +67 -0
- package/logs/debug-shims.js +46 -0
- package/logs/denied-keys.js +21 -0
- package/logs/index.js +14 -3
- package/logs/jest-logger-setup.js +25 -0
- package/logs/levels.js +71 -0
- package/logs/logger-runtime.js +137 -0
- package/logs/logger.js +134 -42
- package/logs/record.js +276 -0
- package/logs/redact.js +328 -0
- package/logs/serialize.js +329 -0
- package/logs/sinks.js +93 -0
- package/logs/summarize-event.js +163 -0
- package/modules/module.js +32 -19
- package/modules/requester/oauth-2.js +28 -22
- package/modules/requester/requester.js +69 -6
- package/package.json +5 -6
- package/queues/queue-delivery.js +41 -0
- package/queues/queuer-util.js +1 -15
- package/telemetry/bind-telemetry-context.js +4 -0
- package/telemetry/instrument-handler.js +19 -2
- package/telemetry/no-op-telemetry.js +4 -0
- package/telemetry/otel-telemetry.js +25 -6
- package/telemetry/telemetry-context.js +19 -8
- package/telemetry/telemetry-runtime.js +17 -0
- package/telemetry/telemetry-service.js +1 -0
- package/types/errors/index.d.ts +24 -10
- package/types/logs/index.d.ts +61 -2
- package/user/repositories/user-repository-documentdb.js +10 -42
|
@@ -680,7 +680,11 @@ The encryption system uses **fail-fast error handling**:
|
|
|
680
680
|
|
|
681
681
|
### Logging Configuration
|
|
682
682
|
|
|
683
|
-
|
|
683
|
+
`FRIGG_LOG_LEVEL` is the one Frigg log level. The level rules, the record
|
|
684
|
+
fields and the redaction rules are in the
|
|
685
|
+
[Logging guide](../../../../docs/guides/LOGGING.md). The encryption logger
|
|
686
|
+
still writes text lines through `console` with the same level names until it
|
|
687
|
+
moves to the structured logger (ADR-048, incremental adoption).
|
|
684
688
|
|
|
685
689
|
```bash
|
|
686
690
|
# Production (minimal logging)
|
|
@@ -814,6 +818,26 @@ export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
|
|
|
814
818
|
❌ Query on encrypted fields (not supported)
|
|
815
819
|
❌ Manually decrypt data (use extension)
|
|
816
820
|
|
|
821
|
+
The integration mapping repositories' `queryMappings()` (PostgreSQL, MongoDB
|
|
822
|
+
and DocumentDB) filters and sorts inside the `mapping` JSON, so it refuses to
|
|
823
|
+
run while field-level encryption still encrypts `IntegrationMapping.mapping` on
|
|
824
|
+
write. Opt the field out in the app definition, together with any nested
|
|
825
|
+
`mapping.*` path that `encryption.schema` encrypts; the Prisma extension and
|
|
826
|
+
`DocumentDBEncryptionService` both honor the opt-out:
|
|
827
|
+
|
|
828
|
+
```javascript
|
|
829
|
+
encryption: {
|
|
830
|
+
disable: { IntegrationMapping: ['mapping'] },
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
The opt-out applies to writes only. Rows written encrypted before it stay
|
|
835
|
+
readable, but `queryMappings()` does not match them until they are written
|
|
836
|
+
again. It returns the rows it matches decrypted, like
|
|
837
|
+
`findMappingsByIntegration()`: a nested `mapping.*` path written encrypted
|
|
838
|
+
before its opt-out reads plain, though conditions and ordering on that path
|
|
839
|
+
still see the ciphertext until the row is written again.
|
|
840
|
+
|
|
817
841
|
## Future Enhancements
|
|
818
842
|
|
|
819
843
|
### Planned
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const { logger } = require('./logger');
|
|
12
|
+
const { registerDeniedKeys } = require('../../logs/denied-keys');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Core encryption schema (immutable - cannot be overridden by custom schemas)
|
|
@@ -120,6 +121,7 @@ function registerCustomSchema(schema) {
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
customSchema = { ...schema };
|
|
124
|
+
registerDeniedKeys(Object.values(schema).flatMap((config) => config.fields));
|
|
123
125
|
logger.info(
|
|
124
126
|
`Registered custom encryption schema for models: ${Object.keys(customSchema).join(', ')}`
|
|
125
127
|
);
|
|
@@ -149,6 +151,7 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
registerDeniedKeys(fields);
|
|
152
155
|
return [...new Set(fields)];
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { getEncryptionConfig } = require('../prisma');
|
|
2
|
+
const { Cryptor } = require('../../encrypt/Cryptor');
|
|
3
|
+
const {
|
|
4
|
+
getFieldsToDecryptOnRead,
|
|
5
|
+
getFieldsToEncryptOnWrite,
|
|
6
|
+
loadCustomEncryptionSchema,
|
|
7
|
+
} = require('./encryption-schema-registry');
|
|
8
|
+
const {
|
|
9
|
+
createFieldEncryptionService,
|
|
10
|
+
} = require('./prisma-encryption-extension');
|
|
11
|
+
|
|
12
|
+
let plainMappingEncryption = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The `IntegrationMapping` fields, `mapping` itself or a nested `mapping.*`
|
|
16
|
+
* path, that field-level encryption encrypts on write. An empty result is
|
|
17
|
+
* kept for the life of the process.
|
|
18
|
+
*
|
|
19
|
+
* @returns {string[]} Empty when every mapping path is written as plain JSON
|
|
20
|
+
*/
|
|
21
|
+
function getMappingFieldsEncryptedOnWrite() {
|
|
22
|
+
return mappingEncryption().encryptedOnWrite;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @throws {Error} When field-level encryption still encrypts `mapping`, or a
|
|
27
|
+
* nested `mapping.*` path, on write, naming the opt-out that lifts it
|
|
28
|
+
*/
|
|
29
|
+
function assertMappingWrittenUnencrypted() {
|
|
30
|
+
const fields = getMappingFieldsEncryptedOnWrite();
|
|
31
|
+
if (fields.length === 0) return;
|
|
32
|
+
|
|
33
|
+
const encrypted = fields
|
|
34
|
+
.map((field) => `IntegrationMapping.${field}`)
|
|
35
|
+
.join(', ');
|
|
36
|
+
const optOut = fields.map((field) => `'${field}'`).join(', ');
|
|
37
|
+
throw new Error(
|
|
38
|
+
`queryMappings: field-level encryption still encrypts ${encrypted} on write, so it cannot be queried. Opt out by adding ${optOut} to appDefinition.encryption.disable.IntegrationMapping.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decrypts rows that `queryMappings` read around the Prisma encryption
|
|
44
|
+
* extension, the way reads through the extension do: every
|
|
45
|
+
* `IntegrationMapping` field the schema lists, opted-out paths included, so a
|
|
46
|
+
* path written encrypted before its opt-out comes back plain.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object[]} rows - Rows whose `mapping` is a JSON object
|
|
49
|
+
* @returns {Promise<Object[]>} `rows` itself when encryption is off or the
|
|
50
|
+
* schema lists no field besides `mapping`
|
|
51
|
+
*/
|
|
52
|
+
async function decryptQueriedMappings(rows) {
|
|
53
|
+
const { decryptor } = mappingEncryption();
|
|
54
|
+
if (!decryptor) return rows;
|
|
55
|
+
return decryptor.decryptFieldsInBulk('IntegrationMapping', rows);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mappingEncryption() {
|
|
59
|
+
if (plainMappingEncryption) return plainMappingEncryption;
|
|
60
|
+
|
|
61
|
+
const encryption = currentMappingEncryption();
|
|
62
|
+
if (encryption.encryptedOnWrite.length === 0) {
|
|
63
|
+
plainMappingEncryption = encryption;
|
|
64
|
+
}
|
|
65
|
+
return encryption;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function currentMappingEncryption() {
|
|
69
|
+
const config = getEncryptionConfig();
|
|
70
|
+
if (!config.enabled) return { encryptedOnWrite: [], decryptor: null };
|
|
71
|
+
|
|
72
|
+
loadCustomEncryptionSchema();
|
|
73
|
+
const encryptedOnWrite = getFieldsToEncryptOnWrite(
|
|
74
|
+
'IntegrationMapping'
|
|
75
|
+
).filter((field) => field === 'mapping' || field.startsWith('mapping.'));
|
|
76
|
+
const decryptsBesidesMapping = getFieldsToDecryptOnRead(
|
|
77
|
+
'IntegrationMapping'
|
|
78
|
+
).some((field) => field !== 'mapping');
|
|
79
|
+
const decryptor = decryptsBesidesMapping
|
|
80
|
+
? createFieldEncryptionService(
|
|
81
|
+
new Cryptor({ shouldUseAws: config.method === 'kms' })
|
|
82
|
+
)
|
|
83
|
+
: null;
|
|
84
|
+
return { encryptedOnWrite, decryptor };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Test helper: forget a kept result. */
|
|
88
|
+
function resetMappingEncryptionCheck() {
|
|
89
|
+
plainMappingEncryption = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
assertMappingWrittenUnencrypted,
|
|
94
|
+
decryptQueriedMappings,
|
|
95
|
+
getMappingFieldsEncryptedOnWrite,
|
|
96
|
+
resetMappingEncryptionCheck,
|
|
97
|
+
};
|
|
@@ -17,9 +17,12 @@ class EncryptionLogger {
|
|
|
17
17
|
this.minLevel = this._getMinLevel();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// FRIGG_LOG_LEVEL also takes TRACE and FATAL (ADR-048); this logger has
|
|
21
|
+
// no such levels, so they map to its nearest ones.
|
|
20
22
|
_getMinLevel() {
|
|
21
|
-
const level = process.env.FRIGG_LOG_LEVEL || 'INFO';
|
|
22
|
-
|
|
23
|
+
const level = (process.env.FRIGG_LOG_LEVEL || 'INFO').trim().toUpperCase();
|
|
24
|
+
const mapped = { TRACE: 'DEBUG', FATAL: 'ERROR' }[level] ?? level;
|
|
25
|
+
return LOG_LEVELS[mapped] ?? LOG_LEVELS.INFO;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
_shouldLog(level) {
|
|
@@ -10,6 +10,24 @@ const {
|
|
|
10
10
|
} = require('./encryption-schema-registry');
|
|
11
11
|
const { FieldEncryptionService } = require('./field-encryption-service');
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The FieldEncryptionService the extension runs, with field paths from the
|
|
15
|
+
* encryption schema registry.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('../../encrypt/Cryptor').Cryptor} cryptor
|
|
18
|
+
* @returns {FieldEncryptionService}
|
|
19
|
+
*/
|
|
20
|
+
function createFieldEncryptionService(cryptor) {
|
|
21
|
+
return new FieldEncryptionService({
|
|
22
|
+
cryptor,
|
|
23
|
+
schema: {
|
|
24
|
+
getEncryptedFields,
|
|
25
|
+
getFieldsToEncryptOnWrite,
|
|
26
|
+
getFieldsToDecryptOnRead,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
function createEncryptionExtension({ cryptor, enabled = true }) {
|
|
14
32
|
if (!enabled) {
|
|
15
33
|
return (client) => client;
|
|
@@ -21,14 +39,7 @@ function createEncryptionExtension({ cryptor, enabled = true }) {
|
|
|
21
39
|
);
|
|
22
40
|
}
|
|
23
41
|
|
|
24
|
-
const encryptionService =
|
|
25
|
-
cryptor,
|
|
26
|
-
schema: {
|
|
27
|
-
getEncryptedFields,
|
|
28
|
-
getFieldsToEncryptOnWrite,
|
|
29
|
-
getFieldsToDecryptOnRead,
|
|
30
|
-
},
|
|
31
|
-
});
|
|
42
|
+
const encryptionService = createFieldEncryptionService(cryptor);
|
|
32
43
|
|
|
33
44
|
return {
|
|
34
45
|
name: 'frigg-field-encryption',
|
|
@@ -227,4 +238,4 @@ function createEncryptionExtension({ cryptor, enabled = true }) {
|
|
|
227
238
|
};
|
|
228
239
|
}
|
|
229
240
|
|
|
230
|
-
module.exports = { createEncryptionExtension };
|
|
241
|
+
module.exports = { createEncryptionExtension, createFieldEncryptionService };
|
package/errors/fetch-error.js
CHANGED
|
@@ -1,82 +1,65 @@
|
|
|
1
1
|
const { BaseError } = require('./base-error');
|
|
2
|
-
const {
|
|
3
|
-
|
|
4
|
-
// TODO hide header values
|
|
2
|
+
const { redactUrl, scrubString } = require('../logs/redact');
|
|
5
3
|
|
|
6
4
|
// Parameters names here are based on fetch. See:
|
|
7
5
|
// https://developer.mozilla.org/en-US/docs/Web/API/fetch
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
? init.body instanceof URLSearchParams
|
|
17
|
-
? (() => {
|
|
18
|
-
init.body = init.body.toString();
|
|
19
|
-
return JSON.stringify({ init }, null, 2);
|
|
20
|
-
})()
|
|
21
|
-
: JSON.stringify({ init }, null, 2)
|
|
22
|
-
: '';
|
|
23
|
-
|
|
24
|
-
let responseBodyText = '<response body is unavailable>';
|
|
25
|
-
if (typeof responseBody === 'string') {
|
|
26
|
-
responseBodyText = responseBody;
|
|
27
|
-
} else if (responseBody) {
|
|
28
|
-
responseBodyText = JSON.stringify(responseBody, null, 2);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const responseHeaders = {};
|
|
32
|
-
if (response?.headers) {
|
|
33
|
-
for (const [key, value] of response.headers) {
|
|
34
|
-
responseHeaders[key] = value;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let responseHeaderText = response
|
|
39
|
-
? JSON.stringify({ headers: responseHeaders }, null, 2)
|
|
40
|
-
: '';
|
|
7
|
+
function resourceUrl(resource) {
|
|
8
|
+
if (resource === undefined || resource === null) return '';
|
|
9
|
+
if (typeof resource === 'object' && typeof resource.url === 'string') {
|
|
10
|
+
return redactUrl(resource.url);
|
|
11
|
+
}
|
|
12
|
+
return redactUrl(resource);
|
|
13
|
+
}
|
|
41
14
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
15
|
+
function causeLabel(cause) {
|
|
16
|
+
if (!cause || typeof cause !== 'object') return '';
|
|
17
|
+
const label = cause.code ?? cause.name;
|
|
18
|
+
return label === undefined || label === null ? '' : scrubString(String(label));
|
|
19
|
+
}
|
|
48
20
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
initText,
|
|
58
|
-
stripIndent`
|
|
59
|
-
<<< Response Details <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
|
60
|
-
${response?.status} ${response?.statusText}
|
|
61
|
-
`,
|
|
62
|
-
responseHeaderText,
|
|
63
|
-
responseBodyText,
|
|
64
|
-
stripIndent`
|
|
65
|
-
-----------------------------------------------------
|
|
66
|
-
Stack Trace:
|
|
67
|
-
`,
|
|
68
|
-
];
|
|
21
|
+
function buildMessage({ method, url, response, cause }) {
|
|
22
|
+
const outcome = response
|
|
23
|
+
? response.status === undefined || response.status === null
|
|
24
|
+
? ''
|
|
25
|
+
: String(response.status)
|
|
26
|
+
: causeLabel(cause);
|
|
27
|
+
return [method, url, outcome].filter(Boolean).join(' ');
|
|
28
|
+
}
|
|
69
29
|
|
|
70
|
-
|
|
30
|
+
class FetchError extends BaseError {
|
|
31
|
+
constructor({ resource, init, response, cause, responseBody, body } = {}) {
|
|
32
|
+
const method = String(init?.method ?? 'GET').toUpperCase();
|
|
33
|
+
const url = resourceUrl(resource);
|
|
34
|
+
super(
|
|
35
|
+
buildMessage({ method, url, response, cause }),
|
|
36
|
+
cause ? { cause } : undefined
|
|
37
|
+
);
|
|
71
38
|
|
|
72
|
-
this.response = response;
|
|
73
39
|
this.statusCode = response?.status;
|
|
40
|
+
this.method = method;
|
|
41
|
+
this.url = url;
|
|
42
|
+
Object.defineProperty(this, 'response', {
|
|
43
|
+
value: response ?? null,
|
|
44
|
+
enumerable: false,
|
|
45
|
+
writable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(this, 'body', {
|
|
49
|
+
value: responseBody ?? body,
|
|
50
|
+
enumerable: false,
|
|
51
|
+
writable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
});
|
|
74
54
|
}
|
|
75
55
|
|
|
76
56
|
static async create(options = {}) {
|
|
77
57
|
const { response } = options;
|
|
78
|
-
let responseBody =
|
|
79
|
-
|
|
58
|
+
let responseBody =
|
|
59
|
+
response && !response.bodyUsed && typeof response.text === 'function'
|
|
60
|
+
? await response.text()
|
|
61
|
+
: null;
|
|
62
|
+
if (!responseBody) responseBody = options.responseBody ?? options.body;
|
|
80
63
|
return new FetchError({ ...options, responseBody });
|
|
81
64
|
}
|
|
82
65
|
}
|
|
@@ -2,13 +2,38 @@ 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 { registerDeniedKeys } = require('../logs/denied-keys');
|
|
6
|
+
const {
|
|
7
|
+
extractCredentialFieldsFromModules,
|
|
8
|
+
} = require('../database/encryption/encryption-schema-registry');
|
|
9
|
+
const {
|
|
10
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
11
|
+
} = require('../integrations/utils/map-integration-dto');
|
|
12
|
+
|
|
13
|
+
// Handlers without a database never load the encryption registry, so the
|
|
14
|
+
// credential leaf keys are registered for redaction here too.
|
|
15
|
+
function registerCredentialLogKeys(appDefinition, integrations) {
|
|
16
|
+
try {
|
|
17
|
+
extractCredentialFieldsFromModules(
|
|
18
|
+
getModulesDefinitionFromIntegrationClasses(integrations)
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
// An odd integration shape must not stop the app from loading.
|
|
22
|
+
}
|
|
23
|
+
const schema = appDefinition.encryption?.schema;
|
|
24
|
+
if (schema && typeof schema === 'object') {
|
|
25
|
+
for (const config of Object.values(schema)) {
|
|
26
|
+
registerDeniedKeys(config?.fields);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
5
30
|
|
|
6
31
|
/**
|
|
7
32
|
* Loads the App definition from the nearest backend package
|
|
8
33
|
* @function loadAppDefinition
|
|
9
34
|
* @description Searches for the nearest backend package.json, loads the corresponding index.js file,
|
|
10
35
|
* and extracts the application definition containing integrations and user configuration.
|
|
11
|
-
* @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object}} An object containing the application definition.
|
|
36
|
+
* @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null}} An object containing the application definition.
|
|
12
37
|
* @throws {Error} Throws error if backend package.json cannot be found.
|
|
13
38
|
* @throws {Error} Throws error if index.js file cannot be found in the backend directory.
|
|
14
39
|
* @example
|
|
@@ -36,8 +61,11 @@ function loadAppDefinition() {
|
|
|
36
61
|
adminScripts = [],
|
|
37
62
|
reports = [],
|
|
38
63
|
admin = {},
|
|
64
|
+
logging = null,
|
|
39
65
|
} = appDefinition;
|
|
40
66
|
|
|
67
|
+
registerCredentialLogKeys(appDefinition, integrations);
|
|
68
|
+
|
|
41
69
|
// Degrade consistently: an invalid telemetry block must never take down a
|
|
42
70
|
// router bundle that loads the app definition at module scope (telemetry is
|
|
43
71
|
// never allowed to break a handler). The singletons apply the same
|
|
@@ -59,7 +87,15 @@ function loadAppDefinition() {
|
|
|
59
87
|
};
|
|
60
88
|
}
|
|
61
89
|
|
|
62
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
integrations,
|
|
92
|
+
userConfig,
|
|
93
|
+
adminScripts,
|
|
94
|
+
reports,
|
|
95
|
+
admin,
|
|
96
|
+
telemetry,
|
|
97
|
+
logging,
|
|
98
|
+
};
|
|
63
99
|
}
|
|
64
100
|
|
|
65
101
|
module.exports = {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
const { createHandler
|
|
1
|
+
const { createHandler } = require('@friggframework/core');
|
|
2
|
+
const { getLogger } = require('../logs');
|
|
3
|
+
const { summarizeExpressRequest } = require('../logs/summarize-event');
|
|
2
4
|
const express = require('express');
|
|
3
5
|
const bodyParser = require('body-parser');
|
|
4
6
|
const cors = require('cors');
|
|
5
7
|
const Boom = require('@hapi/boom');
|
|
6
8
|
const serverlessHttp = require('serverless-http');
|
|
7
9
|
|
|
10
|
+
const log = getLogger('frigg.http');
|
|
11
|
+
|
|
8
12
|
const createApp = (applyMiddleware) => {
|
|
9
13
|
const app = express();
|
|
10
14
|
|
|
@@ -21,7 +25,7 @@ const createApp = (applyMiddleware) => {
|
|
|
21
25
|
|
|
22
26
|
if (applyMiddleware) applyMiddleware(app);
|
|
23
27
|
|
|
24
|
-
//
|
|
28
|
+
// The express boundary: send the error response and log it one time.
|
|
25
29
|
app.use((err, req, res, next) => {
|
|
26
30
|
const boomError = err.isBoom ? err : Boom.boomify(err);
|
|
27
31
|
const {
|
|
@@ -29,10 +33,20 @@ const createApp = (applyMiddleware) => {
|
|
|
29
33
|
} = boomError;
|
|
30
34
|
|
|
31
35
|
if (statusCode >= 500) {
|
|
32
|
-
|
|
36
|
+
log.error('Request failed', {
|
|
37
|
+
eventName: 'frigg.http.request_failed',
|
|
38
|
+
statusCode,
|
|
39
|
+
invocation: summarizeExpressRequest(req),
|
|
40
|
+
error: boomError,
|
|
41
|
+
});
|
|
33
42
|
res.status(statusCode).json({ error: 'Internal Server Error' });
|
|
34
43
|
} else {
|
|
35
|
-
|
|
44
|
+
// A client error needs no stack; the logger scrubs the reason.
|
|
45
|
+
log.warn('Request rejected', {
|
|
46
|
+
eventName: 'frigg.http.request_rejected',
|
|
47
|
+
statusCode,
|
|
48
|
+
reason: boomError.message,
|
|
49
|
+
});
|
|
36
50
|
res.status(statusCode).json({ error: err.message });
|
|
37
51
|
}
|
|
38
52
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { Router } = require('express');
|
|
2
|
+
const { runInContext, LOGGER_SCOPE_KEY } = require('../logs/context');
|
|
2
3
|
const { Worker } = require('@friggframework/core');
|
|
3
4
|
const {
|
|
4
5
|
IntegrationEventDispatcher,
|
|
@@ -19,6 +20,7 @@ const {
|
|
|
19
20
|
const {
|
|
20
21
|
getModulesDefinitionFromIntegrationClasses,
|
|
21
22
|
} = require('../integrations/utils/map-integration-dto');
|
|
23
|
+
const { processNotFound } = require('../integrations/use-cases/process-errors');
|
|
22
24
|
|
|
23
25
|
const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
24
26
|
const router = Router();
|
|
@@ -37,12 +39,11 @@ const loadRouterFromObject = (IntegrationClass, routerObject) => {
|
|
|
37
39
|
const dispatcher = new IntegrationEventDispatcher(
|
|
38
40
|
integrationInstance
|
|
39
41
|
);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
req,
|
|
43
|
-
res,
|
|
44
|
-
|
|
45
|
-
});
|
|
42
|
+
// Logs only: a route :integrationId is unauthenticated input.
|
|
43
|
+
const result = await runInContext(
|
|
44
|
+
{ [LOGGER_SCOPE_KEY]: { integrationId: req.params?.integrationId } },
|
|
45
|
+
() => dispatcher.dispatchHttp({ event, req, res, next })
|
|
46
|
+
);
|
|
46
47
|
res.json(result);
|
|
47
48
|
} catch (error) {
|
|
48
49
|
next(error);
|
|
@@ -149,7 +150,7 @@ const loadIntegrationForProcess = async (processId, integrationClass) => {
|
|
|
149
150
|
const process = await processRepository.findById(processId);
|
|
150
151
|
|
|
151
152
|
if (!process) {
|
|
152
|
-
throw
|
|
153
|
+
throw processNotFound(`Process not found: ${processId}`);
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
const instance = await getIntegrationInstance.execute(
|
|
@@ -164,7 +165,7 @@ const createQueueWorker = (integrationClass) => {
|
|
|
164
165
|
const integrationName = integrationClass.Definition.name;
|
|
165
166
|
|
|
166
167
|
class QueueWorker extends Worker {
|
|
167
|
-
async _run(params, context) {
|
|
168
|
+
async _run(params, context, delivery) {
|
|
168
169
|
const logCtx = {
|
|
169
170
|
integration: integrationName,
|
|
170
171
|
event: params.event,
|
|
@@ -254,6 +255,7 @@ const createQueueWorker = (integrationClass) => {
|
|
|
254
255
|
event: params.event,
|
|
255
256
|
data: params.data,
|
|
256
257
|
context: context,
|
|
258
|
+
delivery,
|
|
257
259
|
});
|
|
258
260
|
console.log(
|
|
259
261
|
`[QueueWorker] ${params.event} dispatched ok`,
|
|
@@ -273,11 +275,6 @@ const createQueueWorker = (integrationClass) => {
|
|
|
273
275
|
return;
|
|
274
276
|
}
|
|
275
277
|
|
|
276
|
-
console.error(
|
|
277
|
-
`Error in ${params.event} for ${integrationName}:`,
|
|
278
|
-
error
|
|
279
|
-
);
|
|
280
|
-
|
|
281
278
|
// 4xx HTTP errors are permanent — the requester already
|
|
282
279
|
// attempted token refresh (401) and backoff (429/5xx).
|
|
283
280
|
// By the time a 4xx reaches here, retrying won't help.
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
|
|
25
25
|
const { execFile } = require('child_process');
|
|
26
26
|
const path = require('path');
|
|
27
|
+
const { getLogger } = require('../logs');
|
|
28
|
+
|
|
29
|
+
const log = getLogger('frigg.database.migration');
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Execute Prisma migration command
|
|
@@ -153,7 +156,13 @@ exports.handler = async (event, context) => {
|
|
|
153
156
|
console.log('='.repeat(60));
|
|
154
157
|
console.log('Database Migration Handler');
|
|
155
158
|
console.log('='.repeat(60));
|
|
156
|
-
|
|
159
|
+
log.info('Database migration invoked', {
|
|
160
|
+
eventName: 'frigg.database.migration.invoked',
|
|
161
|
+
migrationId: event.migrationId,
|
|
162
|
+
dbType: process.env.DB_TYPE,
|
|
163
|
+
targetStage: process.env.STAGE,
|
|
164
|
+
action: event.command || 'deploy',
|
|
165
|
+
});
|
|
157
166
|
console.log('Context:', JSON.stringify({
|
|
158
167
|
functionName: context.functionName,
|
|
159
168
|
functionVersion: context.functionVersion,
|
|
@@ -18,9 +18,9 @@ class IntegrationEventDispatcher {
|
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
async dispatchJob({ event, data, context }) {
|
|
21
|
+
async dispatchJob({ event, data, context, delivery }) {
|
|
22
22
|
return this._dispatch(event, (instance, handler) =>
|
|
23
|
-
handler.call(instance, { data, context })
|
|
23
|
+
handler.call(instance, { data, context, delivery })
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { createAppHandler } = require('./../app-handler-helpers');
|
|
2
2
|
const { loadAppDefinition } = require('../app-definition-loader');
|
|
3
3
|
const { Router } = require('express');
|
|
4
|
+
const { runInContext, LOGGER_SCOPE_KEY } = require('../../logs/context');
|
|
4
5
|
const {
|
|
5
6
|
IntegrationEventDispatcher,
|
|
6
7
|
} = require('../integration-event-dispatcher');
|
|
@@ -55,12 +56,18 @@ for (const IntegrationClass of integrationClasses) {
|
|
|
55
56
|
const dispatcher = new IntegrationEventDispatcher(
|
|
56
57
|
integrationInstance
|
|
57
58
|
);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
// Logs only: the id comes from an unauthenticated URL, so it must
|
|
60
|
+
// not reach the telemetry context that drives usage attribution.
|
|
61
|
+
await runInContext(
|
|
62
|
+
{ [LOGGER_SCOPE_KEY]: { integrationId: req.params.integrationId } },
|
|
63
|
+
() =>
|
|
64
|
+
dispatcher.dispatchHttp({
|
|
65
|
+
event: 'WEBHOOK_RECEIVED',
|
|
66
|
+
req,
|
|
67
|
+
res,
|
|
68
|
+
next,
|
|
69
|
+
})
|
|
70
|
+
);
|
|
64
71
|
} catch (error) {
|
|
65
72
|
next(error);
|
|
66
73
|
}
|