@friggframework/core 2.0.0-next.112 → 2.0.0-next.114

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 (64) hide show
  1. package/.eslintrc.json +18 -1
  2. package/CLAUDE.md +51 -12
  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/encryption/README.md +5 -1
  12. package/database/encryption/encryption-schema-registry.js +3 -0
  13. package/database/encryption/logger.js +5 -2
  14. package/errors/fetch-error.js +58 -64
  15. package/handlers/app-definition-loader.js +38 -2
  16. package/handlers/app-handler-helpers.js +18 -4
  17. package/handlers/backend-utils.js +10 -13
  18. package/handlers/database-migration-handler.js +10 -1
  19. package/handlers/integration-event-dispatcher.js +2 -2
  20. package/handlers/routers/integration-webhook-routers.js +13 -6
  21. package/handlers/routers/websocket.js +27 -7
  22. package/handlers/workers/db-migration.js +12 -1
  23. package/handlers/workers/dlq-processor.js +64 -38
  24. package/index.js +19 -1
  25. package/integrations/integration-base.js +56 -38
  26. package/integrations/integration-router.js +21 -20
  27. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  28. package/jest.config.js +2 -0
  29. package/logs/.eslintrc.json +26 -1
  30. package/logs/__fixtures__/events.js +82 -0
  31. package/logs/__fixtures__/matchers.js +45 -0
  32. package/logs/__fixtures__/secrets.js +59 -0
  33. package/logs/__fixtures__/vectors.js +329 -0
  34. package/logs/__fixtures__/with-env.js +45 -0
  35. package/logs/context.js +109 -0
  36. package/logs/contract-keys.js +67 -0
  37. package/logs/debug-shims.js +46 -0
  38. package/logs/denied-keys.js +21 -0
  39. package/logs/index.js +14 -3
  40. package/logs/jest-logger-setup.js +25 -0
  41. package/logs/levels.js +71 -0
  42. package/logs/logger-runtime.js +137 -0
  43. package/logs/logger.js +134 -42
  44. package/logs/record.js +276 -0
  45. package/logs/redact.js +328 -0
  46. package/logs/serialize.js +329 -0
  47. package/logs/sinks.js +93 -0
  48. package/logs/summarize-event.js +163 -0
  49. package/modules/module.js +32 -19
  50. package/modules/requester/oauth-2.js +28 -22
  51. package/modules/requester/requester.js +69 -6
  52. package/package.json +5 -6
  53. package/queues/queue-delivery.js +41 -0
  54. package/queues/queuer-util.js +1 -15
  55. package/telemetry/bind-telemetry-context.js +4 -0
  56. package/telemetry/instrument-handler.js +19 -2
  57. package/telemetry/no-op-telemetry.js +4 -0
  58. package/telemetry/otel-telemetry.js +25 -6
  59. package/telemetry/telemetry-context.js +19 -8
  60. package/telemetry/telemetry-runtime.js +17 -0
  61. package/telemetry/telemetry-service.js +1 -0
  62. package/types/errors/index.d.ts +24 -10
  63. package/types/logs/index.d.ts +61 -2
  64. package/user/repositories/user-repository-documentdb.js +10 -42
