@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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const {
|
|
2
|
+
isDeniedNormalized,
|
|
3
|
+
isDigestNormalized,
|
|
4
|
+
isOAuthCallbackShapeNormalized,
|
|
5
|
+
normalizeKey,
|
|
6
|
+
redactUrl,
|
|
7
|
+
scrubString,
|
|
8
|
+
} = require('./redact');
|
|
9
|
+
|
|
10
|
+
const MAX_DEPTH = 6;
|
|
11
|
+
const MAX_CAUSE_DEPTH = 3;
|
|
12
|
+
const MAX_STRING = 2048;
|
|
13
|
+
const MAX_AGGREGATE_ERRORS = 10;
|
|
14
|
+
const HEADER_KEYS = new Set(['headers', 'multivalueheaders', 'rawheaders']);
|
|
15
|
+
|
|
16
|
+
function attempt(fn, fallback) {
|
|
17
|
+
try {
|
|
18
|
+
return fn();
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cutString(value) {
|
|
25
|
+
if (value.length <= MAX_STRING) return value;
|
|
26
|
+
const suffix = `…[truncated:${value.length}]`;
|
|
27
|
+
return value.slice(0, MAX_STRING - suffix.length) + suffix;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanString(value, normalizedKey = '') {
|
|
31
|
+
return cutString(
|
|
32
|
+
scrubString(value, { allowHex: isDigestNormalized(normalizedKey) })
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isError(value) {
|
|
37
|
+
return attempt(
|
|
38
|
+
() =>
|
|
39
|
+
value instanceof Error ||
|
|
40
|
+
Object.prototype.toString.call(value) === '[object Error]',
|
|
41
|
+
false
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isHeadersLike(value) {
|
|
46
|
+
if (typeof Headers !== 'undefined' && value instanceof Headers) return true;
|
|
47
|
+
return (
|
|
48
|
+
!(value instanceof Map) &&
|
|
49
|
+
typeof value.get === 'function' &&
|
|
50
|
+
typeof value.has === 'function' &&
|
|
51
|
+
typeof value.forEach === 'function' &&
|
|
52
|
+
typeof value.entries === 'function'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function headerNames(value) {
|
|
57
|
+
if (value === null || value === undefined) return value;
|
|
58
|
+
if (typeof value !== 'object') return '[REDACTED]';
|
|
59
|
+
if (value instanceof Map) return [...value.keys()].map(String);
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
if (value.every((entry) => typeof entry === 'string')) {
|
|
62
|
+
return value.filter((_entry, i) => i % 2 === 0);
|
|
63
|
+
}
|
|
64
|
+
return value.every(Array.isArray)
|
|
65
|
+
? value.map((entry) => cleanString(String(entry[0])))
|
|
66
|
+
: '[REDACTED]';
|
|
67
|
+
}
|
|
68
|
+
if (isHeadersLike(value)) {
|
|
69
|
+
const names = [];
|
|
70
|
+
value.forEach((_v, name) => names.push(String(name)));
|
|
71
|
+
return names;
|
|
72
|
+
}
|
|
73
|
+
return Object.keys(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function walkObject(value, depth, seen) {
|
|
77
|
+
const keys = Object.keys(value);
|
|
78
|
+
const normalizedKeys = keys.map(normalizeKey);
|
|
79
|
+
const dropAll = isOAuthCallbackShapeNormalized(normalizedKeys);
|
|
80
|
+
const out = {};
|
|
81
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
82
|
+
const key = keys[i];
|
|
83
|
+
const normalized = normalizedKeys[i];
|
|
84
|
+
if (dropAll || isDeniedNormalized(normalized)) {
|
|
85
|
+
out[key] = '[REDACTED]';
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let child;
|
|
89
|
+
try {
|
|
90
|
+
child = value[key];
|
|
91
|
+
} catch {
|
|
92
|
+
out[key] = '[Getter threw]';
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (HEADER_KEYS.has(normalized)) {
|
|
96
|
+
out[key] = attempt(() => headerNames(child), '[Unserializable]');
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const result = walk(child, depth + 1, seen, normalized);
|
|
100
|
+
if (result !== undefined) out[key] = result;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function walkContainer(value, depth, seen) {
|
|
106
|
+
if (value instanceof Map) {
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const [k, v] of value) {
|
|
109
|
+
const key = String(k);
|
|
110
|
+
const normalized = normalizeKey(key);
|
|
111
|
+
if (isDeniedNormalized(normalized)) {
|
|
112
|
+
out[key] = '[REDACTED]';
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const result = walk(v, depth + 1, seen, normalized);
|
|
116
|
+
if (result !== undefined) out[key] = result;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
if (value instanceof Set) {
|
|
121
|
+
return [...value].map((v) => walk(v, depth + 1, seen) ?? null);
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
return value.map((v) => walk(v, depth + 1, seen) ?? null);
|
|
125
|
+
}
|
|
126
|
+
if (isHeadersLike(value)) return headerNames(value);
|
|
127
|
+
return walkObject(value, depth, seen);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function walkSpecial(value) {
|
|
131
|
+
if (value instanceof Date) {
|
|
132
|
+
return Number.isNaN(value.getTime())
|
|
133
|
+
? 'Invalid Date'
|
|
134
|
+
: value.toISOString();
|
|
135
|
+
}
|
|
136
|
+
if (value instanceof URL) return redactUrl(value);
|
|
137
|
+
if (value instanceof URLSearchParams) {
|
|
138
|
+
return [...value.keys()].map((k) => `${k}=REDACTED`).join('&');
|
|
139
|
+
}
|
|
140
|
+
if (Buffer.isBuffer(value)) return { type: 'Buffer', length: value.length };
|
|
141
|
+
if (ArrayBuffer.isView(value)) {
|
|
142
|
+
return {
|
|
143
|
+
type: value.constructor?.name ?? 'TypedArray',
|
|
144
|
+
length: value.byteLength,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (value instanceof ArrayBuffer) {
|
|
148
|
+
return { type: 'ArrayBuffer', length: value.byteLength };
|
|
149
|
+
}
|
|
150
|
+
if (value instanceof RegExp) return cleanString(String(value));
|
|
151
|
+
if (
|
|
152
|
+
(value._bsontype === 'ObjectId' || value._bsontype === 'ObjectID') &&
|
|
153
|
+
typeof value.toHexString === 'function'
|
|
154
|
+
) {
|
|
155
|
+
return value.toHexString();
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function walk(value, depth, seen, normalizedKey) {
|
|
161
|
+
switch (typeof value) {
|
|
162
|
+
case 'string':
|
|
163
|
+
return cleanString(value, normalizedKey);
|
|
164
|
+
case 'number':
|
|
165
|
+
return Number.isFinite(value) ? value : String(value);
|
|
166
|
+
case 'boolean':
|
|
167
|
+
return value;
|
|
168
|
+
case 'bigint':
|
|
169
|
+
return value.toString();
|
|
170
|
+
case 'undefined':
|
|
171
|
+
case 'symbol':
|
|
172
|
+
case 'function':
|
|
173
|
+
return undefined;
|
|
174
|
+
default:
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
if (value === null) return null;
|
|
178
|
+
if (isError(value)) return serializeErrorAt(value, 0, new WeakSet());
|
|
179
|
+
if (seen.has(value)) return '[Circular]';
|
|
180
|
+
if (depth >= MAX_DEPTH) return '[Depth]';
|
|
181
|
+
try {
|
|
182
|
+
const special = walkSpecial(value);
|
|
183
|
+
if (special !== undefined) return special;
|
|
184
|
+
seen.add(value);
|
|
185
|
+
try {
|
|
186
|
+
return walkContainer(value, depth, seen);
|
|
187
|
+
} finally {
|
|
188
|
+
seen.delete(value);
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
return '[Unserializable]';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function serializeValue(value, { depth = 0 } = {}) {
|
|
196
|
+
try {
|
|
197
|
+
return walk(value, depth, new WeakSet());
|
|
198
|
+
} catch {
|
|
199
|
+
return '[Unserializable]';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function errorType(err) {
|
|
204
|
+
const ctorName = attempt(() => err.constructor?.name, undefined);
|
|
205
|
+
const name = attempt(() => err.name, undefined);
|
|
206
|
+
if (typeof ctorName === 'string' && ctorName && ctorName !== 'Error') {
|
|
207
|
+
return ctorName;
|
|
208
|
+
}
|
|
209
|
+
if (typeof name === 'string' && name) return name;
|
|
210
|
+
return ctorName || 'Error';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function lastParagraph(message) {
|
|
214
|
+
const paragraphs = message
|
|
215
|
+
.split(/\n\s*\n/)
|
|
216
|
+
.map((p) => p.trim())
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
return paragraphs[paragraphs.length - 1] ?? '';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function errorStatus(err) {
|
|
222
|
+
const status = attempt(
|
|
223
|
+
() => err.statusCode ?? err.status ?? err.response?.status,
|
|
224
|
+
undefined
|
|
225
|
+
);
|
|
226
|
+
if (typeof status === 'number' && Number.isFinite(status)) return status;
|
|
227
|
+
if (typeof status === 'string' && status) return cleanString(status);
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function errorCode(err) {
|
|
232
|
+
const code = attempt(() => err.code, undefined);
|
|
233
|
+
if (typeof code === 'number' && Number.isFinite(code)) return code;
|
|
234
|
+
if (typeof code === 'string' && code) return cleanString(code);
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function errorStack(err, type, message) {
|
|
239
|
+
const stack = attempt(() => err.stack, undefined);
|
|
240
|
+
if (typeof stack !== 'string') return undefined;
|
|
241
|
+
const frames = stack.split('\n').filter((line) => /^\s*at\s/.test(line));
|
|
242
|
+
return cutString(
|
|
243
|
+
[`${type}: ${message}`, ...frames.map((f) => scrubString(f))].join('\n')
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function nested(value, depth, seen) {
|
|
248
|
+
if (depth > MAX_CAUSE_DEPTH) return '[Depth]';
|
|
249
|
+
if (value !== null && typeof value === 'object' && seen.has(value)) {
|
|
250
|
+
return '[Circular]';
|
|
251
|
+
}
|
|
252
|
+
return serializeErrorAt(value, depth, seen);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function nonErrorMessage(value) {
|
|
256
|
+
if (value === null || typeof value !== 'object') {
|
|
257
|
+
return cleanString(attempt(() => String(value), '[Unserializable]'));
|
|
258
|
+
}
|
|
259
|
+
const message = attempt(() => value.message, undefined);
|
|
260
|
+
if (typeof message === 'string') return cleanString(message);
|
|
261
|
+
return cutString(
|
|
262
|
+
attempt(() => JSON.stringify(serializeValue(value)), '[Unserializable]')
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function serializeErrorAt(err, depth, seen) {
|
|
267
|
+
if (!isError(err)) {
|
|
268
|
+
return { type: 'NonError', message: nonErrorMessage(err) };
|
|
269
|
+
}
|
|
270
|
+
seen.add(err);
|
|
271
|
+
const type = cutString(scrubString(errorType(err)));
|
|
272
|
+
const rawMessage = attempt(() => err.message, '');
|
|
273
|
+
let message = typeof rawMessage === 'string' ? rawMessage : String(rawMessage);
|
|
274
|
+
if (type.startsWith('PrismaClient')) message = lastParagraph(message);
|
|
275
|
+
message = cleanString(message);
|
|
276
|
+
|
|
277
|
+
const out = { type, message };
|
|
278
|
+
const code = errorCode(err);
|
|
279
|
+
if (code !== undefined) out.code = code;
|
|
280
|
+
const status = errorStatus(err);
|
|
281
|
+
if (status !== undefined) out.status = status;
|
|
282
|
+
const stack = errorStack(err, type, message);
|
|
283
|
+
if (stack !== undefined) out.stack = stack;
|
|
284
|
+
|
|
285
|
+
const cause = attempt(() => err.cause, undefined);
|
|
286
|
+
if (cause !== undefined && cause !== null) {
|
|
287
|
+
out.cause = nested(cause, depth + 1, seen);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const errors = attempt(() => err.errors, undefined);
|
|
291
|
+
if (type === 'AggregateError' && Array.isArray(errors)) {
|
|
292
|
+
out.errors = errors
|
|
293
|
+
.slice(0, MAX_AGGREGATE_ERRORS)
|
|
294
|
+
.map((e) => nested(e, depth + 1, seen));
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function serializeError(err) {
|
|
300
|
+
try {
|
|
301
|
+
return serializeErrorAt(err, 0, new WeakSet());
|
|
302
|
+
} catch {
|
|
303
|
+
return { type: 'Error', message: '[Unserializable]' };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function toSanitizedSurrogate(err) {
|
|
308
|
+
const serialized = serializeError(err);
|
|
309
|
+
const surrogate = new Error(serialized.message);
|
|
310
|
+
surrogate.name =
|
|
311
|
+
serialized.type === 'NonError' ? 'Error' : serialized.type;
|
|
312
|
+
surrogate.stack =
|
|
313
|
+
serialized.stack ?? `${surrogate.name}: ${serialized.message}`;
|
|
314
|
+
if (serialized.code !== undefined) surrogate.code = serialized.code;
|
|
315
|
+
if (serialized.status !== undefined) {
|
|
316
|
+
surrogate.statusCode = serialized.status;
|
|
317
|
+
}
|
|
318
|
+
return surrogate;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
isError,
|
|
323
|
+
serializeValue,
|
|
324
|
+
serializeError,
|
|
325
|
+
toSanitizedSurrogate,
|
|
326
|
+
MAX_DEPTH,
|
|
327
|
+
MAX_CAUSE_DEPTH,
|
|
328
|
+
MAX_STRING,
|
|
329
|
+
};
|
package/logs/sinks.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const { deepFreeze } = require('./record');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_RETRIES = 10;
|
|
5
|
+
const MAX_SLEEP_MS = 20;
|
|
6
|
+
|
|
7
|
+
function sleepSync(ms) {
|
|
8
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function defaultWriteStderr(line) {
|
|
12
|
+
fs.writeSync(2, line);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function safeCode(code) {
|
|
16
|
+
return String(code ?? 'UNKNOWN').replace(/[^A-Za-z0-9_]/g, '').slice(0, 32) || 'UNKNOWN';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createStdoutSink({
|
|
20
|
+
fd = 1,
|
|
21
|
+
writeSync = fs.writeSync,
|
|
22
|
+
writeStderr = defaultWriteStderr,
|
|
23
|
+
sleep = sleepSync,
|
|
24
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
25
|
+
} = {}) {
|
|
26
|
+
const reportFailure = (code) => {
|
|
27
|
+
try {
|
|
28
|
+
writeStderr(`frigg.logger.write_failed code=${safeCode(code)}\n`);
|
|
29
|
+
} catch {
|
|
30
|
+
// Nothing is left to report to.
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
name: 'stdout',
|
|
36
|
+
write(record) {
|
|
37
|
+
let buffer;
|
|
38
|
+
try {
|
|
39
|
+
buffer = Buffer.from(`${JSON.stringify(record)}\n`);
|
|
40
|
+
} catch {
|
|
41
|
+
reportFailure('SERIALIZE');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let offset = 0;
|
|
45
|
+
let retries = 0;
|
|
46
|
+
// EAGAIN and a zero-byte write share one bounded retry budget.
|
|
47
|
+
const retry = () => {
|
|
48
|
+
if (retries >= maxRetries) {
|
|
49
|
+
reportFailure('EAGAIN');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
retries += 1;
|
|
53
|
+
sleep(Math.min(retries, MAX_SLEEP_MS));
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
while (offset < buffer.length) {
|
|
57
|
+
let written;
|
|
58
|
+
try {
|
|
59
|
+
written = writeSync(fd, buffer, offset, buffer.length - offset);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error?.code !== 'EAGAIN') {
|
|
62
|
+
reportFailure(error?.code);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!retry()) return;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (written > 0) offset += written;
|
|
69
|
+
else if (!retry()) return;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createMemorySink({ install = true } = {}) {
|
|
76
|
+
const records = [];
|
|
77
|
+
const sink = {
|
|
78
|
+
name: 'memory',
|
|
79
|
+
records,
|
|
80
|
+
write(record) {
|
|
81
|
+
records.push(deepFreeze(JSON.parse(JSON.stringify(record))));
|
|
82
|
+
},
|
|
83
|
+
clear() {
|
|
84
|
+
records.length = 0;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
if (install) {
|
|
88
|
+
require('./logger-runtime').setSinks([sink]);
|
|
89
|
+
}
|
|
90
|
+
return sink;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { createStdoutSink, createMemorySink };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const { redactUrl } = require('./redact');
|
|
2
|
+
|
|
3
|
+
// Best-effort extraction of the logical event/processId/integrationId from a
|
|
4
|
+
// JSON message body: `data.*` first, then the top level. Used only for log
|
|
5
|
+
// correlation. Never throws.
|
|
6
|
+
function summarizeMessageBody(bodyStr) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(bodyStr);
|
|
9
|
+
return {
|
|
10
|
+
event: parsed?.event,
|
|
11
|
+
processId: parsed?.data?.processId ?? parsed?.processId,
|
|
12
|
+
integrationId: parsed?.data?.integrationId ?? parsed?.integrationId,
|
|
13
|
+
};
|
|
14
|
+
} catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function objectKeys(value) {
|
|
20
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
21
|
+
? Object.keys(value)
|
|
22
|
+
: [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function rawQueryKeys(rawQueryString) {
|
|
26
|
+
if (typeof rawQueryString !== 'string' || !rawQueryString) return [];
|
|
27
|
+
try {
|
|
28
|
+
return [...new URLSearchParams(rawQueryString).keys()];
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unique(list) {
|
|
35
|
+
return [...new Set(list)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The route template keeps ids out of the field: REST v1 `resource`, or the
|
|
39
|
+
// path part of an HTTP API v2 `routeKey` such as `GET /api/{id}`.
|
|
40
|
+
function routeTemplate(event, path) {
|
|
41
|
+
if (typeof event.resource === 'string' && event.resource) {
|
|
42
|
+
return event.resource;
|
|
43
|
+
}
|
|
44
|
+
if (typeof event.routeKey === 'string') {
|
|
45
|
+
const space = event.routeKey.indexOf(' ');
|
|
46
|
+
if (space > 0) return event.routeKey.slice(space + 1);
|
|
47
|
+
}
|
|
48
|
+
return path;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function summarizeHttp(event) {
|
|
52
|
+
const path = event.path || event.rawPath;
|
|
53
|
+
const summary = {
|
|
54
|
+
source: 'http',
|
|
55
|
+
method: event.httpMethod || event.requestContext?.http?.method,
|
|
56
|
+
path,
|
|
57
|
+
route: routeTemplate(event, path),
|
|
58
|
+
queryKeys: unique([
|
|
59
|
+
...objectKeys(event.queryStringParameters),
|
|
60
|
+
...objectKeys(event.multiValueQueryStringParameters),
|
|
61
|
+
...rawQueryKeys(event.rawQueryString),
|
|
62
|
+
]),
|
|
63
|
+
headerNames: unique(
|
|
64
|
+
[...objectKeys(event.headers), ...objectKeys(event.multiValueHeaders)].map(
|
|
65
|
+
(name) => name.toLowerCase()
|
|
66
|
+
)
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
if (typeof event.routeKey === 'string' && event.routeKey) {
|
|
70
|
+
summary.routeKey = event.routeKey;
|
|
71
|
+
}
|
|
72
|
+
return summary;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Best-effort extraction of correlation identifiers from a Lambda event.
|
|
76
|
+
// For SQS: messageIds plus the parsed event/processId/integrationId of each
|
|
77
|
+
// record body. For HTTP: method, route, query keys and header names. Never
|
|
78
|
+
// throws and never returns a body, a query value or a header value.
|
|
79
|
+
function summarizeLambdaEvent(event) {
|
|
80
|
+
try {
|
|
81
|
+
if (!event || typeof event !== 'object') return {};
|
|
82
|
+
if (Array.isArray(event.Records)) {
|
|
83
|
+
return {
|
|
84
|
+
source: 'sqs',
|
|
85
|
+
records: event.Records.map((r) => ({
|
|
86
|
+
messageId: r?.messageId,
|
|
87
|
+
receiveCount: r?.attributes?.ApproximateReceiveCount,
|
|
88
|
+
...summarizeMessageBody(r?.body),
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (event.httpMethod || event.requestContext?.http) {
|
|
93
|
+
return summarizeHttp(event);
|
|
94
|
+
}
|
|
95
|
+
return { source: 'other' };
|
|
96
|
+
} catch {
|
|
97
|
+
return { source: 'other' };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The bounded form that goes into the logger scope: no per-record array.
|
|
102
|
+
function toScopeInvocation(summary) {
|
|
103
|
+
if (!summary || typeof summary !== 'object' || !summary.source) {
|
|
104
|
+
return { source: 'other' };
|
|
105
|
+
}
|
|
106
|
+
if (summary.source === 'sqs') {
|
|
107
|
+
return {
|
|
108
|
+
source: 'sqs',
|
|
109
|
+
recordCount: Array.isArray(summary.records) ? summary.records.length : 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (summary.source === 'http') {
|
|
113
|
+
const invocation = {
|
|
114
|
+
source: 'http',
|
|
115
|
+
method: summary.method,
|
|
116
|
+
route: summary.route,
|
|
117
|
+
};
|
|
118
|
+
if (summary.routeKey) invocation.routeKey = summary.routeKey;
|
|
119
|
+
return invocation;
|
|
120
|
+
}
|
|
121
|
+
return { source: summary.source };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Per-request detail for the entry and failure records, never for the
|
|
125
|
+
// scope. A raw path can hold a token, so it goes through redactUrl.
|
|
126
|
+
function toRequestDetails(summary) {
|
|
127
|
+
if (!summary || summary.source !== 'http') return {};
|
|
128
|
+
return {
|
|
129
|
+
path: redactUrl(summary.path ?? ''),
|
|
130
|
+
queryKeys: summary.queryKeys ?? [],
|
|
131
|
+
headerNames: summary.headerNames ?? [],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The full redacted request summary (ADR-048 §6: it goes under `invocation`).
|
|
136
|
+
function toRequestInvocation(summary) {
|
|
137
|
+
return { ...toScopeInvocation(summary), ...toRequestDetails(summary) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The same shape from an express request, for the express error boundary.
|
|
141
|
+
function summarizeExpressRequest(req) {
|
|
142
|
+
try {
|
|
143
|
+
if (!req || typeof req !== 'object') return { source: 'http' };
|
|
144
|
+
return {
|
|
145
|
+
source: 'http',
|
|
146
|
+
method: req.method,
|
|
147
|
+
path: redactUrl(req.path ?? ''),
|
|
148
|
+
queryKeys: objectKeys(req.query),
|
|
149
|
+
headerNames: unique(objectKeys(req.headers).map((name) => name.toLowerCase())),
|
|
150
|
+
};
|
|
151
|
+
} catch {
|
|
152
|
+
return { source: 'http' };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
summarizeLambdaEvent,
|
|
158
|
+
summarizeMessageBody,
|
|
159
|
+
toScopeInvocation,
|
|
160
|
+
toRequestDetails,
|
|
161
|
+
toRequestInvocation,
|
|
162
|
+
summarizeExpressRequest,
|
|
163
|
+
};
|
package/modules/module.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { Delegate } = require('../core');
|
|
2
2
|
const _ = require('lodash');
|
|
3
|
-
const {
|
|
3
|
+
const { getLogger } = require('../logs');
|
|
4
4
|
const { ModuleConstants } = require('./ModuleConstants');
|
|
5
5
|
const {
|
|
6
6
|
createCredentialRepository,
|
|
@@ -32,6 +32,14 @@ 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 ?? 'unknown'}`).child(() => ({
|
|
36
|
+
entityId: this.entity?.id,
|
|
37
|
+
credentialId:
|
|
38
|
+
this.credential?.id ??
|
|
39
|
+
(typeof this.credential === 'string'
|
|
40
|
+
? this.credential
|
|
41
|
+
: undefined),
|
|
42
|
+
}));
|
|
35
43
|
this.modelName = this.definition.modelName;
|
|
36
44
|
this.apiClass = this.definition.API;
|
|
37
45
|
|
|
@@ -49,6 +57,7 @@ class Module extends Delegate {
|
|
|
49
57
|
const apiParams = {
|
|
50
58
|
...this.definition.env,
|
|
51
59
|
delegate: this,
|
|
60
|
+
logger: this.logger,
|
|
52
61
|
...(state ? { state } : {}),
|
|
53
62
|
...(this.credential?.data
|
|
54
63
|
? this.apiParamsFromCredential(this.credential.data)
|
|
@@ -100,7 +109,10 @@ class Module extends Delegate {
|
|
|
100
109
|
try {
|
|
101
110
|
if (await this.testAuthRequest(this.api)) validAuth = true;
|
|
102
111
|
} catch (e) {
|
|
103
|
-
|
|
112
|
+
this.logger.warn('testAuth failed', {
|
|
113
|
+
eventName: `${this.logger.name}.test_auth_failed`,
|
|
114
|
+
error: e,
|
|
115
|
+
});
|
|
104
116
|
}
|
|
105
117
|
return validAuth;
|
|
106
118
|
}
|
|
@@ -113,9 +125,9 @@ class Module extends Delegate {
|
|
|
113
125
|
const apiParams = this.apiParamsFromCredential(this.api);
|
|
114
126
|
|
|
115
127
|
if (!apiParams.refresh_token && this.api.isRefreshable) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
128
|
+
this.logger.warn('No refresh_token in apiParams', {
|
|
129
|
+
eventName: `${this.logger.name}.refresh_token_missing`,
|
|
130
|
+
});
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
Object.assign(credentialDetails.details, apiParams);
|
|
@@ -133,10 +145,10 @@ class Module extends Delegate {
|
|
|
133
145
|
moduleName: this.name,
|
|
134
146
|
});
|
|
135
147
|
} catch (err) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
148
|
+
this.logger.error('Failed to propagate CREDENTIAL_VALIDATED', {
|
|
149
|
+
eventName: `${this.logger.name}.credential_validated_propagation_failed`,
|
|
150
|
+
error: err,
|
|
151
|
+
});
|
|
140
152
|
}
|
|
141
153
|
}
|
|
142
154
|
}
|
|
@@ -177,12 +189,11 @@ class Module extends Delegate {
|
|
|
177
189
|
if (!this.credential.id) return;
|
|
178
190
|
|
|
179
191
|
if (diagnosticInfo) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
192
|
+
this.logger.warn('Credentials rejected', {
|
|
193
|
+
eventName: `${this.logger.name}.credentials_rejected`,
|
|
194
|
+
statusCode: diagnosticInfo.statusCode,
|
|
195
|
+
error: diagnosticInfo,
|
|
196
|
+
});
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
await this.credentialRepository.updateAuthenticationStatus(
|
|
@@ -216,16 +227,17 @@ class Module extends Delegate {
|
|
|
216
227
|
}),
|
|
217
228
|
});
|
|
218
229
|
} catch (err) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
);
|
|
230
|
+
this.logger.error('Failed to propagate CREDENTIAL_INVALIDATED', {
|
|
231
|
+
eventName: `${this.logger.name}.credential_invalidated_propagation_failed`,
|
|
232
|
+
error: err,
|
|
233
|
+
});
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
|
|
226
237
|
async deauthorize() {
|
|
227
238
|
//todo: Check if this is correct, we're instantiating a new api without params (credentials, tokens, etc...)
|
|
228
239
|
this.api = new this.apiClass();
|
|
240
|
+
this.api.logger = this.logger;
|
|
229
241
|
|
|
230
242
|
// Remove persisted credential (if any)
|
|
231
243
|
if (this.entity?.credential) {
|
|
@@ -234,6 +246,7 @@ class Module extends Delegate {
|
|
|
234
246
|
|
|
235
247
|
// Delete credential via repository
|
|
236
248
|
await this.credentialRepository.deleteCredentialById(credentialId);
|
|
249
|
+
this.credential = undefined;
|
|
237
250
|
|
|
238
251
|
// Unset credential reference on the Entity document
|
|
239
252
|
const entityId = this.entity.id;
|