@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.
Files changed (74) hide show
  1. package/.eslintrc.json +18 -1
  2. package/CLAUDE.md +101 -13
  3. package/README.md +19 -22
  4. package/core/CLAUDE.md +28 -13
  5. package/core/Worker.js +32 -37
  6. package/core/create-handler.js +97 -197
  7. package/core/index.js +8 -1
  8. package/core/invocation-scope.js +225 -0
  9. package/core/secrets-to-env.js +7 -6
  10. package/database/config.js +0 -7
  11. package/database/documentdb-encryption-service.js +6 -2
  12. package/database/documentdb-utils.js +2 -1
  13. package/database/encryption/README.md +25 -1
  14. package/database/encryption/encryption-schema-registry.js +3 -0
  15. package/database/encryption/integration-mapping-encryption.js +97 -0
  16. package/database/encryption/logger.js +5 -2
  17. package/database/encryption/prisma-encryption-extension.js +20 -9
  18. package/errors/fetch-error.js +48 -65
  19. package/handlers/app-definition-loader.js +38 -2
  20. package/handlers/app-handler-helpers.js +18 -4
  21. package/handlers/backend-utils.js +10 -13
  22. package/handlers/database-migration-handler.js +10 -1
  23. package/handlers/integration-event-dispatcher.js +2 -2
  24. package/handlers/routers/integration-webhook-routers.js +13 -6
  25. package/handlers/routers/websocket.js +27 -7
  26. package/handlers/workers/db-migration.js +12 -1
  27. package/handlers/workers/dlq-processor.js +64 -38
  28. package/index.js +19 -1
  29. package/integrations/integration-base.js +56 -38
  30. package/integrations/integration-router.js +21 -20
  31. package/integrations/repositories/integration-mapping-query-pipeline.js +152 -0
  32. package/integrations/repositories/integration-mapping-query.js +193 -0
  33. package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
  34. package/integrations/repositories/integration-mapping-repository-interface.js +48 -0
  35. package/integrations/repositories/integration-mapping-repository-mongo.js +72 -0
  36. package/integrations/repositories/integration-mapping-repository-postgres.js +118 -0
  37. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  38. package/jest.config.js +2 -0
  39. package/logs/.eslintrc.json +26 -1
  40. package/logs/__fixtures__/events.js +82 -0
  41. package/logs/__fixtures__/matchers.js +45 -0
  42. package/logs/__fixtures__/secrets.js +59 -0
  43. package/logs/__fixtures__/vectors.js +329 -0
  44. package/logs/__fixtures__/with-env.js +45 -0
  45. package/logs/context.js +109 -0
  46. package/logs/contract-keys.js +67 -0
  47. package/logs/debug-shims.js +46 -0
  48. package/logs/denied-keys.js +21 -0
  49. package/logs/index.js +14 -3
  50. package/logs/jest-logger-setup.js +25 -0
  51. package/logs/levels.js +71 -0
  52. package/logs/logger-runtime.js +137 -0
  53. package/logs/logger.js +134 -42
  54. package/logs/record.js +276 -0
  55. package/logs/redact.js +328 -0
  56. package/logs/serialize.js +329 -0
  57. package/logs/sinks.js +93 -0
  58. package/logs/summarize-event.js +163 -0
  59. package/modules/module.js +32 -19
  60. package/modules/requester/oauth-2.js +28 -22
  61. package/modules/requester/requester.js +69 -6
  62. package/package.json +5 -6
  63. package/queues/queue-delivery.js +41 -0
  64. package/queues/queuer-util.js +1 -15
  65. package/telemetry/bind-telemetry-context.js +4 -0
  66. package/telemetry/instrument-handler.js +19 -2
  67. package/telemetry/no-op-telemetry.js +4 -0
  68. package/telemetry/otel-telemetry.js +25 -6
  69. package/telemetry/telemetry-context.js +19 -8
  70. package/telemetry/telemetry-runtime.js +17 -0
  71. package/telemetry/telemetry-service.js +1 -0
  72. package/types/errors/index.d.ts +24 -10
  73. package/types/logs/index.d.ts +61 -2
  74. 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 { flushDebugLog } = require('../logs');
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
- flushDebugLog(e);
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
- console.warn(
117
- `[Frigg] No refresh_token in apiParams for module ${this.name}.`
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
- console.error(
137
- `[Frigg] Failed to propagate CREDENTIAL_VALIDATED for module ${this.name}:`,
138
- err?.message || err
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
- console.error(
181
- `[Frigg] Module ${this.name} credentials rejected (status ${
182
- diagnosticInfo.statusCode ?? '?'
183
- }):`,
184
- diagnosticInfo.message ?? diagnosticInfo
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
- console.error(
220
- `[Frigg] Failed to propagate CREDENTIAL_INVALIDATED for module ${this.name}:`,
221
- err?.message || err
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;