package/logs/redact.js ADDED
@@ -0,0 +1,328 @@
1
+ const { CONTRACT_KEYS } = require('./contract-keys');
2
+
3
+ const DENIED_SUFFIXES = [
4
+ 'token',
5
+ 'secret',
6
+ 'password',
7
+ 'apikey',
8
+ 'privatekey',
9
+ 'signature',
10
+ 'authorization',
11
+ 'cookie',
12
+ 'pwd',
13
+ 'passphrase',
14
+ 'credentials',
15
+ 'sessionid',
16
+ ];
17
+
18
+ // Core encryption registry leaves and exact names the suffix rule does not
19
+ // cover. `header` is Node's raw `_header` request text. `domain` is not
20
+ // seeded (too common); a module can still register it as a credential field.
21
+ const deniedKeys = new Set([
22
+ 'hashword',
23
+ 'cookies',
24
+ 'apikeyvalue',
25
+ 'mapping',
26
+ 'auth',
27
+ 'header',
28
+ ]);
29
+
30
+ // Record-contract fields. A credential field with one of these names must not
31
+ // blank the field in every record.
32
+ const PROTECTED_KEYS = new Set(CONTRACT_KEYS.map(normalizeKey));
33
+
34
+ const DENIED_SUFFIX = new RegExp(`(?:${DENIED_SUFFIXES.join('|')})$`);
35
+ const PAIR_KEYS = new Set(['code', 'codeverifier']);
36
+ const DIGEST_KEY = /(sha(1|256)|hash|digest|checksum)$/;
37
+
38
+ const MAX_SCRUB_INPUT = 65536;
39
+
40
+ const URL_IN_TEXT = /\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"'<>`]+/g;
41
+ const URL_TRAILING_PUNCT = /[.,;:!?)\]}]+$/;
42
+ const ABSOLUTE_URL = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
43
+ const RAW_ABSOLUTE_PATH = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*([^?#]*)/;
44
+ const RAW_RELATIVE_PATH = /^[^?#]*/;
45
+ const PATH_TOKEN_SEGMENT = /^[A-Za-z0-9_.~-]{20,}$/;
46
+ const PREFIXED_TOKEN =
47
+ /(?<![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;
48
+ const JWT = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
49
+ const AUTH_SCHEME = /\b(Bearer|bearer|BEARER|Basic|basic|BASIC|Token|token|TOKEN)(\s+)([A-Za-z0-9._~+/=-]{8,})/g;
50
+ const AUTH_SCHEME_WORD = /^(bearer|basic|token)$/i;
51
+ const COLON_PAIR = /(?<![A-Za-z0-9_-])([A-Za-z_][A-Za-z0-9_.-]{0,99})(["']?)(\s*:\s*)(["']?)([^\s"',;{}]+)/g;
52
+ const COOKIE_LINE = /(?<![A-Za-z0-9_-])((?:set-)?cookie)(\s*:\s*)([^\r\n]+)/gi;
53
+ const CODE_PAIR = /(?<![A-Za-z0-9_.-])code=/;
54
+ const JSON_PAIR = /"([^"\\]{1,100})"(\s*:\s*)"((?:[^"\\]|\\.)*)"/g;
55
+ const TEXT_PAIR = /(?<![A-Za-z0-9_.-])([A-Za-z0-9_.-]{1,100})=([^\s&"'<>,;)]+)/g;
56
+ const HEX_RUN = /(?<![A-Za-z0-9])[0-9a-fA-F]{40,}(?![A-Za-z0-9])/g;
57
+ const BASE64_RUN = /(?<![A-Za-z0-9+/=_-])[A-Za-z0-9+/_-]{40,}={0,2}(?![A-Za-z0-9+/=_-])/g;
58
+ const FORM_TEXT = /^[A-Za-z0-9_.%[\]-]+=[^&\s]*(?:&[A-Za-z0-9_.%[\]-]+=[^&\s]*)+$/;
59
+ const PATH_SEGMENT = /^[A-Za-z0-9_-]*$/;
60
+
61
+ function redacted(value) {
62
+ return `[REDACTED:${value.length}]`;
63
+ }
64
+
65
+ function normalizeKey(key) {
66
+ if (typeof key !== 'string') {
67
+ if (typeof key !== 'number') return '';
68
+ key = String(key);
69
+ }
70
+ return key.toLowerCase().replace(/[-_\s]/g, '');
71
+ }
72
+
73
+ // The *Normalized variants take a normalizeKey() result, so a walk
74
+ // normalizes each key once.
75
+ function isDeniedNormalized(normalized) {
76
+ if (!normalized || PROTECTED_KEYS.has(normalized)) return false;
77
+ return deniedKeys.has(normalized) || DENIED_SUFFIX.test(normalized);
78
+ }
79
+
80
+ function isDeniedKey(key) {
81
+ return isDeniedNormalized(normalizeKey(key));
82
+ }
83
+
84
+ function isDigestNormalized(normalized) {
85
+ return DIGEST_KEY.test(normalized);
86
+ }
87
+
88
+ function isDigestKey(key) {
89
+ return isDigestNormalized(normalizeKey(key));
90
+ }
91
+
92
+ function isPairKey(key, { withState = false } = {}) {
93
+ const normalized = normalizeKey(key);
94
+ return (
95
+ isDeniedKey(key) ||
96
+ PAIR_KEYS.has(normalized) ||
97
+ (withState && normalized === 'state')
98
+ );
99
+ }
100
+
101
+ // Returns the leaves it skipped as record-contract keys, so a caller can warn.
102
+ function addDeniedKeys(keys) {
103
+ const ignored = [];
104
+ if (!Array.isArray(keys)) return ignored;
105
+ for (const key of keys) {
106
+ if (typeof key !== 'string') continue;
107
+ const rawLeaf = key.split('.').pop();
108
+ const leaf = normalizeKey(rawLeaf);
109
+ if (!leaf) continue;
110
+ if (PROTECTED_KEYS.has(leaf)) ignored.push(rawLeaf);
111
+ else deniedKeys.add(leaf);
112
+ }
113
+ return ignored;
114
+ }
115
+
116
+ function hasMixedClasses(run) {
117
+ return /[A-Z]/.test(run) && /[a-z]/.test(run) && /[0-9]/.test(run);
118
+ }
119
+
120
+ function isPathLike(run) {
121
+ if (!run.startsWith('/')) return false;
122
+ const segments = run.split('/');
123
+ if (segments.length < 3) return false;
124
+ return segments.every(
125
+ (segment) => segment.length < 40 && PATH_SEGMENT.test(segment)
126
+ );
127
+ }
128
+
129
+ function isRedactedValue(value) {
130
+ return value === 'REDACTED' || value.startsWith('[REDACTED');
131
+ }
132
+
133
+ function scrubTokens(text, { allowHex = false } = {}) {
134
+ let out = text.replace(PREFIXED_TOKEN, redacted);
135
+ out = out.replace(JWT, redacted);
136
+ out = out.replace(AUTH_SCHEME, (match, scheme, space, credential) => {
137
+ const classes = [/[A-Z]/, /[a-z]/, /[0-9]/, /[._~+/=-]/].filter((re) =>
138
+ re.test(credential)
139
+ ).length;
140
+ return classes >= 2 ? `${scheme}${space}${redacted(credential)}` : match;
141
+ });
142
+ out = out.replace(JSON_PAIR, (match, key, colon, value) =>
143
+ isDeniedKey(key) || normalizeKey(key) === 'codeverifier'
144
+ ? `"${key}"${colon}"${redacted(value)}"`
145
+ : match
146
+ );
147
+ out = out.replace(COOKIE_LINE, (match, key, colon, value) =>
148
+ isRedactedValue(value) ? match : `${key}${colon}${redacted(value)}`
149
+ );
150
+ out = out.replace(COLON_PAIR, (match, key, close, colon, open, value) =>
151
+ isDeniedKey(key) &&
152
+ !isRedactedValue(value) &&
153
+ !AUTH_SCHEME_WORD.test(value)
154
+ ? `${key}${close}${colon}${open}${redacted(value)}`
155
+ : match
156
+ );
157
+ const withState = CODE_PAIR.test(out);
158
+ out = out.replace(TEXT_PAIR, (match, key, value) =>
159
+ isPairKey(key, { withState }) && !isRedactedValue(value)
160
+ ? `${key}=${redacted(value)}`
161
+ : match
162
+ );
163
+ if (!allowHex) out = out.replace(HEX_RUN, redacted);
164
+ out = out.replace(BASE64_RUN, (run) =>
165
+ hasMixedClasses(run) && !isPathLike(run)
166
+ ? redacted(run)
167
+ : run
168
+ );
169
+ return out;
170
+ }
171
+
172
+ function queryKeys(search) {
173
+ return search
174
+ .replace(/^[?#]/, '')
175
+ .split('&')
176
+ .map((pair) => pair.split('=')[0])
177
+ .filter(Boolean);
178
+ }
179
+
180
+ function redactParams(search, prefix) {
181
+ const keys = queryKeys(search);
182
+ if (keys.length === 0) return '';
183
+ return `${prefix}${keys.map((key) => `${key}=REDACTED`).join('&')}`;
184
+ }
185
+
186
+ function redactHash(hash) {
187
+ if (!hash) return '';
188
+ if (hash.includes('=')) return redactParams(hash, '#');
189
+ return scrubTokens(hash);
190
+ }
191
+
192
+ // A long segment mixing upper case, lower case and digits is an opaque
193
+ // token (webhook secrets live in paths); hex ids and UUIDs lack one class.
194
+ function redactPathSegment(segment) {
195
+ if (PATH_TOKEN_SEGMENT.test(segment) && hasMixedClasses(segment)) {
196
+ return redacted(segment);
197
+ }
198
+ return scrubTokens(segment);
199
+ }
200
+
201
+ // The raw path, not URL#pathname, which percent-encodes `{proxy+}`.
202
+ function redactPath(pathname) {
203
+ return pathname.split('/').map(redactPathSegment).join('/');
204
+ }
205
+
206
+ function redactUrlFallback(raw) {
207
+ const out = raw
208
+ .replace(/(:\/\/)[^/?#@\s]*@/, '$1')
209
+ .replace(/([?&#][^=&#\s]+)=[^&#\s]*/g, '$1=REDACTED');
210
+ return scrubTokens(out);
211
+ }
212
+
213
+ function redactUrl(url) {
214
+ if (url === undefined || url === null) return '';
215
+ let raw;
216
+ try {
217
+ raw = url instanceof URL ? url.href : String(url);
218
+ } catch {
219
+ return '';
220
+ }
221
+ try {
222
+ if (ABSOLUTE_URL.test(raw)) {
223
+ const u = new URL(raw);
224
+ const path = raw.match(RAW_ABSOLUTE_PATH)[1];
225
+ return `${u.protocol}//${u.host}${redactPath(path)}${redactParams(
226
+ u.search,
227
+ '?'
228
+ )}${redactHash(u.hash)}`;
229
+ }
230
+ if (raw.startsWith('/')) {
231
+ const u = new URL(raw, 'https://relative.invalid');
232
+ return `${redactPath(raw.match(RAW_RELATIVE_PATH)[0])}${redactParams(
233
+ u.search,
234
+ '?'
235
+ )}${redactHash(u.hash)}`;
236
+ }
237
+ } catch {
238
+ return redactUrlFallback(raw);
239
+ }
240
+ return redactUrlFallback(raw);
241
+ }
242
+
243
+ function looksLikeJson(trimmed) {
244
+ if (trimmed.startsWith('{')) return trimmed.endsWith('}');
245
+ return /^\[\s*[{["\]\d-]/.test(trimmed) && trimmed.endsWith(']');
246
+ }
247
+
248
+ function scrubJsonText(text, originalLength) {
249
+ try {
250
+ return JSON.stringify(redactValue(JSON.parse(text)));
251
+ } catch {
252
+ return `[unparsed:${originalLength}]`;
253
+ }
254
+ }
255
+
256
+ function scrubFormText(text) {
257
+ const pairs = text.split('&').map((pair) => {
258
+ const index = pair.indexOf('=');
259
+ return [pair.slice(0, index), pair.slice(index + 1)];
260
+ });
261
+ const decodedKeys = pairs.map(([key]) => {
262
+ try {
263
+ return decodeURIComponent(key);
264
+ } catch {
265
+ return key;
266
+ }
267
+ });
268
+ const dropAll = isOAuthCallbackShape(decodedKeys);
269
+ return pairs
270
+ .map(([key, value], i) => {
271
+ if (dropAll || isPairKey(decodedKeys[i])) {
272
+ return `${key}=${redacted(value)}`;
273
+ }
274
+ return `${key}=${scrubTokens(value)}`;
275
+ })
276
+ .join('&');
277
+ }
278
+
279
+ function scrubText(text, options) {
280
+ const withUrls = text.replace(URL_IN_TEXT, (match) => {
281
+ const trailing = match.match(URL_TRAILING_PUNCT)?.[0] ?? '';
282
+ const core = trailing ? match.slice(0, -trailing.length) : match;
283
+ return `${redactUrl(core)}${trailing}`;
284
+ });
285
+ return scrubTokens(withUrls, options);
286
+ }
287
+
288
+ function scrubString(str, { allowHex = false } = {}) {
289
+ if (typeof str !== 'string' || str.length === 0) return str;
290
+ try {
291
+ const text =
292
+ str.length > MAX_SCRUB_INPUT ? str.slice(0, MAX_SCRUB_INPUT) : str;
293
+ const trimmed = text.trim();
294
+ if (looksLikeJson(trimmed)) return scrubJsonText(trimmed, str.length);
295
+ if (FORM_TEXT.test(trimmed)) return scrubFormText(trimmed);
296
+ return scrubText(text, { allowHex });
297
+ } catch {
298
+ return redacted(str);
299
+ }
300
+ }
301
+
302
+ function isOAuthCallbackShapeNormalized(normalizedKeys) {
303
+ const keys = new Set(normalizedKeys);
304
+ return keys.has('code') && (keys.has('state') || keys.has('codeverifier'));
305
+ }
306
+
307
+ function isOAuthCallbackShape(keys) {
308
+ return isOAuthCallbackShapeNormalized(keys.map(normalizeKey));
309
+ }
310
+
311
+ function redactValue(value) {
312
+ const { serializeValue } = require('./serialize');
313
+ return serializeValue(value);
314
+ }
315
+
316
+ module.exports = {
317
+ normalizeKey,
318
+ isDeniedKey,
319
+ isDeniedNormalized,
320
+ isDigestKey,
321
+ isDigestNormalized,
322
+ isOAuthCallbackShape,
323
+ isOAuthCallbackShapeNormalized,
324
+ addDeniedKeys,
325
+ redactUrl,
326
+ scrubString,
327
+ redactValue,
328
+ };
@@ -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
+ };