@slates/provider 1.0.0-rc.1 → 1.0.0-rc.11

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.
@@ -0,0 +1,776 @@
1
+ import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
2
+ import type { SlateContext } from '../context';
3
+
4
+ export interface SlateHttpTraceTextBody {
5
+ contentType?: string;
6
+ text: string;
7
+ truncated?: boolean;
8
+ }
9
+
10
+ export interface SlateHttpTrace {
11
+ startedAt: string;
12
+ durationMs: number;
13
+ request: {
14
+ method: string;
15
+ url: string;
16
+ headers?: Record<string, string>;
17
+ body?: SlateHttpTraceTextBody;
18
+ };
19
+ response?: {
20
+ status: number;
21
+ statusText?: string;
22
+ headers?: Record<string, string>;
23
+ body?: SlateHttpTraceTextBody;
24
+ };
25
+ error?: {
26
+ code?: string;
27
+ message: string;
28
+ };
29
+ }
30
+
31
+ interface SlateHttpTraceDraft {
32
+ context: SlateContext<any, any, any>;
33
+ startedAt: string;
34
+ startedAtMs: number;
35
+ request: SlateHttpTrace['request'];
36
+ }
37
+
38
+ type TraceAwareAxiosRequestConfig = InternalAxiosRequestConfig & {
39
+ __slatesHttpTraceDraft?: SlateHttpTraceDraft;
40
+ };
41
+
42
+ let traceDraftsByConfig = new WeakMap<object, SlateHttpTraceDraft>();
43
+ let traceDraftsByHeaders = new WeakMap<object, SlateHttpTraceDraft>();
44
+
45
+ let TRACE_TEXT_LIMIT = 10 * 1024;
46
+ let REDACTED_VALUE = '[redacted]';
47
+ let STRUCTURED_DEPTH_LIMIT = 6;
48
+ let STRUCTURED_ENTRY_LIMIT = 50;
49
+
50
+ // Atoms that identify a header/field/param name as containing a secret.
51
+ // These are matched with token boundaries so `sig` flags `x-sig-key` but NOT `configure`,
52
+ // and `code` only flags `code`/`auth_code`/etc., never `encode`/`decoder`.
53
+ // `[-_.]?` allows compound atoms to match either with or without a separator
54
+ // (e.g. `apikey`, `api-key`, `api_key`, `api.key`).
55
+ let CORE_SECRET_KEY_ATOMS = [
56
+ // HTTP authentication
57
+ 'authorization',
58
+ 'proxy[-_.]?authorization',
59
+ 'www[-_.]?authenticate',
60
+ 'bearer',
61
+
62
+ // Generic tokens
63
+ 'token',
64
+ 'tokens',
65
+ 'access[-_.]?token',
66
+ 'refresh[-_.]?token',
67
+ 'id[-_.]?token',
68
+ 'auth[-_.]?token',
69
+ 'bearer[-_.]?token',
70
+ 'session[-_.]?token',
71
+ 'api[-_.]?token',
72
+ 'app[-_.]?token',
73
+ 'user[-_.]?token',
74
+ 'oauth[-_.]?token',
75
+ 'personal[-_.]?access[-_.]?token',
76
+ 'x[-_.]?auth[-_.]?token',
77
+ 'x[-_.]?access[-_.]?token',
78
+ 'x[-_.]?refresh[-_.]?token',
79
+
80
+ // API keys
81
+ 'apikey',
82
+ 'api[-_.]?key',
83
+ 'app[-_.]?key',
84
+ 'x[-_.]?api[-_.]?key',
85
+ 'x[-_.]?app[-_.]?key',
86
+
87
+ // OAuth client credentials
88
+ 'client[-_.]?secret',
89
+ 'client[-_.]?key',
90
+ 'consumer[-_.]?secret',
91
+ 'consumer[-_.]?key',
92
+
93
+ // Cryptographic keys
94
+ 'private[-_.]?key',
95
+ 'privatekey',
96
+ 'secret[-_.]?key',
97
+ 'secretkey',
98
+ 'secret[-_.]?access[-_.]?key',
99
+ 'encryption[-_.]?key',
100
+ 'decryption[-_.]?key',
101
+ 'signing[-_.]?key',
102
+ 'signing[-_.]?secret',
103
+ 'master[-_.]?key',
104
+ 'master[-_.]?secret',
105
+ 'master[-_.]?password',
106
+ 'shared[-_.]?secret',
107
+ 'webhook[-_.]?secret',
108
+ 'webhook[-_.]?signing[-_.]?secret',
109
+
110
+ // OAuth codes & PKCE
111
+ 'auth[-_.]?code',
112
+ 'authcode',
113
+ 'authorization[-_.]?code',
114
+ 'code[-_.]?verifier',
115
+ 'code[-_.]?challenge',
116
+
117
+ // Passwords & passcodes
118
+ 'password',
119
+ 'passwd',
120
+ 'pwd',
121
+ 'passphrase',
122
+ 'pass[-_.]?phrase',
123
+ 'pin',
124
+ 'passcode',
125
+
126
+ // Generic secret markers
127
+ 'secret',
128
+ 'secrets',
129
+ 'credential',
130
+ 'credentials',
131
+ 'creds',
132
+ 'auth',
133
+ 'x[-_.]?auth',
134
+ 'x[-_.]?auth[-_.]?key',
135
+
136
+ // CSRF / XSRF
137
+ 'csrf',
138
+ 'xsrf',
139
+ 'csrf[-_.]?token',
140
+ 'xsrf[-_.]?token',
141
+ 'x[-_.]?csrf[-_.]?token',
142
+ 'x[-_.]?xsrf[-_.]?token',
143
+
144
+ // JWT
145
+ 'jwt',
146
+
147
+ // Sessions & cookies
148
+ 'session[-_.]?id',
149
+ 'sessionid',
150
+ 'sid',
151
+ 'cookie',
152
+ 'cookies',
153
+ 'set[-_.]?cookie',
154
+
155
+ // Signatures / MAC
156
+ 'signature',
157
+ 'x[-_.]?hub[-_.]?signature',
158
+ 'x[-_.]?signature',
159
+ 'sig',
160
+ 'hmac',
161
+ 'mac',
162
+
163
+ // Crypto artifacts
164
+ 'salt',
165
+ 'nonce',
166
+
167
+ // Multi-factor / verification
168
+ 'otp',
169
+ 'totp',
170
+ 'mfa',
171
+ 'mfa[-_.]?code',
172
+ '2fa',
173
+ 'two[-_.]?factor',
174
+ 'twofactor',
175
+ 'verification[-_.]?code',
176
+ 'verify[-_.]?code',
177
+ 'confirm(?:ation)?[-_.]?code',
178
+ 'sms[-_.]?code',
179
+ 'email[-_.]?code',
180
+
181
+ // Recovery & seed phrases
182
+ 'seed[-_.]?phrase',
183
+ 'mnemonic',
184
+ 'recovery[-_.]?key',
185
+ 'recovery[-_.]?code',
186
+ 'recovery[-_.]?phrase',
187
+
188
+ // PII / financial
189
+ 'card[-_.]?number',
190
+ 'card[-_.]?code',
191
+ 'cvv',
192
+ 'cvc',
193
+ 'ccv',
194
+ 'cc[-_.]?num(?:ber)?',
195
+ 'pan',
196
+ 'ssn',
197
+ 'social[-_.]?security',
198
+ 'license[-_.]?key'
199
+ ];
200
+
201
+ // Extra atoms that are considered secret-like specifically in URL query strings
202
+ // and x-www-form-urlencoded bodies, where a lone `code` typically carries an OAuth
203
+ // authorization code. We keep `code` out of CORE to avoid redacting `{"code": 404}`
204
+ // in structured JSON responses.
205
+ let URL_FORM_SECRET_KEY_ATOMS = [...CORE_SECRET_KEY_ATOMS, 'code'];
206
+
207
+ let buildKeyBoundaryPattern = (atoms: string[], flags: string) =>
208
+ new RegExp(
209
+ `(?:^|[^A-Za-z0-9])(?:${atoms.join('|')})(?=[^A-Za-z0-9]|$)`,
210
+ flags
211
+ );
212
+
213
+ let SECRET_KEY_PATTERN = buildKeyBoundaryPattern(CORE_SECRET_KEY_ATOMS, 'i');
214
+ let URL_FORM_SECRET_KEY_PATTERN = buildKeyBoundaryPattern(
215
+ URL_FORM_SECRET_KEY_ATOMS,
216
+ 'i'
217
+ );
218
+
219
+ // Explicit non-secret keys that would otherwise trip the patterns above because
220
+ // they embed a secret-sounding token (e.g. `token_type` contains `token`).
221
+ // Keys here describe the *kind* of a secret, not the secret itself.
222
+ let NON_SECRET_KEY_EXACT = new Set<string>([
223
+ 'token_type',
224
+ 'token-type',
225
+ 'tokentype',
226
+ 'token_kind',
227
+ 'token-kind',
228
+ 'tokenkind',
229
+ 'token_ttl',
230
+ 'token-ttl',
231
+ 'token_expires_in',
232
+ 'token-expires-in',
233
+ 'token_expiry',
234
+ 'signature_method',
235
+ 'signature-method',
236
+ 'signature_type',
237
+ 'signature-type',
238
+ 'signature_version',
239
+ 'signature-version',
240
+ 'auth_type',
241
+ 'auth-type',
242
+ 'auth_method',
243
+ 'auth-method',
244
+ 'authentication_type',
245
+ 'authentication-type',
246
+ 'authentication_method',
247
+ 'authentication-method',
248
+ 'grant_type',
249
+ 'grant-type',
250
+ 'password_strength',
251
+ 'password-strength',
252
+ 'password_policy',
253
+ 'password-policy',
254
+ 'key_type',
255
+ 'key-type',
256
+ 'key_name',
257
+ 'key-name',
258
+ 'key_id',
259
+ 'key-id',
260
+ 'kid'
261
+ ]);
262
+
263
+ let isSecretKeyName = (name: string): boolean => {
264
+ let normalized = name.toLowerCase();
265
+ if (NON_SECRET_KEY_EXACT.has(normalized)) return false;
266
+ return SECRET_KEY_PATTERN.test(normalized);
267
+ };
268
+
269
+ let isUrlFormSecretKeyName = (name: string): boolean => {
270
+ let normalized = name.toLowerCase();
271
+ if (NON_SECRET_KEY_EXACT.has(normalized)) return false;
272
+ return URL_FORM_SECRET_KEY_PATTERN.test(normalized);
273
+ };
274
+
275
+ // Patterns that identify well-known secret values regardless of the surrounding key.
276
+ // Keeps low false-positive risk by matching formats that are unlikely to occur otherwise.
277
+ let KNOWN_SECRET_VALUE_PATTERNS: RegExp[] = [
278
+ // JSON Web Tokens (header.payload.signature)
279
+ /\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/g,
280
+ // AWS access key IDs
281
+ /\b(?:AKIA|ASIA|AGPA|AIDA|ANPA|ANVA|AROA|APKA|ASCA)[A-Z0-9]{16}\b/g,
282
+ // GitHub tokens (PAT / OAuth / server-to-server / user-to-server / refresh)
283
+ /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g,
284
+ // Slack tokens
285
+ /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g,
286
+ // Stripe secret / public / restricted keys
287
+ /\b(?:sk|rk|pk)_(?:test|live)_[A-Za-z0-9]{16,}\b/g,
288
+ // OpenAI API keys
289
+ /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g,
290
+ // Anthropic API keys
291
+ /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
292
+ // Google API keys
293
+ /\bAIza[A-Za-z0-9_-]{35}\b/g,
294
+ // SendGrid API keys
295
+ /\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g,
296
+ // Twilio API keys
297
+ /\bSK[0-9a-fA-F]{32}\b/g,
298
+ // PEM-encoded private keys (multi-line)
299
+ /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g,
300
+ // SSH private keys (OpenSSH format)
301
+ /-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]*?-----END OPENSSH PRIVATE KEY-----/g
302
+ ];
303
+
304
+ // HTTP authentication schemes in free text, e.g. "Authorization: Bearer eyJ..."
305
+ let HTTP_AUTH_SCHEME_PATTERN =
306
+ /\b(bearer|basic|digest|token|negotiate|apikey)\s+([A-Za-z0-9_\-.~+/=]{4,})/gi;
307
+
308
+ // Generic `key=value` scan; the key is filtered via isUrlFormSecretKeyName at replace time
309
+ // so we can apply a deny-list for compound keys like `token_type` or `grant_type`.
310
+ let GENERIC_KEY_VALUE_PATTERN = /(?<![A-Za-z0-9])([A-Za-z0-9][A-Za-z0-9_\-.]*)(\s*=\s*)([^&#\s;]+)/g;
311
+
312
+ // Generic `"key": "value"` scan; the key is filtered via isSecretKeyName at replace time.
313
+ let GENERIC_JSON_KEY_VALUE_PATTERN =
314
+ /"([^"\\]*(?:\\.[^"\\]*)*)"(\s*:\s*)"([^"\\]*(?:\\.[^"\\]*)*)"/g;
315
+
316
+ let SAFE_HEADER_NAMES = new Set([
317
+ 'accept',
318
+ 'accept-encoding',
319
+ 'accept-language',
320
+ 'cache-control',
321
+ 'connection',
322
+ 'content-encoding',
323
+ 'content-language',
324
+ 'content-length',
325
+ 'content-type',
326
+ 'date',
327
+ 'etag',
328
+ 'expires',
329
+ 'if-modified-since',
330
+ 'if-none-match',
331
+ 'keep-alive',
332
+ 'last-modified',
333
+ 'location',
334
+ 'origin',
335
+ 'pragma',
336
+ 'referer',
337
+ 'referrer-policy',
338
+ 'request-id',
339
+ 'retry-after',
340
+ 'server',
341
+ 'transfer-encoding',
342
+ 'user-agent',
343
+ 'vary',
344
+ 'via',
345
+ 'x-correlation-id',
346
+ 'x-powered-by',
347
+ 'x-request-id',
348
+ 'x-slates-provider',
349
+ 'x-trace-id'
350
+ ]);
351
+
352
+ let SAFE_HEADER_PREFIXES = ['ratelimit-', 'x-ratelimit-'];
353
+
354
+ let isRecord = (value: unknown): value is Record<string, unknown> =>
355
+ typeof value === 'object' && value !== null && !Array.isArray(value);
356
+
357
+ let isObjectLike = (value: unknown): value is object =>
358
+ typeof value === 'object' && value !== null;
359
+
360
+ let normalizeContentType = (value: string | undefined) =>
361
+ value?.split(';')[0]?.trim().toLowerCase() || undefined;
362
+
363
+ let isTextContentType = (contentType?: string) =>
364
+ !contentType ||
365
+ /(^text\/)|json|xml|html|javascript|graphql|x-www-form-urlencoded|svg/.test(contentType);
366
+
367
+ let isSafeHeaderName = (name: string) => {
368
+ let normalized = name.toLowerCase();
369
+ if (isSecretKeyName(normalized)) return false;
370
+ if (SAFE_HEADER_NAMES.has(normalized)) return true;
371
+ return SAFE_HEADER_PREFIXES.some(prefix => normalized.startsWith(prefix));
372
+ };
373
+
374
+ let truncateText = (text: string) => {
375
+ if (text.length <= TRACE_TEXT_LIMIT) {
376
+ return { text, truncated: false };
377
+ }
378
+
379
+ return {
380
+ text: `${text.slice(0, TRACE_TEXT_LIMIT)}...[truncated]`,
381
+ truncated: true
382
+ };
383
+ };
384
+
385
+ let sanitizeFreeText = (text: string) => {
386
+ if (!text) return text;
387
+
388
+ let result = text;
389
+
390
+ result = result.replace(
391
+ HTTP_AUTH_SCHEME_PATTERN,
392
+ (_match, scheme: string) => `${scheme} ${REDACTED_VALUE}`
393
+ );
394
+
395
+ for (let pattern of KNOWN_SECRET_VALUE_PATTERNS) {
396
+ result = result.replace(pattern, REDACTED_VALUE);
397
+ }
398
+
399
+ result = result.replace(
400
+ GENERIC_KEY_VALUE_PATTERN,
401
+ (match, key: string, equals: string, _value: string) =>
402
+ isUrlFormSecretKeyName(key) ? `${key}${equals}${REDACTED_VALUE}` : match
403
+ );
404
+
405
+ result = result.replace(
406
+ GENERIC_JSON_KEY_VALUE_PATTERN,
407
+ (match, key: string, colon: string, _value: string) =>
408
+ isSecretKeyName(key) ? `"${key}"${colon}"${REDACTED_VALUE}"` : match
409
+ );
410
+
411
+ return result;
412
+ };
413
+
414
+ let sanitizeScalar = (value: string) => truncateText(sanitizeFreeText(value));
415
+
416
+ let redactStructuredValue = (value: unknown, depth = 0): unknown => {
417
+ if (value == null) return value;
418
+ if (depth >= STRUCTURED_DEPTH_LIMIT) return '[truncated]';
419
+
420
+ if (
421
+ typeof value === 'string' ||
422
+ typeof value === 'number' ||
423
+ typeof value === 'boolean' ||
424
+ typeof value === 'bigint'
425
+ ) {
426
+ return typeof value === 'string' ? sanitizeFreeText(value) : value;
427
+ }
428
+
429
+ if (Array.isArray(value)) {
430
+ return value
431
+ .slice(0, STRUCTURED_ENTRY_LIMIT)
432
+ .map(entry => redactStructuredValue(entry, depth + 1));
433
+ }
434
+
435
+ if (isRecord(value)) {
436
+ return Object.fromEntries(
437
+ Object.entries(value)
438
+ .slice(0, STRUCTURED_ENTRY_LIMIT)
439
+ .map(([key, entry]) => [
440
+ key,
441
+ isSecretKeyName(key)
442
+ ? REDACTED_VALUE
443
+ : redactStructuredValue(entry, depth + 1)
444
+ ])
445
+ );
446
+ }
447
+
448
+ return String(value);
449
+ };
450
+
451
+ let maybeParseJson = (text: string, contentType?: string) => {
452
+ let trimmed = text.trim();
453
+ if (!trimmed) return null;
454
+ if (!contentType?.includes('json') && !/^[\[{]/.test(trimmed)) {
455
+ return null;
456
+ }
457
+
458
+ try {
459
+ return JSON.parse(trimmed);
460
+ } catch {
461
+ return null;
462
+ }
463
+ };
464
+
465
+ let sanitizeTextBody = (
466
+ text: string,
467
+ contentType?: string
468
+ ): SlateHttpTraceTextBody | undefined => {
469
+ if (!text) return undefined;
470
+
471
+ let normalizedContentType = normalizeContentType(contentType);
472
+ let parsedJson = maybeParseJson(text, normalizedContentType);
473
+ let normalizedText =
474
+ parsedJson !== null
475
+ ? JSON.stringify(redactStructuredValue(parsedJson))
476
+ : sanitizeFreeText(text);
477
+ let { text: truncatedText, truncated } = truncateText(normalizedText);
478
+
479
+ return {
480
+ ...(normalizedContentType ? { contentType: normalizedContentType } : {}),
481
+ text: truncatedText,
482
+ ...(truncated ? { truncated: true } : {})
483
+ };
484
+ };
485
+
486
+ let isBinaryLike = (value: unknown) => {
487
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return true;
488
+ if (value instanceof ArrayBuffer) return true;
489
+ if (ArrayBuffer.isView(value)) return true;
490
+ if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
491
+ if (typeof FormData !== 'undefined' && value instanceof FormData) return true;
492
+ return false;
493
+ };
494
+
495
+ let serializeTextBody = (
496
+ value: unknown,
497
+ contentType?: string
498
+ ): SlateHttpTraceTextBody | undefined => {
499
+ let normalizedContentType = normalizeContentType(contentType);
500
+
501
+ if (value == null || isBinaryLike(value) || !isTextContentType(normalizedContentType)) {
502
+ return undefined;
503
+ }
504
+
505
+ if (typeof value === 'string') {
506
+ return sanitizeTextBody(value, normalizedContentType);
507
+ }
508
+
509
+ if (value instanceof URLSearchParams) {
510
+ return sanitizeTextBody(value.toString(), normalizedContentType);
511
+ }
512
+
513
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
514
+ return sanitizeTextBody(String(value), normalizedContentType);
515
+ }
516
+
517
+ if (Array.isArray(value) || isRecord(value)) {
518
+ return sanitizeTextBody(JSON.stringify(redactStructuredValue(value)), 'application/json');
519
+ }
520
+
521
+ return undefined;
522
+ };
523
+
524
+ let buildUrl = (baseURL: string | undefined, url: string | undefined) => {
525
+ if (!url) return '';
526
+ if (!baseURL) return url;
527
+
528
+ try {
529
+ return new URL(url, baseURL).toString();
530
+ } catch {
531
+ return url;
532
+ }
533
+ };
534
+
535
+ // URLSearchParams percent-encodes `[` and `]`, which makes `[redacted]` render as
536
+ // `%5Bredacted%5D`. Undo that specific encoding so traces stay readable.
537
+ let REDACTED_ENCODED = encodeURIComponent(REDACTED_VALUE);
538
+ let decodeRedactedMarker = (value: string) =>
539
+ REDACTED_ENCODED === REDACTED_VALUE
540
+ ? value
541
+ : value.split(REDACTED_ENCODED).join(REDACTED_VALUE);
542
+
543
+ let redactHashFragment = (hash: string) => {
544
+ if (!hash || !hash.includes('=')) return hash;
545
+ let withoutHash = hash.startsWith('#') ? hash.slice(1) : hash;
546
+ let redacted = withoutHash.replace(
547
+ /(?<![A-Za-z0-9])([A-Za-z0-9][A-Za-z0-9_\-.]*)=([^&]*)/g,
548
+ (match, key: string, _rawValue: string) =>
549
+ isUrlFormSecretKeyName(key) ? `${key}=${REDACTED_VALUE}` : match
550
+ );
551
+ return redacted ? `#${redacted}` : '';
552
+ };
553
+
554
+ let sanitizeUrl = (value: string) => {
555
+ if (!value) return value;
556
+
557
+ try {
558
+ let url = new URL(value);
559
+
560
+ if (url.username || url.password) {
561
+ url.username = '';
562
+ url.password = '';
563
+ }
564
+
565
+ for (let key of Array.from(url.searchParams.keys())) {
566
+ if (isUrlFormSecretKeyName(key)) {
567
+ url.searchParams.set(key, REDACTED_VALUE);
568
+ }
569
+ }
570
+
571
+ url.hash = redactHashFragment(url.hash);
572
+
573
+ return decodeRedactedMarker(url.toString());
574
+ } catch {
575
+ return value.replace(
576
+ /([?&]([^=&#]+)=)([^&#]+)/g,
577
+ (_match, prefix: string, key: string, rawValue: string) =>
578
+ isUrlFormSecretKeyName(key) ? `${prefix}${REDACTED_VALUE}` : `${prefix}${rawValue}`
579
+ );
580
+ }
581
+ };
582
+
583
+ let flattenHeaderValue = (value: unknown): string | undefined => {
584
+ if (typeof value === 'string') return value;
585
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
586
+ if (Array.isArray(value)) {
587
+ let normalized = value
588
+ .map(entry => flattenHeaderValue(entry))
589
+ .filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
590
+ return normalized.length > 0 ? normalized.join(', ') : undefined;
591
+ }
592
+ return undefined;
593
+ };
594
+
595
+ let headerEntries = (headers: unknown): [string, string][] => {
596
+ if (!headers) return [];
597
+
598
+ if (typeof (headers as { toJSON?: unknown }).toJSON === 'function') {
599
+ let json = (headers as { toJSON(): unknown }).toJSON();
600
+ if (isRecord(json)) {
601
+ return Object.entries(json)
602
+ .map(([key, value]) => {
603
+ let flattened = flattenHeaderValue(value);
604
+ return flattened ? ([key, flattened] as [string, string]) : null;
605
+ })
606
+ .filter((entry): entry is [string, string] => entry !== null);
607
+ }
608
+ }
609
+
610
+ if (typeof (headers as { entries?: unknown }).entries === 'function') {
611
+ return Array.from(
612
+ (headers as { entries(): IterableIterator<[string, string]> }).entries()
613
+ ).map(([key, value]) => [key, value]);
614
+ }
615
+
616
+ if (isRecord(headers)) {
617
+ return Object.entries(headers)
618
+ .map(([key, value]) => {
619
+ let flattened = flattenHeaderValue(value);
620
+ return flattened ? ([key, flattened] as [string, string]) : null;
621
+ })
622
+ .filter((entry): entry is [string, string] => entry !== null);
623
+ }
624
+
625
+ return [];
626
+ };
627
+
628
+ let sanitizeHeaders = (headers: unknown) => {
629
+ let entries = headerEntries(headers)
630
+ .map(([key, value]) => [key.toLowerCase(), sanitizeScalar(value).text] as [string, string])
631
+ .filter(([key]) => isSafeHeaderName(key));
632
+
633
+ if (entries.length === 0) return undefined;
634
+ return Object.fromEntries(entries);
635
+ };
636
+
637
+ let getContentType = (headers: unknown) =>
638
+ headerEntries(headers).find(([key]) => key.toLowerCase() === 'content-type')?.[1];
639
+
640
+ let takeTraceDraft = (config: TraceAwareAxiosRequestConfig | undefined) => {
641
+ if (!config) return undefined;
642
+
643
+ let draft =
644
+ config.__slatesHttpTraceDraft ??
645
+ (isObjectLike(config) ? traceDraftsByConfig.get(config) : undefined) ??
646
+ (isObjectLike(config.headers) ? traceDraftsByHeaders.get(config.headers) : undefined);
647
+
648
+ if (!draft) return undefined;
649
+
650
+ return draft;
651
+ };
652
+
653
+ let clearTraceDraft = (
654
+ config: TraceAwareAxiosRequestConfig | undefined,
655
+ draft: SlateHttpTraceDraft
656
+ ) => {
657
+ if (!config) return;
658
+
659
+ if (config.__slatesHttpTraceDraft === draft) {
660
+ delete config.__slatesHttpTraceDraft;
661
+ }
662
+
663
+ if (isObjectLike(config)) {
664
+ traceDraftsByConfig.delete(config);
665
+ }
666
+
667
+ if (isObjectLike(config.headers)) {
668
+ traceDraftsByHeaders.delete(config.headers);
669
+ }
670
+ };
671
+
672
+ export let attachHttpTraceDraft = (
673
+ config: InternalAxiosRequestConfig,
674
+ context: SlateContext<any, any, any>
675
+ ) => {
676
+ let traceAwareConfig = config as TraceAwareAxiosRequestConfig;
677
+ let contentType = getContentType(config.headers);
678
+
679
+ traceAwareConfig.__slatesHttpTraceDraft = {
680
+ context,
681
+ startedAt: new Date().toISOString(),
682
+ startedAtMs: Date.now(),
683
+ request: {
684
+ method: (config.method ?? 'GET').toUpperCase(),
685
+ url: sanitizeUrl(buildUrl(config.baseURL, config.url)),
686
+ ...(sanitizeHeaders(config.headers) ? { headers: sanitizeHeaders(config.headers) } : {}),
687
+ ...(serializeTextBody(config.data, contentType)
688
+ ? { body: serializeTextBody(config.data, contentType) }
689
+ : {})
690
+ }
691
+ };
692
+
693
+ if (isObjectLike(traceAwareConfig)) {
694
+ traceDraftsByConfig.set(traceAwareConfig, traceAwareConfig.__slatesHttpTraceDraft);
695
+ }
696
+
697
+ if (isObjectLike(traceAwareConfig.headers)) {
698
+ traceDraftsByHeaders.set(
699
+ traceAwareConfig.headers,
700
+ traceAwareConfig.__slatesHttpTraceDraft
701
+ );
702
+ }
703
+
704
+ return config;
705
+ };
706
+
707
+ export let recordHttpTraceFromResponse = (response: AxiosResponse) => {
708
+ let config = response.config as TraceAwareAxiosRequestConfig | undefined;
709
+ let draft = takeTraceDraft(config);
710
+ if (!draft) return response;
711
+ clearTraceDraft(config, draft);
712
+
713
+ let contentType = getContentType(response.headers);
714
+ draft.context.recordHttpTrace({
715
+ startedAt: draft.startedAt,
716
+ durationMs: Math.max(Date.now() - draft.startedAtMs, 0),
717
+ request: draft.request,
718
+ response: {
719
+ status: response.status,
720
+ ...(response.statusText ? { statusText: response.statusText } : {}),
721
+ ...(sanitizeHeaders(response.headers)
722
+ ? { headers: sanitizeHeaders(response.headers) }
723
+ : {}),
724
+ ...(serializeTextBody(response.data, contentType)
725
+ ? { body: serializeTextBody(response.data, contentType) }
726
+ : {})
727
+ }
728
+ });
729
+
730
+ return response;
731
+ };
732
+
733
+ export let recordHttpTraceFromError = (error: AxiosError) => {
734
+ let config = error.config as TraceAwareAxiosRequestConfig | undefined;
735
+ let draft = takeTraceDraft(config);
736
+ if (!draft) return;
737
+ clearTraceDraft(config, draft);
738
+
739
+ let responseContentType = getContentType(error.response?.headers);
740
+ draft.context.recordHttpTrace({
741
+ startedAt: draft.startedAt,
742
+ durationMs: Math.max(Date.now() - draft.startedAtMs, 0),
743
+ request: draft.request,
744
+ ...(error.response
745
+ ? {
746
+ response: {
747
+ status: error.response.status,
748
+ ...(error.response.statusText ? { statusText: error.response.statusText } : {}),
749
+ ...(sanitizeHeaders(error.response.headers)
750
+ ? { headers: sanitizeHeaders(error.response.headers) }
751
+ : {}),
752
+ ...(serializeTextBody(error.response.data, responseContentType)
753
+ ? { body: serializeTextBody(error.response.data, responseContentType) }
754
+ : {})
755
+ }
756
+ }
757
+ : {}),
758
+ error: {
759
+ ...(error.code ? { code: error.code } : {}),
760
+ message: error.message
761
+ }
762
+ });
763
+ };
764
+
765
+ // Exposed for unit tests. Not part of the public API.
766
+ export let __traceInternals = {
767
+ sanitizeFreeText,
768
+ sanitizeUrl,
769
+ sanitizeHeaders,
770
+ isSafeHeaderName,
771
+ isSecretKeyName,
772
+ isUrlFormSecretKeyName,
773
+ redactStructuredValue,
774
+ SECRET_KEY_PATTERN,
775
+ URL_FORM_SECRET_KEY_PATTERN
776
+ };