@friggframework/core 2.0.0--canary.654.be44e66.0 → 2.0.0--canary.654.42e42b2.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/Worker.js +10 -5
- package/core/create-handler.js +2 -0
- package/handlers/app-definition-loader.js +17 -2
- package/handlers/app-handler-helpers.js +2 -1
- package/logs/__fixtures__/vectors.js +17 -0
- package/logs/debug-shims.js +8 -2
- package/logs/redact.js +39 -7
- package/logs/summarize-event.js +30 -4
- package/modules/module.js +1 -1
- package/package.json +5 -5
package/core/Worker.js
CHANGED
|
@@ -3,8 +3,10 @@ const _ = require('lodash');
|
|
|
3
3
|
const { RequiredPropertyError } = require('../errors');
|
|
4
4
|
const { get } = require('../assertions');
|
|
5
5
|
const { runMessageScope } = require('./invocation-scope');
|
|
6
|
+
const { getLogger } = require('../logs');
|
|
6
7
|
|
|
7
8
|
const sqs = new SQSClient({ region: process.env.AWS_REGION });
|
|
9
|
+
const log = getLogger('frigg.worker');
|
|
8
10
|
|
|
9
11
|
class Worker {
|
|
10
12
|
async getQueueURL(params) {
|
|
@@ -55,15 +57,18 @@ class Worker {
|
|
|
55
57
|
// Treat as success so SQS deletes it from the queue.
|
|
56
58
|
// Logged explicitly — silent discards made prod debugging
|
|
57
59
|
// extremely hard; keep this visible.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
event: parsedEvent,
|
|
61
|
-
reason: error.message,
|
|
60
|
+
log.error('Record halted (discarded, no retry)', {
|
|
61
|
+
eventName: 'frigg.worker.record_halted',
|
|
62
62
|
statusCode: error.statusCode,
|
|
63
|
+
error,
|
|
63
64
|
});
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
// The message goes back to SQS, so WARN (ADR-048 §4).
|
|
68
|
+
log.warn('Record failed, returned for retry', {
|
|
69
|
+
eventName: 'frigg.worker.record_failed',
|
|
70
|
+
error,
|
|
71
|
+
});
|
|
67
72
|
batchItemFailures.push({ itemIdentifier: record.messageId });
|
|
68
73
|
}
|
|
69
74
|
});
|
package/core/create-handler.js
CHANGED
|
@@ -6,6 +6,7 @@ const { getLogger, toSanitizedSurrogate } = require('../logs');
|
|
|
6
6
|
const {
|
|
7
7
|
summarizeLambdaEvent,
|
|
8
8
|
toScopeInvocation,
|
|
9
|
+
toRequestDetails,
|
|
9
10
|
} = require('../logs/summarize-event');
|
|
10
11
|
const {
|
|
11
12
|
runInvocationScope,
|
|
@@ -64,6 +65,7 @@ const createHandler = (optionByName = {}) => {
|
|
|
64
65
|
try {
|
|
65
66
|
log.info('Handler invoked', {
|
|
66
67
|
eventName: 'frigg.handler.invoked',
|
|
68
|
+
...toRequestDetails(eventSummary),
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
@@ -3,6 +3,7 @@ const path = require('node:path');
|
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
|
|
5
5
|
const { addDeniedKeys } = require('../logs/redact');
|
|
6
|
+
const { getLogger } = require('../logs');
|
|
6
7
|
const {
|
|
7
8
|
extractCredentialFieldsFromModules,
|
|
8
9
|
} = require('../database/encryption/encryption-schema-registry');
|
|
@@ -12,18 +13,32 @@ const {
|
|
|
12
13
|
|
|
13
14
|
// Handlers without a database never load the encryption registry, so the
|
|
14
15
|
// 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
|
+
|
|
15
29
|
function registerCredentialLogKeys(appDefinition, integrations) {
|
|
16
30
|
try {
|
|
17
|
-
extractCredentialFieldsFromModules(
|
|
31
|
+
const fields = extractCredentialFieldsFromModules(
|
|
18
32
|
getModulesDefinitionFromIntegrationClasses(integrations)
|
|
19
33
|
);
|
|
34
|
+
warnIgnoredKeys(addDeniedKeys(fields));
|
|
20
35
|
} catch {
|
|
21
36
|
// An odd integration shape must not stop the app from loading.
|
|
22
37
|
}
|
|
23
38
|
const schema = appDefinition.encryption?.schema;
|
|
24
39
|
if (schema && typeof schema === 'object') {
|
|
25
40
|
for (const config of Object.values(schema)) {
|
|
26
|
-
addDeniedKeys(config?.fields);
|
|
41
|
+
warnIgnoredKeys(addDeniedKeys(config?.fields));
|
|
27
42
|
}
|
|
28
43
|
}
|
|
29
44
|
}
|
|
@@ -39,10 +39,11 @@ const createApp = (applyMiddleware) => {
|
|
|
39
39
|
});
|
|
40
40
|
res.status(statusCode).json({ error: 'Internal Server Error' });
|
|
41
41
|
} else {
|
|
42
|
+
// A client error needs no stack; the logger scrubs the reason.
|
|
42
43
|
log.warn('Request rejected', {
|
|
43
44
|
eventName: 'frigg.http.request_rejected',
|
|
44
45
|
statusCode,
|
|
45
|
-
|
|
46
|
+
reason: boomError.message,
|
|
46
47
|
});
|
|
47
48
|
res.status(statusCode).json({ error: err.message });
|
|
48
49
|
}
|
|
@@ -7,6 +7,16 @@ const { summarizeLambdaEvent } = require('../summarize-event');
|
|
|
7
7
|
const secretUrl = `https://api.example.com/v1/items?api_key=${S.apiKeyQuery}`;
|
|
8
8
|
const SIGNATURE_VALUE = 'fakeSgnValue2Gh9Tc5Wp7LmQx3';
|
|
9
9
|
|
|
10
|
+
// Built by concatenation so push protection does not match the prefixes.
|
|
11
|
+
const TOKEN_TAIL = 'ZqPr3fixTail8Hn2Kd8Ws1Yc6';
|
|
12
|
+
const PREFIXED_TOKENS = [
|
|
13
|
+
'sk_' + 'live_' + TOKEN_TAIL,
|
|
14
|
+
'whsec' + '_' + TOKEN_TAIL,
|
|
15
|
+
'xox' + 'b-' + TOKEN_TAIL,
|
|
16
|
+
'gh' + 'p_' + TOKEN_TAIL,
|
|
17
|
+
'AK' + 'IA' + 'ZQPR3FIXTAIL8HN2',
|
|
18
|
+
];
|
|
19
|
+
|
|
10
20
|
function errorWithMessage(message, name = 'Error') {
|
|
11
21
|
const error = new Error(message);
|
|
12
22
|
error.name = name;
|
|
@@ -188,6 +198,13 @@ function vectors() {
|
|
|
188
198
|
fields: () => ({ note: `blob ${S.base64Run} end` }),
|
|
189
199
|
secrets: [S.base64Run],
|
|
190
200
|
},
|
|
201
|
+
{
|
|
202
|
+
name: 'provider token prefixes',
|
|
203
|
+
level: 'warn',
|
|
204
|
+
message: () => `provider rejected ${PREFIXED_TOKENS.join(' and ')}`,
|
|
205
|
+
fields: () => ({ note: PREFIXED_TOKENS.join(',') }),
|
|
206
|
+
secrets: PREFIXED_TOKENS,
|
|
207
|
+
},
|
|
191
208
|
{
|
|
192
209
|
name: '40-hex token',
|
|
193
210
|
level: 'error',
|
package/logs/debug-shims.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
const util = require('node:util');
|
|
2
2
|
const { getLogger } = require('./logger');
|
|
3
3
|
const { getContext } = require('./context');
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
summarizeLambdaEvent,
|
|
6
|
+
toScopeInvocation,
|
|
7
|
+
toRequestDetails,
|
|
8
|
+
} = require('./summarize-event');
|
|
5
9
|
|
|
6
10
|
// Deprecated. Kept for one major version; use getLogger or this.logger.
|
|
7
11
|
const LEGACY_LOGGER_NAME = 'frigg.legacy';
|
|
@@ -28,8 +32,10 @@ function initDebugLog(...initMessages) {
|
|
|
28
32
|
const log = legacy();
|
|
29
33
|
if (!log.isLevelEnabled('DEBUG')) return;
|
|
30
34
|
const event = initMessages.find((m) => m && typeof m === 'object');
|
|
35
|
+
const summary = summarizeLambdaEvent(event);
|
|
31
36
|
log.debug('Debug log initialized', {
|
|
32
|
-
invocation: toScopeInvocation(
|
|
37
|
+
invocation: toScopeInvocation(summary),
|
|
38
|
+
...toRequestDetails(summary),
|
|
33
39
|
});
|
|
34
40
|
}
|
|
35
41
|
|
package/logs/redact.js
CHANGED
|
@@ -14,8 +14,8 @@ const DENIED_SUFFIXES = [
|
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
// Core encryption registry leaves and exact names the suffix rule does not
|
|
17
|
-
// cover. `header` is Node's raw `_header` request text. `domain`
|
|
18
|
-
//
|
|
17
|
+
// cover. `header` is Node's raw `_header` request text. `domain` is not
|
|
18
|
+
// seeded (too common); a module can still register it as a credential field.
|
|
19
19
|
const deniedKeys = new Set([
|
|
20
20
|
'hashword',
|
|
21
21
|
'cookies',
|
|
@@ -25,6 +25,29 @@ const deniedKeys = new Set([
|
|
|
25
25
|
'header',
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
+
// Record-contract fields. A credential field with one of these names must not
|
|
29
|
+
// blank the field in every record.
|
|
30
|
+
const PROTECTED_KEYS = new Set(
|
|
31
|
+
[
|
|
32
|
+
'integrationId',
|
|
33
|
+
'integrationType',
|
|
34
|
+
'userId',
|
|
35
|
+
'version',
|
|
36
|
+
'entityId',
|
|
37
|
+
'credentialId',
|
|
38
|
+
'requestId',
|
|
39
|
+
'messageId',
|
|
40
|
+
'processId',
|
|
41
|
+
'integrationEvent',
|
|
42
|
+
'eventName',
|
|
43
|
+
'handlerName',
|
|
44
|
+
'method',
|
|
45
|
+
'route',
|
|
46
|
+
'routeKey',
|
|
47
|
+
'statusCode',
|
|
48
|
+
].map((key) => key.toLowerCase())
|
|
49
|
+
);
|
|
50
|
+
|
|
28
51
|
const PAIR_KEYS = new Set(['code', 'codeverifier']);
|
|
29
52
|
const DIGEST_KEY = /(sha(1|256)|hash|digest|checksum)$/;
|
|
30
53
|
|
|
@@ -34,6 +57,8 @@ const URL_IN_TEXT = /\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"'<>`]+/g;
|
|
|
34
57
|
const URL_TRAILING_PUNCT = /[.,;:!?)\]}]+$/;
|
|
35
58
|
const ABSOLUTE_URL = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
|
|
36
59
|
const ROOT_SLASH = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*\//;
|
|
60
|
+
const PREFIXED_TOKEN =
|
|
61
|
+
/(?<![A-Za-z0-9_-])(?:(?:sk|rk)_(?:live|test)_|whsec_|xox[abprs]-|gh[opsu]_|github_pat_|shp(?:at|ss)_|glpat-|npm_)[A-Za-z0-9_-]{16,}|\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g;
|
|
37
62
|
const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
|
|
38
63
|
const AUTH_SCHEME = /\b(Bearer|bearer|BEARER|Basic|basic|BASIC|Token|token|TOKEN)(\s+)([A-Za-z0-9._~+/=-]{8,})/g;
|
|
39
64
|
const AUTH_SCHEME_WORD = /^(bearer|basic|token)$/i;
|
|
@@ -62,7 +87,7 @@ function normalizeKey(key) {
|
|
|
62
87
|
|
|
63
88
|
function isDeniedKey(key) {
|
|
64
89
|
const normalized = normalizeKey(key);
|
|
65
|
-
if (!normalized) return false;
|
|
90
|
+
if (!normalized || PROTECTED_KEYS.has(normalized)) return false;
|
|
66
91
|
if (deniedKeys.has(normalized)) return true;
|
|
67
92
|
return DENIED_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
|
|
68
93
|
}
|
|
@@ -80,13 +105,19 @@ function isPairKey(key, { withState = false } = {}) {
|
|
|
80
105
|
);
|
|
81
106
|
}
|
|
82
107
|
|
|
108
|
+
// Returns the leaves it skipped as record-contract keys, so a caller can warn.
|
|
83
109
|
function addDeniedKeys(keys) {
|
|
84
|
-
|
|
110
|
+
const ignored = [];
|
|
111
|
+
if (!Array.isArray(keys)) return ignored;
|
|
85
112
|
for (const key of keys) {
|
|
86
113
|
if (typeof key !== 'string') continue;
|
|
87
|
-
const
|
|
88
|
-
|
|
114
|
+
const rawLeaf = key.split('.').pop();
|
|
115
|
+
const leaf = normalizeKey(rawLeaf);
|
|
116
|
+
if (!leaf) continue;
|
|
117
|
+
if (PROTECTED_KEYS.has(leaf)) ignored.push(rawLeaf);
|
|
118
|
+
else deniedKeys.add(leaf);
|
|
89
119
|
}
|
|
120
|
+
return ignored;
|
|
90
121
|
}
|
|
91
122
|
|
|
92
123
|
function hasMixedClasses(run) {
|
|
@@ -107,7 +138,8 @@ function isRedactedValue(value) {
|
|
|
107
138
|
}
|
|
108
139
|
|
|
109
140
|
function scrubTokens(text, { allowHex = false, segment = false } = {}) {
|
|
110
|
-
let out = text.replace(
|
|
141
|
+
let out = text.replace(PREFIXED_TOKEN, redacted);
|
|
142
|
+
out = out.replace(JWT, redacted);
|
|
111
143
|
out = out.replace(AUTH_SCHEME, (match, scheme, space, credential) => {
|
|
112
144
|
const classes = [/[A-Z]/, /[a-z]/, /[0-9]/, /[._~+/=-]/].filter((re) =>
|
|
113
145
|
re.test(credential)
|
package/logs/summarize-event.js
CHANGED
|
@@ -32,13 +32,26 @@ function unique(list) {
|
|
|
32
32
|
return [...new Set(list)];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// The route template keeps ids out of the field: REST v1 `resource`, or the
|
|
36
|
+
// path part of an HTTP API v2 `routeKey` such as `GET /api/{id}`.
|
|
37
|
+
function routeTemplate(event, path) {
|
|
38
|
+
if (typeof event.resource === 'string' && event.resource) {
|
|
39
|
+
return event.resource;
|
|
40
|
+
}
|
|
41
|
+
if (typeof event.routeKey === 'string') {
|
|
42
|
+
const space = event.routeKey.indexOf(' ');
|
|
43
|
+
if (space > 0) return event.routeKey.slice(space + 1);
|
|
44
|
+
}
|
|
45
|
+
return path;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
function summarizeHttp(event) {
|
|
36
49
|
const path = event.path || event.rawPath;
|
|
37
50
|
const summary = {
|
|
38
51
|
source: 'http',
|
|
39
52
|
method: event.httpMethod || event.requestContext?.http?.method,
|
|
40
53
|
path,
|
|
41
|
-
route: path,
|
|
54
|
+
route: routeTemplate(event, path),
|
|
42
55
|
queryKeys: unique([
|
|
43
56
|
...objectKeys(event.queryStringParameters),
|
|
44
57
|
...objectKeys(event.multiValueQueryStringParameters),
|
|
@@ -98,8 +111,6 @@ function toScopeInvocation(summary) {
|
|
|
98
111
|
source: 'http',
|
|
99
112
|
method: summary.method,
|
|
100
113
|
route: summary.route,
|
|
101
|
-
queryKeys: summary.queryKeys ?? [],
|
|
102
|
-
headerNames: summary.headerNames ?? [],
|
|
103
114
|
};
|
|
104
115
|
if (summary.routeKey) invocation.routeKey = summary.routeKey;
|
|
105
116
|
return invocation;
|
|
@@ -107,4 +118,19 @@ function toScopeInvocation(summary) {
|
|
|
107
118
|
return { source: summary.source };
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
|
|
121
|
+
// Per-request detail for the one entry record, never for the scope.
|
|
122
|
+
function toRequestDetails(summary) {
|
|
123
|
+
if (!summary || summary.source !== 'http') return {};
|
|
124
|
+
return {
|
|
125
|
+
path: summary.path,
|
|
126
|
+
queryKeys: summary.queryKeys ?? [],
|
|
127
|
+
headerNames: summary.headerNames ?? [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
summarizeLambdaEvent,
|
|
133
|
+
summarizeMessageBody,
|
|
134
|
+
toScopeInvocation,
|
|
135
|
+
toRequestDetails,
|
|
136
|
+
};
|
package/modules/module.js
CHANGED
|
@@ -32,7 +32,7 @@ class Module extends Delegate {
|
|
|
32
32
|
this.credential = entityObj?.credential;
|
|
33
33
|
this.definition = definition;
|
|
34
34
|
this.name = this.definition.moduleName;
|
|
35
|
-
this.logger = getLogger(`module.${this.name}`).child(() => ({
|
|
35
|
+
this.logger = getLogger(`module.${this.name ?? 'unknown'}`).child(() => ({
|
|
36
36
|
entityId: this.entity?.id,
|
|
37
37
|
credentialId:
|
|
38
38
|
this.credential?.id ??
|
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.42e42b2.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.42e42b2.0",
|
|
51
|
+
"@friggframework/prettier-config": "2.0.0--canary.654.42e42b2.0",
|
|
52
|
+
"@friggframework/test": "2.0.0--canary.654.42e42b2.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": "42e42b2b48e90e9b46ffe6bcf009150a66a010cd"
|
|
93
93
|
}
|