@friggframework/core 2.0.0--canary.654.e9530b8.0 → 2.0.0--canary.654.70623e2.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/core/CLAUDE.md +3 -2
- package/core/invocation-scope.js +14 -10
- package/database/encryption/encryption-schema-registry.js +3 -3
- package/database/encryption/logger.js +5 -2
- package/handlers/app-definition-loader.js +3 -18
- package/handlers/backend-utils.js +8 -7
- package/handlers/routers/integration-webhook-routers.js +13 -6
- package/handlers/routers/websocket.js +1 -1
- package/logs/context.js +9 -2
- package/logs/denied-keys.js +21 -0
- package/logs/record.js +11 -1
- package/logs/summarize-event.js +4 -3
- package/package.json +5 -5
- package/user/repositories/user-repository-documentdb.js +2 -2
package/core/CLAUDE.md
CHANGED
|
@@ -158,8 +158,9 @@ class MyIntegration extends Delegate {
|
|
|
158
158
|
|
|
159
159
|
5. **Flush** (in `runInvocationScope`'s `finally`):
|
|
160
160
|
```javascript
|
|
161
|
-
//
|
|
162
|
-
//
|
|
161
|
+
// Telemetry + log sinks under one deadline, fixed at flush start:
|
|
162
|
+
// min(flushTimeoutMs, remaining time - 50 ms). The usage rollup runs in
|
|
163
|
+
// parallel with no bound of its own.
|
|
163
164
|
```
|
|
164
165
|
|
|
165
166
|
### SQS Job Processing Lifecycle
|
package/core/invocation-scope.js
CHANGED
|
@@ -128,9 +128,19 @@ async function flushInvocation({
|
|
|
128
128
|
flushTimeoutMs,
|
|
129
129
|
context,
|
|
130
130
|
}) {
|
|
131
|
-
// ADR-048 §11:
|
|
132
|
-
|
|
131
|
+
// ADR-048 §11: the deadline is fixed at flush start. The usage rollup
|
|
132
|
+
// emits no telemetry, so it runs in parallel with no bound of its own.
|
|
133
|
+
const deadline = Math.min(
|
|
134
|
+
flushTimeoutMs,
|
|
135
|
+
remainingTimeMs(context) - FLUSH_MARGIN_MS
|
|
136
|
+
);
|
|
137
|
+
await Promise.all([
|
|
138
|
+
flushUsageRollup(usageRollup, eventSummary, shouldUseDatabase),
|
|
139
|
+
flushBounded(telemetry, deadline),
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
133
142
|
|
|
143
|
+
function flushBounded(telemetry, deadline) {
|
|
134
144
|
const flushes = [];
|
|
135
145
|
if (isTelemetryEnabled(telemetry)) {
|
|
136
146
|
flushes.push((signal) => flushTelemetry(telemetry, { signal }));
|
|
@@ -138,14 +148,8 @@ async function flushInvocation({
|
|
|
138
148
|
if (hasFlushableSinks()) {
|
|
139
149
|
flushes.push((signal) => flushSinks({ signal }));
|
|
140
150
|
}
|
|
141
|
-
if (!flushes.length) return;
|
|
142
|
-
|
|
143
|
-
const deadline = Math.min(
|
|
144
|
-
flushTimeoutMs,
|
|
145
|
-
remainingTimeMs(context) - FLUSH_MARGIN_MS
|
|
146
|
-
);
|
|
147
|
-
if (!(deadline > 0)) return;
|
|
148
|
-
await withDeadline(deadline, (signal) =>
|
|
151
|
+
if (!flushes.length || !(deadline > 0)) return undefined;
|
|
152
|
+
return withDeadline(deadline, (signal) =>
|
|
149
153
|
Promise.allSettled(flushes.map((flush) => flush(signal)))
|
|
150
154
|
);
|
|
151
155
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const { logger } = require('./logger');
|
|
12
|
-
const {
|
|
12
|
+
const { registerDeniedKeys } = require('../../logs/denied-keys');
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Core encryption schema (immutable - cannot be overridden by custom schemas)
|
|
@@ -121,7 +121,7 @@ function registerCustomSchema(schema) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
customSchema = { ...schema };
|
|
124
|
-
|
|
124
|
+
registerDeniedKeys(Object.values(schema).flatMap((config) => config.fields));
|
|
125
125
|
logger.info(
|
|
126
126
|
`Registered custom encryption schema for models: ${Object.keys(customSchema).join(', ')}`
|
|
127
127
|
);
|
|
@@ -151,7 +151,7 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
registerDeniedKeys(fields);
|
|
155
155
|
return [...new Set(fields)];
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -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) {
|
|
@@ -2,8 +2,7 @@ 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 {
|
|
6
|
-
const { getLogger } = require('../logs');
|
|
5
|
+
const { registerDeniedKeys } = require('../logs/denied-keys');
|
|
7
6
|
const {
|
|
8
7
|
extractCredentialFieldsFromModules,
|
|
9
8
|
} = require('../database/encryption/encryption-schema-registry');
|
|
@@ -13,32 +12,18 @@ const {
|
|
|
13
12
|
|
|
14
13
|
// Handlers without a database never load the encryption registry, so the
|
|
15
14
|
// credential leaf keys are registered for redaction here too.
|
|
16
|
-
const warnedIgnoredKeys = new Set();
|
|
17
|
-
|
|
18
|
-
function warnIgnoredKeys(ignored) {
|
|
19
|
-
for (const key of ignored) {
|
|
20
|
-
if (warnedIgnoredKeys.has(key)) continue;
|
|
21
|
-
warnedIgnoredKeys.add(key);
|
|
22
|
-
getLogger('frigg.logger').warn(
|
|
23
|
-
'Credential field name is a record field; it stays visible in logs',
|
|
24
|
-
{ eventName: 'frigg.logger.denied_key_ignored', key }
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
15
|
function registerCredentialLogKeys(appDefinition, integrations) {
|
|
30
16
|
try {
|
|
31
|
-
|
|
17
|
+
extractCredentialFieldsFromModules(
|
|
32
18
|
getModulesDefinitionFromIntegrationClasses(integrations)
|
|
33
19
|
);
|
|
34
|
-
warnIgnoredKeys(addDeniedKeys(fields));
|
|
35
20
|
} catch {
|
|
36
21
|
// An odd integration shape must not stop the app from loading.
|
|
37
22
|
}
|
|
38
23
|
const schema = appDefinition.encryption?.schema;
|
|
39
24
|
if (schema && typeof schema === 'object') {
|
|
40
25
|
for (const config of Object.values(schema)) {
|
|
41
|
-
|
|
26
|
+
registerDeniedKeys(config?.fields);
|
|
42
27
|
}
|
|
43
28
|
}
|
|
44
29
|
}
|
|
@@ -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(
|
|
@@ -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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { createHandler } = require('@friggframework/core');
|
|
2
|
-
const { createWebsocketConnectionRepository } = require('../../
|
|
2
|
+
const { createWebsocketConnectionRepository } = require('../../websocket/repositories/websocket-connection-repository-factory');
|
|
3
3
|
const { getLogger } = require('../../logs');
|
|
4
4
|
|
|
5
5
|
const log = getLogger('frigg.websocket');
|
package/logs/context.js
CHANGED
|
@@ -77,8 +77,15 @@ function getLoggerScope() {
|
|
|
77
77
|
for (const key of INTEGRATION_KEYS) {
|
|
78
78
|
if (store[key] !== undefined) scope[key] = store[key];
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// A set (hydrated) id beats a logger-only one, e.g. from a URL or a
|
|
81
|
+
// message body; a null id leaves the logger-only value in place.
|
|
82
|
+
const loggerKeys = store[LOGGER_SCOPE_KEY];
|
|
83
|
+
if (isPlainObject(loggerKeys)) {
|
|
84
|
+
for (const [key, value] of Object.entries(loggerKeys)) {
|
|
85
|
+
if (scope[key] === undefined || scope[key] === null) {
|
|
86
|
+
scope[key] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
82
89
|
}
|
|
83
90
|
scope = Object.freeze(scope);
|
|
84
91
|
scopeByStore.set(store, scope);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { addDeniedKeys } = require('./redact');
|
|
2
|
+
const { getLogger } = require('./logger');
|
|
3
|
+
|
|
4
|
+
const warnedKeys = new Set();
|
|
5
|
+
|
|
6
|
+
// addDeniedKeys plus one WARN per record-contract key it had to skip, so a
|
|
7
|
+
// credential field named like a record field is visible wherever it is set.
|
|
8
|
+
function registerDeniedKeys(keys) {
|
|
9
|
+
const ignored = addDeniedKeys(keys);
|
|
10
|
+
for (const key of ignored) {
|
|
11
|
+
if (warnedKeys.has(key)) continue;
|
|
12
|
+
warnedKeys.add(key);
|
|
13
|
+
getLogger('frigg.logger').warn(
|
|
14
|
+
'Credential field name is a record field; it stays visible in logs',
|
|
15
|
+
{ eventName: 'frigg.logger.denied_key_ignored', key }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return ignored;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { registerDeniedKeys };
|
package/logs/record.js
CHANGED
|
@@ -84,6 +84,15 @@ function traceFields(spanContext) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function sameValue(a, b) {
|
|
88
|
+
if (a === b) return true;
|
|
89
|
+
return (
|
|
90
|
+
typeof a === 'object' &&
|
|
91
|
+
typeof b === 'object' &&
|
|
92
|
+
JSON.stringify(a) === JSON.stringify(b)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
function byteLength(record) {
|
|
88
97
|
return Buffer.byteLength(JSON.stringify(record));
|
|
89
98
|
}
|
|
@@ -182,7 +191,8 @@ function buildRecord({
|
|
|
182
191
|
isPlainObject(value)
|
|
183
192
|
) {
|
|
184
193
|
record[key] = extendInvocation(record[key], value);
|
|
185
|
-
} else {
|
|
194
|
+
} else if (!sameValue(record[key], value)) {
|
|
195
|
+
// A repeat of the winning value is no conflict.
|
|
186
196
|
dropped.add(key);
|
|
187
197
|
}
|
|
188
198
|
continue;
|
package/logs/summarize-event.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
const { redactUrl } = require('./redact');
|
|
2
2
|
|
|
3
3
|
// Best-effort extraction of the logical event/processId/integrationId from a
|
|
4
|
-
// JSON message body. Used only for log
|
|
4
|
+
// JSON message body: `data.*` first, then the top level. Used only for log
|
|
5
|
+
// correlation. Never throws.
|
|
5
6
|
function summarizeMessageBody(bodyStr) {
|
|
6
7
|
try {
|
|
7
8
|
const parsed = JSON.parse(bodyStr);
|
|
8
9
|
return {
|
|
9
10
|
event: parsed?.event,
|
|
10
|
-
processId: parsed?.data?.processId,
|
|
11
|
-
integrationId: parsed?.data?.integrationId,
|
|
11
|
+
processId: parsed?.data?.processId ?? parsed?.processId,
|
|
12
|
+
integrationId: parsed?.data?.integrationId ?? parsed?.integrationId,
|
|
12
13
|
};
|
|
13
14
|
} catch {
|
|
14
15
|
return {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.654.
|
|
4
|
+
"version": "2.0.0--canary.654.70623e2.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@friggframework/eslint-config": "2.0.0--canary.654.
|
|
51
|
-
"@friggframework/prettier-config": "2.0.0--canary.654.
|
|
52
|
-
"@friggframework/test": "2.0.0--canary.654.
|
|
50
|
+
"@friggframework/eslint-config": "2.0.0--canary.654.70623e2.0",
|
|
51
|
+
"@friggframework/prettier-config": "2.0.0--canary.654.70623e2.0",
|
|
52
|
+
"@friggframework/test": "2.0.0--canary.654.70623e2.0",
|
|
53
53
|
"@prisma/client": "^6.19.3",
|
|
54
54
|
"@types/lodash": "4.17.15",
|
|
55
55
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "70623e2fbe9d93afb370528085ea543c784775c4"
|
|
93
93
|
}
|
|
@@ -274,7 +274,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
274
274
|
// Defensive check: verify document was found after update
|
|
275
275
|
if (!updated) {
|
|
276
276
|
throw new Error(
|
|
277
|
-
|
|
277
|
+
`Failed to update individual user: Document not found after update (userId ${userId}). ` +
|
|
278
278
|
'This indicates a database consistency issue.'
|
|
279
279
|
);
|
|
280
280
|
}
|
|
@@ -308,7 +308,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
308
308
|
|
|
309
309
|
if (!updated) {
|
|
310
310
|
throw new Error(
|
|
311
|
-
|
|
311
|
+
`Failed to update organization user: Document not found after update (userId ${userId}). ` +
|
|
312
312
|
'This indicates a database consistency issue.'
|
|
313
313
|
);
|
|
314
314
|
}
|