@friggframework/core 2.0.0--canary.654.42e42b2.0 → 2.0.0--canary.654.3cd4278.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/.eslintrc.json +18 -1
- package/core/Worker.js +3 -17
- package/core/create-handler.js +28 -31
- package/handlers/app-handler-helpers.js +2 -0
- package/handlers/backend-utils.js +0 -5
- package/handlers/workers/dlq-processor.js +9 -11
- package/integrations/integration-base.js +1 -1
- package/logs/__fixtures__/secrets.js +56 -19
- package/logs/__fixtures__/vectors.js +3 -3
- package/logs/debug-shims.js +2 -5
- package/logs/record.js +33 -1
- package/logs/redact.js +17 -9
- package/logs/summarize-event.js +28 -2
- package/modules/requester/oauth-2.js +1 -1
- package/package.json +5 -5
- package/user/repositories/user-repository-documentdb.js +6 -14
package/.eslintrc.json
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "@friggframework/eslint-config"
|
|
2
|
+
"extends": "@friggframework/eslint-config",
|
|
3
|
+
"rules": {
|
|
4
|
+
"no-restricted-syntax": [
|
|
5
|
+
"warn",
|
|
6
|
+
{
|
|
7
|
+
"selector": "CallExpression[callee.object.object.name='process'][callee.object.property.name=/^(stdout|stderr)$/][callee.property.name='write']",
|
|
8
|
+
"message": "Write through the logger; only logs/sinks.js writes to fd 1 (ADR-048 §14)"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"overrides": [
|
|
13
|
+
{
|
|
14
|
+
"files": ["*.test.js", "database/utils/prisma-runner.js"],
|
|
15
|
+
"rules": {
|
|
16
|
+
"no-restricted-syntax": "off"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
3
20
|
}
|
package/core/Worker.js
CHANGED
|
@@ -29,28 +29,14 @@ class Worker {
|
|
|
29
29
|
|
|
30
30
|
for (const record of records) {
|
|
31
31
|
await runMessageScope(record, async () => {
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
let parsedEvent;
|
|
35
|
-
try {
|
|
36
|
-
parsedEvent = JSON.parse(record.body)?.event;
|
|
37
|
-
} catch {
|
|
38
|
-
parsedEvent = undefined;
|
|
39
|
-
}
|
|
40
|
-
console.log(`[Worker] record begin`, {
|
|
41
|
-
messageId: record.messageId,
|
|
42
|
-
event: parsedEvent,
|
|
43
|
-
receiveCount: record.attributes?.ApproximateReceiveCount,
|
|
44
|
-
});
|
|
32
|
+
// messageId, receiveCount and the event come from the scope.
|
|
33
|
+
log.debug('Record started', { eventName: 'frigg.worker.record_started' });
|
|
45
34
|
|
|
46
35
|
try {
|
|
47
36
|
const runParams = JSON.parse(record.body);
|
|
48
37
|
this._validateParams(runParams);
|
|
49
38
|
await this._run(runParams, context);
|
|
50
|
-
|
|
51
|
-
messageId: record.messageId,
|
|
52
|
-
event: runParams?.event,
|
|
53
|
-
});
|
|
39
|
+
log.debug('Record succeeded', { eventName: 'frigg.worker.record_succeeded' });
|
|
54
40
|
} catch (error) {
|
|
55
41
|
if (error.isHaltError) {
|
|
56
42
|
// HaltError means "discard this message, don't retry".
|
package/core/create-handler.js
CHANGED
|
@@ -6,7 +6,7 @@ const { getLogger, toSanitizedSurrogate } = require('../logs');
|
|
|
6
6
|
const {
|
|
7
7
|
summarizeLambdaEvent,
|
|
8
8
|
toScopeInvocation,
|
|
9
|
-
|
|
9
|
+
toRequestInvocation,
|
|
10
10
|
} = require('../logs/summarize-event');
|
|
11
11
|
const {
|
|
12
12
|
runInvocationScope,
|
|
@@ -58,6 +58,8 @@ const createHandler = (optionByName = {}) => {
|
|
|
58
58
|
routeKey: eventSummary.routeKey,
|
|
59
59
|
invocation: toScopeInvocation(eventSummary),
|
|
60
60
|
};
|
|
61
|
+
// The full redacted summary goes only on the entry and failure records.
|
|
62
|
+
const requestInvocation = toRequestInvocation(eventSummary);
|
|
61
63
|
|
|
62
64
|
return runInvocationScope(
|
|
63
65
|
scopeFields,
|
|
@@ -65,7 +67,7 @@ const createHandler = (optionByName = {}) => {
|
|
|
65
67
|
try {
|
|
66
68
|
log.info('Handler invoked', {
|
|
67
69
|
eventName: 'frigg.handler.invoked',
|
|
68
|
-
|
|
70
|
+
invocation: requestInvocation,
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
@@ -86,45 +88,28 @@ const createHandler = (optionByName = {}) => {
|
|
|
86
88
|
// Run the Lambda
|
|
87
89
|
return await method(event, context);
|
|
88
90
|
} catch (error) {
|
|
89
|
-
//
|
|
90
|
-
if (isUserFacingResponse) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
eventName: 'frigg.handler.rejected',
|
|
96
|
-
statusCode,
|
|
97
|
-
error,
|
|
98
|
-
});
|
|
99
|
-
return {
|
|
100
|
-
statusCode,
|
|
101
|
-
body: JSON.stringify({
|
|
102
|
-
error: error.message,
|
|
103
|
-
}),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
log.error('Handler failed', {
|
|
108
|
-
eventName: 'frigg.handler.failed',
|
|
91
|
+
// Allow client-safe errors to pass through with their actual message
|
|
92
|
+
if (isUserFacingResponse && error.isClientSafe === true) {
|
|
93
|
+
const statusCode = error.statusCode || 400;
|
|
94
|
+
log.warn('Request rejected', {
|
|
95
|
+
eventName: 'frigg.handler.rejected',
|
|
96
|
+
statusCode,
|
|
109
97
|
error,
|
|
110
98
|
});
|
|
111
|
-
|
|
112
|
-
// Hide other errors with generic message
|
|
113
99
|
return {
|
|
114
|
-
statusCode
|
|
100
|
+
statusCode,
|
|
115
101
|
body: JSON.stringify({
|
|
116
|
-
error:
|
|
102
|
+
error: error.message,
|
|
117
103
|
}),
|
|
118
104
|
};
|
|
119
105
|
}
|
|
120
106
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// itself is the failure record.
|
|
125
|
-
if (error.isHaltError === true) {
|
|
107
|
+
// Server-to-server: halt errors succeed and won't be
|
|
108
|
+
// retried, so the halt itself is the failure record.
|
|
109
|
+
if (!isUserFacingResponse && error.isHaltError === true) {
|
|
126
110
|
log.error('Handler halted', {
|
|
127
111
|
eventName: 'frigg.handler.halted',
|
|
112
|
+
invocation: requestInvocation,
|
|
128
113
|
error,
|
|
129
114
|
});
|
|
130
115
|
return;
|
|
@@ -132,8 +117,20 @@ const createHandler = (optionByName = {}) => {
|
|
|
132
117
|
|
|
133
118
|
log.error('Handler failed', {
|
|
134
119
|
eventName: 'frigg.handler.failed',
|
|
120
|
+
invocation: requestInvocation,
|
|
135
121
|
error,
|
|
136
122
|
});
|
|
123
|
+
|
|
124
|
+
// Don't leak implementation details to end users.
|
|
125
|
+
if (isUserFacingResponse) {
|
|
126
|
+
return {
|
|
127
|
+
statusCode: 500,
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
error: 'An Internal Error Occurred',
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
137
134
|
// The Lambda runtime writes a rethrown error itself, so
|
|
138
135
|
// rethrow a sanitized copy (ADR-048 §6 item 11).
|
|
139
136
|
throw toSanitizedSurrogate(error);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { createHandler } = require('@friggframework/core');
|
|
2
2
|
const { getLogger } = require('../logs');
|
|
3
|
+
const { summarizeExpressRequest } = require('../logs/summarize-event');
|
|
3
4
|
const express = require('express');
|
|
4
5
|
const bodyParser = require('body-parser');
|
|
5
6
|
const cors = require('cors');
|
|
@@ -35,6 +36,7 @@ const createApp = (applyMiddleware) => {
|
|
|
35
36
|
log.error('Request failed', {
|
|
36
37
|
eventName: 'frigg.http.request_failed',
|
|
37
38
|
statusCode,
|
|
39
|
+
invocation: summarizeExpressRequest(req),
|
|
38
40
|
error: boomError,
|
|
39
41
|
});
|
|
40
42
|
res.status(statusCode).json({ error: 'Internal Server Error' });
|
|
@@ -273,11 +273,6 @@ const createQueueWorker = (integrationClass) => {
|
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
console.error(
|
|
277
|
-
`Error in ${params.event} for ${integrationName}:`,
|
|
278
|
-
error
|
|
279
|
-
);
|
|
280
|
-
|
|
281
276
|
// 4xx HTTP errors are permanent — the requester already
|
|
282
277
|
// attempted token refresh (401) and backoff (429/5xx).
|
|
283
278
|
// By the time a 4xx reaches here, retrying won't help.
|
|
@@ -27,24 +27,22 @@ function extractQueueName(eventSourceARN) {
|
|
|
27
27
|
// The body can hold credentials, so only its size and digest are logged.
|
|
28
28
|
function describeBody(body) {
|
|
29
29
|
const text = typeof body === 'string' ? body : String(body ?? '');
|
|
30
|
+
let parsed = true;
|
|
31
|
+
try {
|
|
32
|
+
JSON.parse(text);
|
|
33
|
+
} catch {
|
|
34
|
+
parsed = false;
|
|
35
|
+
}
|
|
30
36
|
return {
|
|
37
|
+
parsed,
|
|
31
38
|
bodyLength: Buffer.byteLength(text),
|
|
32
39
|
bodySha256: crypto.createHash('sha256').update(text).digest('hex'),
|
|
33
40
|
};
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
function isJson(body) {
|
|
37
|
-
try {
|
|
38
|
-
JSON.parse(body);
|
|
39
|
-
return true;
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
43
|
function logRecord(record) {
|
|
46
|
-
const body = describeBody(record.body);
|
|
47
|
-
if (!
|
|
44
|
+
const { parsed, ...body } = describeBody(record.body);
|
|
45
|
+
if (!parsed) {
|
|
48
46
|
log.warn('DLQ message body is not JSON', {
|
|
49
47
|
eventName: 'frigg.queue.dlq.body_unparsed',
|
|
50
48
|
...body,
|
|
@@ -778,7 +778,7 @@ class IntegrationBase {
|
|
|
778
778
|
if (this.events[eventName]) {
|
|
779
779
|
if (typeof handlers[eventName] === 'string') {
|
|
780
780
|
this.logger.warn(
|
|
781
|
-
|
|
781
|
+
'Binding handler is ignored: the event is already set',
|
|
782
782
|
{
|
|
783
783
|
eventName: `${this.logger.name}.extension_handler_shadowed`,
|
|
784
784
|
bindingName,
|
|
@@ -1,22 +1,59 @@
|
|
|
1
|
-
// Fake values
|
|
1
|
+
// Fake values, generated at load time so secret scanners find no literal in
|
|
2
|
+
// the source. A fixed seed per name keeps every run identical.
|
|
3
|
+
const MIXED = 'BCDFGHJKLMNPQRSTVWXZbcdfghjkmnpqrstvwxz23456789';
|
|
4
|
+
const HEX = '0123456789abcdef';
|
|
5
|
+
|
|
6
|
+
function seededRandom(name) {
|
|
7
|
+
let seed = [...name].reduce(
|
|
8
|
+
(h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619),
|
|
9
|
+
2166136261
|
|
10
|
+
);
|
|
11
|
+
return () => {
|
|
12
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
13
|
+
let t = Math.imul(seed ^ (seed >>> 15), seed | 1);
|
|
14
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
15
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function draw(name, alphabet, length) {
|
|
20
|
+
const next = seededRandom(name);
|
|
21
|
+
let out = '';
|
|
22
|
+
for (let i = 0; i < length; i += 1) {
|
|
23
|
+
out += alphabet[Math.floor(next() * alphabet.length)];
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The fixed 'Zq' + 'B7k' start puts upper case, lower case and a digit in every token.
|
|
29
|
+
function fakeToken(name, length = 28) {
|
|
30
|
+
return `ZqB7k${draw(name, MIXED, length - 5)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const base64url = (value) => Buffer.from(value).toString('base64url');
|
|
34
|
+
|
|
2
35
|
const SECRETS = Object.freeze({
|
|
3
|
-
bearer: '
|
|
4
|
-
apiKeyQuery: '
|
|
5
|
-
friggApiKey: '
|
|
6
|
-
cookie: '
|
|
7
|
-
oauthCode: '
|
|
8
|
-
codeVerifier: '
|
|
9
|
-
password: '
|
|
10
|
-
clientSecret: '
|
|
11
|
-
accessToken: '
|
|
12
|
-
refreshToken: '
|
|
13
|
-
idToken: '
|
|
14
|
-
hashword: '
|
|
15
|
-
dbPassword: '
|
|
16
|
-
signature: '
|
|
17
|
-
jwt:
|
|
18
|
-
|
|
19
|
-
|
|
36
|
+
bearer: fakeToken('bearer'),
|
|
37
|
+
apiKeyQuery: fakeToken('apiKeyQuery'),
|
|
38
|
+
friggApiKey: fakeToken('friggApiKey'),
|
|
39
|
+
cookie: fakeToken('cookie'),
|
|
40
|
+
oauthCode: fakeToken('oauthCode'),
|
|
41
|
+
codeVerifier: fakeToken('codeVerifier'),
|
|
42
|
+
password: fakeToken('password'),
|
|
43
|
+
clientSecret: fakeToken('clientSecret'),
|
|
44
|
+
accessToken: fakeToken('accessToken'),
|
|
45
|
+
refreshToken: fakeToken('refreshToken'),
|
|
46
|
+
idToken: fakeToken('idToken'),
|
|
47
|
+
hashword: fakeToken('hashword'),
|
|
48
|
+
dbPassword: fakeToken('dbPassword'),
|
|
49
|
+
signature: fakeToken('signature'),
|
|
50
|
+
jwt: [
|
|
51
|
+
base64url(JSON.stringify({ alg: 'HS256' })),
|
|
52
|
+
base64url(JSON.stringify({ sub: `fake-${draw('jwtSub', MIXED, 8)}` })),
|
|
53
|
+
base64url(draw('jwtSig', MIXED, 24)),
|
|
54
|
+
].join('.'),
|
|
55
|
+
hexToken: draw('hexToken', HEX, 40),
|
|
56
|
+
base64Run: Buffer.from(draw('base64Run', MIXED, 42)).toString('base64'),
|
|
20
57
|
});
|
|
21
58
|
|
|
22
|
-
module.exports = { SECRETS };
|
|
59
|
+
module.exports = { SECRETS, fakeToken };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// ADR-048 §14 redaction vectors. Each vector is one log call that carries
|
|
2
2
|
// secrets; the suite asserts that no 8-char window of them reaches a record.
|
|
3
|
-
const { SECRETS: S } = require('./secrets');
|
|
3
|
+
const { SECRETS: S, fakeToken } = require('./secrets');
|
|
4
4
|
const { httpApiV2Event, restV1Event, sqsEvent } = require('./events');
|
|
5
5
|
const { summarizeLambdaEvent } = require('../summarize-event');
|
|
6
6
|
|
|
7
7
|
const secretUrl = `https://api.example.com/v1/items?api_key=${S.apiKeyQuery}`;
|
|
8
|
-
const SIGNATURE_VALUE = '
|
|
8
|
+
const SIGNATURE_VALUE = fakeToken('signatureValue', 27);
|
|
9
9
|
|
|
10
10
|
// Built by concatenation so push protection does not match the prefixes.
|
|
11
|
-
const TOKEN_TAIL = '
|
|
11
|
+
const TOKEN_TAIL = fakeToken('prefixTail', 25);
|
|
12
12
|
const PREFIXED_TOKENS = [
|
|
13
13
|
'sk_' + 'live_' + TOKEN_TAIL,
|
|
14
14
|
'whsec' + '_' + TOKEN_TAIL,
|
package/logs/debug-shims.js
CHANGED
|
@@ -3,8 +3,7 @@ const { getLogger } = require('./logger');
|
|
|
3
3
|
const { getContext } = require('./context');
|
|
4
4
|
const {
|
|
5
5
|
summarizeLambdaEvent,
|
|
6
|
-
|
|
7
|
-
toRequestDetails,
|
|
6
|
+
toRequestInvocation,
|
|
8
7
|
} = require('./summarize-event');
|
|
9
8
|
|
|
10
9
|
// Deprecated. Kept for one major version; use getLogger or this.logger.
|
|
@@ -32,10 +31,8 @@ function initDebugLog(...initMessages) {
|
|
|
32
31
|
const log = legacy();
|
|
33
32
|
if (!log.isLevelEnabled('DEBUG')) return;
|
|
34
33
|
const event = initMessages.find((m) => m && typeof m === 'object');
|
|
35
|
-
const summary = summarizeLambdaEvent(event);
|
|
36
34
|
log.debug('Debug log initialized', {
|
|
37
|
-
invocation:
|
|
38
|
-
...toRequestDetails(summary),
|
|
35
|
+
invocation: toRequestInvocation(summarizeLambdaEvent(event)),
|
|
39
36
|
});
|
|
40
37
|
}
|
|
41
38
|
|
package/logs/record.js
CHANGED
|
@@ -78,6 +78,29 @@ function buildMessage(message, callSite) {
|
|
|
78
78
|
return '[non-string message]';
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
const INVOCATION_KEY = 'invocation';
|
|
82
|
+
|
|
83
|
+
function isPlainObject(value) {
|
|
84
|
+
return (
|
|
85
|
+
value !== null &&
|
|
86
|
+
typeof value === 'object' &&
|
|
87
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A boundary adds request detail to the scope's invocation; scope keys win.
|
|
92
|
+
function extendInvocation(scoped, detail, dropped) {
|
|
93
|
+
const merged = { ...scoped };
|
|
94
|
+
for (const [key, value] of Object.entries(detail)) {
|
|
95
|
+
if (!Object.prototype.hasOwnProperty.call(scoped, key)) {
|
|
96
|
+
merged[key] = value;
|
|
97
|
+
} else if (JSON.stringify(scoped[key]) !== JSON.stringify(value)) {
|
|
98
|
+
dropped.add(`${INVOCATION_KEY}.${key}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
function traceFields(spanContext) {
|
|
82
105
|
if (
|
|
83
106
|
!spanContext ||
|
|
@@ -135,7 +158,8 @@ function buildRecord({
|
|
|
135
158
|
OWNED_KEYS.has(key) ||
|
|
136
159
|
RESERVED_KEYS.has(key) ||
|
|
137
160
|
(dropPayloads && PAYLOAD_KEYS.has(key)) ||
|
|
138
|
-
Object.prototype.hasOwnProperty.call(record, key)
|
|
161
|
+
(Object.prototype.hasOwnProperty.call(record, key) &&
|
|
162
|
+
!canExtendInvocation(key, value))
|
|
139
163
|
) {
|
|
140
164
|
dropped.add(key);
|
|
141
165
|
continue;
|
|
@@ -149,10 +173,18 @@ function buildRecord({
|
|
|
149
173
|
const value =
|
|
150
174
|
key === 'error' ? serializeError(accepted[key]) : safe?.[key];
|
|
151
175
|
if (value === undefined || value === null) continue;
|
|
176
|
+
if (key === INVOCATION_KEY && isPlainObject(record[key])) {
|
|
177
|
+
record[key] = extendInvocation(record[key], value, dropped);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
152
180
|
record[key] = value;
|
|
153
181
|
if (isCallSite) callSiteKeys.push(key);
|
|
154
182
|
}
|
|
155
183
|
};
|
|
184
|
+
const canExtendInvocation = (key, value) =>
|
|
185
|
+
key === INVOCATION_KEY &&
|
|
186
|
+
isPlainObject(record[key]) &&
|
|
187
|
+
isPlainObject(value);
|
|
156
188
|
place(readFields(scope), false);
|
|
157
189
|
place(readFields(bindings), false);
|
|
158
190
|
place(callSite, true);
|
package/logs/redact.js
CHANGED
|
@@ -56,7 +56,9 @@ const MAX_SCRUB_INPUT = 65536;
|
|
|
56
56
|
const URL_IN_TEXT = /\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"'<>`]+/g;
|
|
57
57
|
const URL_TRAILING_PUNCT = /[.,;:!?)\]}]+$/;
|
|
58
58
|
const ABSOLUTE_URL = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
|
|
59
|
-
const
|
|
59
|
+
const RAW_ABSOLUTE_PATH = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*([^?#]*)/;
|
|
60
|
+
const RAW_RELATIVE_PATH = /^[^?#]*/;
|
|
61
|
+
const PATH_TOKEN_SEGMENT = /^[A-Za-z0-9_.~-]{20,}$/;
|
|
60
62
|
const PREFIXED_TOKEN =
|
|
61
63
|
/(?<![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;
|
|
62
64
|
const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
|
|
@@ -196,11 +198,18 @@ function redactHash(hash) {
|
|
|
196
198
|
return scrubTokens(hash);
|
|
197
199
|
}
|
|
198
200
|
|
|
201
|
+
// A long segment mixing upper case, lower case and digits is an opaque
|
|
202
|
+
// token (webhook secrets live in paths); hex ids and UUIDs lack one class.
|
|
203
|
+
function redactPathSegment(segment) {
|
|
204
|
+
if (PATH_TOKEN_SEGMENT.test(segment) && hasMixedClasses(segment)) {
|
|
205
|
+
return redacted(segment);
|
|
206
|
+
}
|
|
207
|
+
return scrubTokens(segment, { segment: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The raw path, not URL#pathname, which percent-encodes `{proxy+}`.
|
|
199
211
|
function redactPath(pathname) {
|
|
200
|
-
return pathname
|
|
201
|
-
.split('/')
|
|
202
|
-
.map((segment) => scrubTokens(segment, { segment: true }))
|
|
203
|
-
.join('/');
|
|
212
|
+
return pathname.split('/').map(redactPathSegment).join('/');
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
function redactUrlFallback(raw) {
|
|
@@ -221,16 +230,15 @@ function redactUrl(url) {
|
|
|
221
230
|
try {
|
|
222
231
|
if (ABSOLUTE_URL.test(raw)) {
|
|
223
232
|
const u = new URL(raw);
|
|
224
|
-
const path =
|
|
225
|
-
u.pathname === '/' && !ROOT_SLASH.test(raw) ? '' : u.pathname;
|
|
233
|
+
const path = raw.match(RAW_ABSOLUTE_PATH)[1];
|
|
226
234
|
return `${u.protocol}//${u.host}${redactPath(path)}${redactParams(
|
|
227
235
|
u.search,
|
|
228
236
|
'?'
|
|
229
237
|
)}${redactHash(u.hash)}`;
|
|
230
238
|
}
|
|
231
239
|
if (raw.startsWith('/')) {
|
|
232
|
-
const u = new URL(raw, '
|
|
233
|
-
return `${redactPath(
|
|
240
|
+
const u = new URL(raw, 'https://relative.invalid');
|
|
241
|
+
return `${redactPath(raw.match(RAW_RELATIVE_PATH)[0])}${redactParams(
|
|
234
242
|
u.search,
|
|
235
243
|
'?'
|
|
236
244
|
)}${redactHash(u.hash)}`;
|
package/logs/summarize-event.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { redactUrl } = require('./redact');
|
|
2
|
+
|
|
1
3
|
// Best-effort extraction of the logical event/processId/integrationId from a
|
|
2
4
|
// JSON message body. Used only for log correlation. Never throws.
|
|
3
5
|
function summarizeMessageBody(bodyStr) {
|
|
@@ -118,19 +120,43 @@ function toScopeInvocation(summary) {
|
|
|
118
120
|
return { source: summary.source };
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
// Per-request detail for the
|
|
123
|
+
// Per-request detail for the entry and failure records, never for the
|
|
124
|
+
// scope. A raw path can hold a token, so it goes through redactUrl.
|
|
122
125
|
function toRequestDetails(summary) {
|
|
123
126
|
if (!summary || summary.source !== 'http') return {};
|
|
124
127
|
return {
|
|
125
|
-
path: summary.path,
|
|
128
|
+
path: redactUrl(summary.path ?? ''),
|
|
126
129
|
queryKeys: summary.queryKeys ?? [],
|
|
127
130
|
headerNames: summary.headerNames ?? [],
|
|
128
131
|
};
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
// The full redacted request summary (ADR-048 §6: it goes under `invocation`).
|
|
135
|
+
function toRequestInvocation(summary) {
|
|
136
|
+
return { ...toScopeInvocation(summary), ...toRequestDetails(summary) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The same shape from an express request, for the express error boundary.
|
|
140
|
+
function summarizeExpressRequest(req) {
|
|
141
|
+
try {
|
|
142
|
+
if (!req || typeof req !== 'object') return { source: 'http' };
|
|
143
|
+
return {
|
|
144
|
+
source: 'http',
|
|
145
|
+
method: req.method,
|
|
146
|
+
path: redactUrl(req.path ?? ''),
|
|
147
|
+
queryKeys: objectKeys(req.query),
|
|
148
|
+
headerNames: unique(objectKeys(req.headers).map((name) => name.toLowerCase())),
|
|
149
|
+
};
|
|
150
|
+
} catch {
|
|
151
|
+
return { source: 'http' };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
131
155
|
module.exports = {
|
|
132
156
|
summarizeLambdaEvent,
|
|
133
157
|
summarizeMessageBody,
|
|
134
158
|
toScopeInvocation,
|
|
135
159
|
toRequestDetails,
|
|
160
|
+
toRequestInvocation,
|
|
161
|
+
summarizeExpressRequest,
|
|
136
162
|
};
|
|
@@ -357,7 +357,7 @@ class OAuth2Requester extends Requester {
|
|
|
357
357
|
eventName: `${this.logger.name}.token_refresh_failed`,
|
|
358
358
|
statusCode:
|
|
359
359
|
error?.statusCode ?? error?.status ?? error?.response?.status,
|
|
360
|
-
error,
|
|
360
|
+
reason: error?.message,
|
|
361
361
|
});
|
|
362
362
|
|
|
363
363
|
if (!this._isDefinitiveAuthRejection(error)) {
|
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.3cd4278.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.3cd4278.0",
|
|
51
|
+
"@friggframework/prettier-config": "2.0.0--canary.654.3cd4278.0",
|
|
52
|
+
"@friggframework/test": "2.0.0--canary.654.3cd4278.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": "3cd42784313ae866c499d7c7549b7c28e39d3045"
|
|
93
93
|
}
|
|
@@ -148,13 +148,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
148
148
|
|
|
149
149
|
// Defensive check: verify document was found after insert
|
|
150
150
|
if (!created) {
|
|
151
|
-
log.error('User not found after insert', {
|
|
152
|
-
eventName: 'frigg.user.not_found_after_insert',
|
|
153
|
-
insertedId: fromObjectId(insertedId),
|
|
154
|
-
paramKeys: Object.keys(params),
|
|
155
|
-
});
|
|
156
151
|
throw new Error(
|
|
157
|
-
|
|
152
|
+
`Failed to create individual user: Document not found after insert (insertedId ${fromObjectId(insertedId)}). ` +
|
|
158
153
|
'This indicates a database consistency issue.'
|
|
159
154
|
);
|
|
160
155
|
}
|
|
@@ -192,13 +187,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
192
187
|
|
|
193
188
|
// Defensive check: verify document was found after insert
|
|
194
189
|
if (!created) {
|
|
195
|
-
log.error('Organization user not found after insert', {
|
|
196
|
-
eventName: 'frigg.user.organization_not_found_after_insert',
|
|
197
|
-
insertedId: fromObjectId(insertedId),
|
|
198
|
-
paramKeys: Object.keys(params),
|
|
199
|
-
});
|
|
200
190
|
throw new Error(
|
|
201
|
-
|
|
191
|
+
`Failed to create organization user: Document not found after insert (insertedId ${fromObjectId(insertedId)}). ` +
|
|
202
192
|
'This indicates a database consistency issue.'
|
|
203
193
|
);
|
|
204
194
|
}
|
|
@@ -284,7 +274,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
284
274
|
// Defensive check: verify document was found after update
|
|
285
275
|
if (!updated) {
|
|
286
276
|
throw new Error(
|
|
287
|
-
|
|
277
|
+
'Failed to update individual user: Document not found after update. ' +
|
|
278
|
+
'This indicates a database consistency issue.'
|
|
288
279
|
);
|
|
289
280
|
}
|
|
290
281
|
|
|
@@ -317,7 +308,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
317
308
|
|
|
318
309
|
if (!updated) {
|
|
319
310
|
throw new Error(
|
|
320
|
-
|
|
311
|
+
'Failed to update organization user: Document not found after update. ' +
|
|
312
|
+
'This indicates a database consistency issue.'
|
|
321
313
|
);
|
|
322
314
|
}
|
|
323
315
|
|