@slates/provider 1.0.0-rc.18 → 1.0.0-rc.19

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.
@@ -1,6 +1,10 @@
1
1
  export interface SlateAttachmentUrlContent {
2
2
  type: 'url';
3
3
  url: string;
4
+ headers?: Record<string, string>;
5
+ query?: Record<string, string>;
6
+ refreshReference?: unknown;
7
+ refreshAt?: string;
4
8
  }
5
9
 
6
10
  export interface SlateAttachmentInlineContent {
@@ -9,21 +13,58 @@ export interface SlateAttachmentInlineContent {
9
13
  content: string;
10
14
  }
11
15
 
16
+ export interface SlateAttachmentUploadReferenceContent {
17
+ type: 'upload_reference';
18
+ referenceId: string;
19
+ }
20
+
12
21
  export interface SlateAttachment {
13
22
  mimeType?: string;
14
- content: SlateAttachmentUrlContent | SlateAttachmentInlineContent;
23
+ attachmentHash?: string;
24
+ content:
25
+ | SlateAttachmentUrlContent
26
+ | SlateAttachmentInlineContent
27
+ | SlateAttachmentUploadReferenceContent;
15
28
  }
16
29
 
17
- export let createUrlAttachment = (url: string, mimeType?: string): SlateAttachment => ({
18
- mimeType,
19
- content: {
20
- type: 'url',
21
- url
30
+ export let createUrlAttachment = (
31
+ url: string,
32
+ opts: {
33
+ mimeType?: string;
34
+ attachmentHash?: string;
35
+ headers?: Record<string, string>;
36
+ query?: Record<string, string>;
37
+ refreshReference?: unknown;
38
+ refreshAt?: string;
39
+ } = {}
40
+ ): SlateAttachment => {
41
+ if (opts.refreshReference !== undefined && !opts.refreshAt) {
42
+ throw new Error('createUrlAttachment: refreshAt is required when refreshReference is set');
22
43
  }
23
- });
24
44
 
25
- export let createBase64Attachment = (content: string, mimeType?: string): SlateAttachment => ({
45
+ return {
46
+ mimeType: opts.mimeType,
47
+ attachmentHash: opts.attachmentHash,
48
+ content: {
49
+ type: 'url',
50
+ url,
51
+ ...(opts.headers ? { headers: opts.headers } : {}),
52
+ ...(opts.query ? { query: opts.query } : {}),
53
+ ...(opts.refreshReference !== undefined
54
+ ? { refreshReference: opts.refreshReference }
55
+ : {}),
56
+ ...(opts.refreshAt ? { refreshAt: opts.refreshAt } : {})
57
+ }
58
+ };
59
+ };
60
+
61
+ export let createBase64Attachment = (
62
+ content: string,
63
+ mimeType?: string,
64
+ attachmentHash?: string
65
+ ): SlateAttachment => ({
26
66
  mimeType,
67
+ attachmentHash,
27
68
  content: {
28
69
  type: 'content',
29
70
  encoding: 'base64',
@@ -31,8 +72,13 @@ export let createBase64Attachment = (content: string, mimeType?: string): SlateA
31
72
  }
32
73
  });
33
74
 
34
- export let createTextAttachment = (content: string, mimeType?: string): SlateAttachment => ({
75
+ export let createTextAttachment = (
76
+ content: string,
77
+ mimeType?: string,
78
+ attachmentHash?: string
79
+ ): SlateAttachment => ({
35
80
  mimeType,
81
+ attachmentHash,
36
82
  content: {
37
83
  type: 'content',
38
84
  encoding: 'utf-8',
@@ -0,0 +1,59 @@
1
+ import type { SlateAttachment } from './attachment';
2
+
3
+ export interface SlateLiveInvocationInfo {
4
+ token: string;
5
+ baseUrl: string;
6
+ maxAttachmentSizeBytes: number;
7
+ }
8
+
9
+ export interface SlateDirectUploadInput {
10
+ live: SlateLiveInvocationInfo;
11
+ mimeType?: string;
12
+ filename?: string;
13
+ body: ReadableStream<Uint8Array> | Uint8Array;
14
+ }
15
+
16
+ export let uploadAttachmentDirect = async (
17
+ d: SlateDirectUploadInput
18
+ ): Promise<SlateAttachment> => {
19
+ let baseUrl = d.live.baseUrl.replace(/\/$/, '');
20
+
21
+ let registerRes = await fetch(`${baseUrl}/slates-hub/live-invocation/attachments`, {
22
+ method: 'POST',
23
+ headers: {
24
+ 'content-type': 'application/json',
25
+ authorization: `Bearer ${d.live.token}`
26
+ },
27
+ body: JSON.stringify({
28
+ attachments: [{ mimeType: d.mimeType, filename: d.filename }]
29
+ })
30
+ });
31
+ if (!registerRes.ok) {
32
+ throw new Error(
33
+ `Failed to register attachment for direct upload (status ${registerRes.status})`
34
+ );
35
+ }
36
+
37
+ let { attachments } = (await registerRes.json()) as {
38
+ attachments: { referenceId: string; uploadUrl: string }[];
39
+ };
40
+ let registration = attachments[0];
41
+ if (!registration) {
42
+ throw new Error('Live invocation API returned no attachment registrations');
43
+ }
44
+
45
+ let putRes = await fetch(registration.uploadUrl, {
46
+ method: 'PUT',
47
+ ...(d.body instanceof Uint8Array ? {} : ({ duplex: 'half' } as any)),
48
+ headers: d.mimeType ? { 'content-type': d.mimeType } : undefined,
49
+ body: d.body
50
+ });
51
+ if (!putRes.ok) {
52
+ throw new Error(`Failed to upload attachment content (status ${putRes.status})`);
53
+ }
54
+
55
+ return {
56
+ mimeType: d.mimeType,
57
+ content: { type: 'upload_reference', referenceId: registration.referenceId }
58
+ };
59
+ };
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ import type { SlateContext } from '../context/context';
3
+ import type { SlateSpecification } from '../specification/specification';
4
+ import { tool } from './tool';
5
+
6
+ export let GET_FILE_URL_TOOL_ID = 'metorial$getFileUrl';
7
+
8
+ export let getFileUrlToolInputSchema = z.object({
9
+ url: z.string(),
10
+ reference: z.any().nullable()
11
+ });
12
+ export type GetFileUrlToolInput = z.infer<typeof getFileUrlToolInputSchema>;
13
+
14
+ export let getFileUrlToolOutputSchema = z.object({
15
+ url: z.string(),
16
+ expiresAt: z.string(),
17
+ headers: z.record(z.string(), z.string()).optional(),
18
+ query: z.record(z.string(), z.string()).optional()
19
+ });
20
+ export type GetFileUrlToolOutput = z.infer<typeof getFileUrlToolOutputSchema>;
21
+
22
+ export type GetFileUrlHandler<ConfigType extends {}, AuthType extends {}> = (
23
+ context: SlateContext<ConfigType, AuthType, GetFileUrlToolInput>
24
+ ) => Promise<GetFileUrlToolOutput>;
25
+
26
+ export let getFileUrlTool = <ConfigType extends {}, AuthType extends {}>(
27
+ spec: SlateSpecification<ConfigType, AuthType>,
28
+ handleGetFileUrl: GetFileUrlHandler<ConfigType, AuthType>
29
+ ) =>
30
+ tool(spec, {
31
+ key: GET_FILE_URL_TOOL_ID,
32
+ name: 'Get File URL',
33
+ description: 'Reserved: refreshes an expiring attachment download URL. Hub-internal only.'
34
+ })
35
+ .input(getFileUrlToolInputSchema)
36
+ .output(getFileUrlToolOutputSchema)
37
+ .handleInvocation(async ctx => ({
38
+ output: await handleGetFileUrl(ctx),
39
+ message: 'ok'
40
+ }))
41
+ .build();
@@ -1,5 +1,6 @@
1
1
  export * from './action';
2
2
  export * from './builder';
3
+ export * from './getFileUrlTool';
3
4
  export * from './publicTool';
4
5
  export * from './scopes';
5
6
  export * from './tool';
package/src/auth/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './auth';
2
2
  export * from './oauth';
3
+ export * from './redact';
3
4
  export * from './types';
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { SlateAttachment } from '../action/attachment';
3
+ import { AuthConfigSecretRedactor, redactUrlAttachmentSecrets } from './redact';
4
+
5
+ describe('AuthConfigSecretRedactor', () => {
6
+ let authConfig = { token: '123SECRET', nested: { token2: 'also_secret' } };
7
+
8
+ it('redacts values that match a secret to placeholders', () => {
9
+ let redactor = new AuthConfigSecretRedactor(authConfig);
10
+
11
+ expect(
12
+ redactor.redact({
13
+ headers: { Authorization: 'Bearer 123SECRET' },
14
+ query: { key: 'also_secret' }
15
+ })
16
+ ).toEqual({
17
+ headers: { Authorization: 'Bearer 123SECRET' }, // whole-value match only, not substring
18
+ query: { key: '$$MT$secret$authConfig$nested.token2' }
19
+ });
20
+ });
21
+
22
+ it('redacts a bare string value', () => {
23
+ let redactor = new AuthConfigSecretRedactor(authConfig);
24
+ expect(redactor.redact('123SECRET')).toBe('$$MT$secret$authConfig$token');
25
+ });
26
+
27
+ it('leaves non-secret strings untouched', () => {
28
+ let redactor = new AuthConfigSecretRedactor(authConfig);
29
+ expect(redactor.redact('https://example.com/file.pdf')).toBe(
30
+ 'https://example.com/file.pdf'
31
+ );
32
+ });
33
+
34
+ it('recurses through arrays and nested objects', () => {
35
+ let redactor = new AuthConfigSecretRedactor(authConfig);
36
+ expect(redactor.redact({ list: ['also_secret', 'plain'] })).toEqual({
37
+ list: ['$$MT$secret$authConfig$nested.token2', 'plain']
38
+ });
39
+ });
40
+
41
+ it('never registers an empty-string secret', () => {
42
+ let redactor = new AuthConfigSecretRedactor({ token: '' });
43
+ expect(redactor.redact({ a: '', b: '' })).toEqual({ a: '', b: '' });
44
+ });
45
+
46
+ it('is a no-op when there is no auth config', () => {
47
+ let redactor = new AuthConfigSecretRedactor(undefined);
48
+ expect(redactor.redact({ a: 'b' })).toEqual({ a: 'b' });
49
+ });
50
+ });
51
+
52
+ describe('redactUrlAttachmentSecrets', () => {
53
+ let authConfig = { token: '123SECRET', nested: { token2: 'also_secret' } };
54
+
55
+ let urlAttachment = (
56
+ overrides: Partial<Extract<SlateAttachment['content'], { type: 'url' }>> = {}
57
+ ): SlateAttachment => ({
58
+ mimeType: 'application/pdf',
59
+ content: { type: 'url', url: 'https://example.com/file.pdf', ...overrides }
60
+ });
61
+
62
+ it('redacts headers and query on url attachments', () => {
63
+ let attachments: SlateAttachment[] = [
64
+ urlAttachment({
65
+ headers: { Authorization: 'Bearer 123SECRET' },
66
+ query: { key: 'also_secret' }
67
+ })
68
+ ];
69
+
70
+ expect(redactUrlAttachmentSecrets(attachments, authConfig)).toEqual([
71
+ urlAttachment({
72
+ headers: { Authorization: 'Bearer 123SECRET' },
73
+ query: { key: '$$MT$secret$authConfig$nested.token2' }
74
+ })
75
+ ]);
76
+ });
77
+
78
+ it('leaves url attachments with no headers/query untouched', () => {
79
+ let attachments = [urlAttachment()];
80
+ expect(redactUrlAttachmentSecrets(attachments, authConfig)).toEqual(attachments);
81
+ });
82
+
83
+ it('leaves content and upload_reference attachments untouched', () => {
84
+ let attachments: SlateAttachment[] = [
85
+ {
86
+ mimeType: 'text/plain',
87
+ content: { type: 'content', encoding: 'utf-8', content: 'hi' }
88
+ },
89
+ { content: { type: 'upload_reference', referenceId: 'ref_1' } }
90
+ ];
91
+
92
+ expect(redactUrlAttachmentSecrets(attachments, authConfig)).toEqual(attachments);
93
+ });
94
+
95
+ it('is a no-op when there is no auth config', () => {
96
+ let attachments = [urlAttachment({ headers: { Authorization: 'Bearer 123SECRET' } })];
97
+ expect(redactUrlAttachmentSecrets(attachments, undefined)).toBe(attachments);
98
+ });
99
+
100
+ it('passes an undefined attachment list through unchanged', () => {
101
+ expect(redactUrlAttachmentSecrets(undefined, authConfig)).toBeUndefined();
102
+ });
103
+ });
@@ -0,0 +1,111 @@
1
+ import type { SlateAttachment } from '../action/attachment';
2
+
3
+ /**
4
+ * Placeholder prefix used to mark a redacted auth-config secret. Must match the hub's
5
+ * AuthConfigSecretSerializer (oss/src/slates/apps/hub/src/lib/secretSerializer.ts) exactly --
6
+ * the hub resolves these placeholders back to live secret values when it proxies an attachment.
7
+ */
8
+ export let AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = '$$MT$secret$authConfig$';
9
+
10
+ /**
11
+ * Walks an auth config object and replaces any string value that exactly matches one of its
12
+ * leaf values with a `$$MT$secret$authConfig$<path>` placeholder. Literal, whole-value
13
+ * substitution only -- not a substring scan, and not a heuristic key-name/value-shape guess
14
+ * like the http trace redactor in '../axios/trace'.
15
+ */
16
+ export class AuthConfigSecretRedactor {
17
+ private readonly secretToPlaceholder = new Map<string, string>();
18
+
19
+ public constructor(authConfig: Record<string, unknown> | undefined) {
20
+ if (authConfig) this.buildSecretMap(authConfig);
21
+ }
22
+
23
+ public redact<T>(value: T): T {
24
+ return this.transform(value, stringValue => {
25
+ return this.secretToPlaceholder.get(stringValue) ?? stringValue;
26
+ });
27
+ }
28
+
29
+ private buildSecretMap(value: unknown, path: string[] = []): void {
30
+ if (typeof value === 'string') {
31
+ // Never register "" -- it would replace every empty header/query value with a placeholder.
32
+ if (value.length === 0) return;
33
+
34
+ this.secretToPlaceholder.set(
35
+ value,
36
+ `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path.join('.')}`
37
+ );
38
+ return;
39
+ }
40
+
41
+ if (Array.isArray(value)) {
42
+ value.forEach((item, index) => {
43
+ this.buildSecretMap(item, [...path, String(index)]);
44
+ });
45
+
46
+ return;
47
+ }
48
+
49
+ if (this.isPlainObject(value)) {
50
+ Object.entries(value).forEach(([key, nestedValue]) => {
51
+ this.buildSecretMap(nestedValue, [...path, key]);
52
+ });
53
+ }
54
+ }
55
+
56
+ private transform<T>(value: T, transformString: (value: string) => string): T {
57
+ if (typeof value === 'string') {
58
+ return transformString(value) as T;
59
+ }
60
+
61
+ if (Array.isArray(value)) {
62
+ return value.map(item => this.transform(item, transformString)) as T;
63
+ }
64
+
65
+ if (this.isPlainObject(value)) {
66
+ let result: Record<string, unknown> = {};
67
+
68
+ Object.entries(value).forEach(([key, nestedValue]) => {
69
+ result[key] = this.transform(nestedValue, transformString);
70
+ });
71
+
72
+ return result as T;
73
+ }
74
+
75
+ return value;
76
+ }
77
+
78
+ private isPlainObject(value: unknown): value is Record<string, unknown> {
79
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Applied to every finalized attachment list right before it's sent over
85
+ * `slates/action.tool.invoke` (see @slates/provider-handler). Only `type: 'url'` attachments
86
+ * carry headers/query -- content/upload_reference attachments pass through untouched.
87
+ */
88
+ export let redactUrlAttachmentSecrets = (
89
+ attachments: SlateAttachment[] | undefined,
90
+ authConfig: Record<string, unknown> | undefined
91
+ ): SlateAttachment[] | undefined => {
92
+ if (!authConfig || !attachments) return attachments;
93
+
94
+ let redactor = new AuthConfigSecretRedactor(authConfig);
95
+
96
+ return attachments.map(attachment => {
97
+ if (attachment.content.type !== 'url') return attachment;
98
+ if (!attachment.content.headers && !attachment.content.query) return attachment;
99
+
100
+ return {
101
+ ...attachment,
102
+ content: {
103
+ ...attachment.content,
104
+ headers: attachment.content.headers
105
+ ? redactor.redact(attachment.content.headers)
106
+ : undefined,
107
+ query: attachment.content.query ? redactor.redact(attachment.content.query) : undefined
108
+ }
109
+ };
110
+ });
111
+ };
package/src/auth/types.ts CHANGED
@@ -19,7 +19,6 @@ export type SlateAuthWithOauth<
19
19
  scopes: {
20
20
  title: string;
21
21
  description?: string;
22
- defaultChecked?: boolean;
23
22
  scope: string;
24
23
  }[];
25
24
 
@@ -1,4 +1,5 @@
1
1
  import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
2
+ import { AuthConfigSecretRedactor } from '../auth/redact';
2
3
  import type { SlatePublicContext } from '../context';
3
4
 
4
5
  export interface SlateHttpTraceTextBody {
@@ -377,10 +378,12 @@ let truncateText = (text: string) => {
377
378
  };
378
379
  };
379
380
 
380
- let sanitizeFreeText = (text: string) => {
381
+ let sanitizeFreeText = (text: string, redactor?: AuthConfigSecretRedactor) => {
381
382
  if (!text) return text;
382
383
 
383
- let result = text;
384
+ // Literal auth-config secret values are replaced first -- this catches secrets sitting
385
+ // under a key name the heuristics below don't recognize as secret-like.
386
+ let result = redactor ? redactor.redact(text) : text;
384
387
 
385
388
  result = result.replace(
386
389
  HTTP_AUTH_SCHEME_PATTERN,
@@ -406,9 +409,14 @@ let sanitizeFreeText = (text: string) => {
406
409
  return result;
407
410
  };
408
411
 
409
- let sanitizeScalar = (value: string) => truncateText(sanitizeFreeText(value));
412
+ let sanitizeScalar = (value: string, redactor?: AuthConfigSecretRedactor) =>
413
+ truncateText(sanitizeFreeText(value, redactor));
410
414
 
411
- let redactStructuredValue = (value: unknown, depth = 0): unknown => {
415
+ let redactStructuredValue = (
416
+ value: unknown,
417
+ depth = 0,
418
+ redactor?: AuthConfigSecretRedactor
419
+ ): unknown => {
412
420
  if (value == null) return value;
413
421
  if (depth >= STRUCTURED_DEPTH_LIMIT) return '[truncated]';
414
422
 
@@ -418,13 +426,13 @@ let redactStructuredValue = (value: unknown, depth = 0): unknown => {
418
426
  typeof value === 'boolean' ||
419
427
  typeof value === 'bigint'
420
428
  ) {
421
- return typeof value === 'string' ? sanitizeFreeText(value) : value;
429
+ return typeof value === 'string' ? sanitizeFreeText(value, redactor) : value;
422
430
  }
423
431
 
424
432
  if (Array.isArray(value)) {
425
433
  return value
426
434
  .slice(0, STRUCTURED_ENTRY_LIMIT)
427
- .map(entry => redactStructuredValue(entry, depth + 1));
435
+ .map(entry => redactStructuredValue(entry, depth + 1, redactor));
428
436
  }
429
437
 
430
438
  if (isRecord(value)) {
@@ -433,7 +441,9 @@ let redactStructuredValue = (value: unknown, depth = 0): unknown => {
433
441
  .slice(0, STRUCTURED_ENTRY_LIMIT)
434
442
  .map(([key, entry]) => [
435
443
  key,
436
- isSecretKeyName(key) ? REDACTED_VALUE : redactStructuredValue(entry, depth + 1)
444
+ isSecretKeyName(key)
445
+ ? REDACTED_VALUE
446
+ : redactStructuredValue(entry, depth + 1, redactor)
437
447
  ])
438
448
  );
439
449
  }
@@ -457,7 +467,8 @@ let maybeParseJson = (text: string, contentType?: string) => {
457
467
 
458
468
  let sanitizeTextBody = (
459
469
  text: string,
460
- contentType?: string
470
+ contentType?: string,
471
+ redactor?: AuthConfigSecretRedactor
461
472
  ): SlateHttpTraceTextBody | undefined => {
462
473
  if (!text) return undefined;
463
474
 
@@ -465,8 +476,8 @@ let sanitizeTextBody = (
465
476
  let parsedJson = maybeParseJson(text, normalizedContentType);
466
477
  let normalizedText =
467
478
  parsedJson !== null
468
- ? JSON.stringify(redactStructuredValue(parsedJson))
469
- : sanitizeFreeText(text);
479
+ ? JSON.stringify(redactStructuredValue(parsedJson, 0, redactor))
480
+ : sanitizeFreeText(text, redactor);
470
481
  let { text: truncatedText, truncated } = truncateText(normalizedText);
471
482
 
472
483
  return {
@@ -487,7 +498,8 @@ let isBinaryLike = (value: unknown) => {
487
498
 
488
499
  let serializeTextBody = (
489
500
  value: unknown,
490
- contentType?: string
501
+ contentType?: string,
502
+ redactor?: AuthConfigSecretRedactor
491
503
  ): SlateHttpTraceTextBody | undefined => {
492
504
  let normalizedContentType = normalizeContentType(contentType);
493
505
 
@@ -496,19 +508,23 @@ let serializeTextBody = (
496
508
  }
497
509
 
498
510
  if (typeof value === 'string') {
499
- return sanitizeTextBody(value, normalizedContentType);
511
+ return sanitizeTextBody(value, normalizedContentType, redactor);
500
512
  }
501
513
 
502
514
  if (value instanceof URLSearchParams) {
503
- return sanitizeTextBody(value.toString(), normalizedContentType);
515
+ return sanitizeTextBody(value.toString(), normalizedContentType, redactor);
504
516
  }
505
517
 
506
518
  if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
507
- return sanitizeTextBody(String(value), normalizedContentType);
519
+ return sanitizeTextBody(String(value), normalizedContentType, redactor);
508
520
  }
509
521
 
510
522
  if (Array.isArray(value) || isRecord(value)) {
511
- return sanitizeTextBody(JSON.stringify(redactStructuredValue(value)), 'application/json');
523
+ return sanitizeTextBody(
524
+ JSON.stringify(redactStructuredValue(value, 0, redactor)),
525
+ 'application/json',
526
+ redactor
527
+ );
512
528
  }
513
529
 
514
530
  return undefined;
@@ -533,18 +549,21 @@ let decodeRedactedMarker = (value: string) =>
533
549
  ? value
534
550
  : value.split(REDACTED_ENCODED).join(REDACTED_VALUE);
535
551
 
536
- let redactHashFragment = (hash: string) => {
552
+ let redactHashFragment = (hash: string, redactor?: AuthConfigSecretRedactor) => {
537
553
  if (!hash?.includes('=')) return hash;
538
554
  let withoutHash = hash.startsWith('#') ? hash.slice(1) : hash;
539
555
  let redacted = withoutHash.replace(
540
556
  /(?<![A-Za-z0-9])([A-Za-z0-9][A-Za-z0-9_\-.]*)=([^&]*)/g,
541
- (match, key: string, _rawValue: string) =>
542
- isUrlFormSecretKeyName(key) ? `${key}=${REDACTED_VALUE}` : match
557
+ (match, key: string, rawValue: string) => {
558
+ if (isUrlFormSecretKeyName(key)) return `${key}=${REDACTED_VALUE}`;
559
+ let literalRedacted = redactor ? redactor.redact(rawValue) : rawValue;
560
+ return literalRedacted !== rawValue ? `${key}=${literalRedacted}` : match;
561
+ }
543
562
  );
544
563
  return redacted ? `#${redacted}` : '';
545
564
  };
546
565
 
547
- let sanitizeUrl = (value: string) => {
566
+ let sanitizeUrl = (value: string, redactor?: AuthConfigSecretRedactor) => {
548
567
  if (!value) return value;
549
568
 
550
569
  try {
@@ -558,14 +577,21 @@ let sanitizeUrl = (value: string) => {
558
577
  for (let key of Array.from(url.searchParams.keys())) {
559
578
  if (isUrlFormSecretKeyName(key)) {
560
579
  url.searchParams.set(key, REDACTED_VALUE);
580
+ continue;
561
581
  }
582
+
583
+ // Not a secret-looking key -- still catch a literal auth-config secret value.
584
+ let rawValue = url.searchParams.get(key) ?? '';
585
+ let literalRedacted = redactor ? redactor.redact(rawValue) : rawValue;
586
+ if (literalRedacted !== rawValue) url.searchParams.set(key, literalRedacted);
562
587
  }
563
588
 
564
- url.hash = redactHashFragment(url.hash);
589
+ url.hash = redactHashFragment(url.hash, redactor);
565
590
 
566
591
  return decodeRedactedMarker(url.toString());
567
592
  } catch {
568
- return value.replace(
593
+ let text = redactor ? redactor.redact(value) : value;
594
+ return text.replace(
569
595
  /([?&]([^=&#]+)=)([^&#]+)/g,
570
596
  (_match, prefix: string, key: string, rawValue: string) =>
571
597
  isUrlFormSecretKeyName(key) ? `${prefix}${REDACTED_VALUE}` : `${prefix}${rawValue}`
@@ -618,9 +644,12 @@ let headerEntries = (headers: unknown): [string, string][] => {
618
644
  return [];
619
645
  };
620
646
 
621
- let sanitizeHeaders = (headers: unknown) => {
647
+ let sanitizeHeaders = (headers: unknown, redactor?: AuthConfigSecretRedactor) => {
622
648
  let entries = headerEntries(headers)
623
- .map(([key, value]) => [key.toLowerCase(), sanitizeScalar(value).text] as [string, string])
649
+ .map(
650
+ ([key, value]) =>
651
+ [key.toLowerCase(), sanitizeScalar(value, redactor).text] as [string, string]
652
+ )
624
653
  .filter(([key]) => isSafeHeaderName(key));
625
654
 
626
655
  if (entries.length === 0) return undefined;
@@ -668,6 +697,7 @@ export let attachHttpTraceDraft = (
668
697
  ) => {
669
698
  let traceAwareConfig = config as TraceAwareAxiosRequestConfig;
670
699
  let contentType = getContentType(config.headers);
700
+ let redactor = new AuthConfigSecretRedactor(context._getAuthConfigForRedaction());
671
701
 
672
702
  traceAwareConfig.__slatesHttpTraceDraft = {
673
703
  context,
@@ -675,10 +705,12 @@ export let attachHttpTraceDraft = (
675
705
  startedAtMs: Date.now(),
676
706
  request: {
677
707
  method: (config.method ?? 'GET').toUpperCase(),
678
- url: sanitizeUrl(buildUrl(config.baseURL, config.url)),
679
- ...(sanitizeHeaders(config.headers) ? { headers: sanitizeHeaders(config.headers) } : {}),
680
- ...(serializeTextBody(config.data, contentType)
681
- ? { body: serializeTextBody(config.data, contentType) }
708
+ url: sanitizeUrl(buildUrl(config.baseURL, config.url), redactor),
709
+ ...(sanitizeHeaders(config.headers, redactor)
710
+ ? { headers: sanitizeHeaders(config.headers, redactor) }
711
+ : {}),
712
+ ...(serializeTextBody(config.data, contentType, redactor)
713
+ ? { body: serializeTextBody(config.data, contentType, redactor) }
682
714
  : {})
683
715
  }
684
716
  };
@@ -703,6 +735,7 @@ export let recordHttpTraceFromResponse = (response: AxiosResponse) => {
703
735
  if (!draft) return response;
704
736
  clearTraceDraft(config, draft);
705
737
 
738
+ let redactor = new AuthConfigSecretRedactor(draft.context._getAuthConfigForRedaction());
706
739
  let contentType = getContentType(response.headers);
707
740
  draft.context.recordHttpTrace({
708
741
  startedAt: draft.startedAt,
@@ -711,11 +744,11 @@ export let recordHttpTraceFromResponse = (response: AxiosResponse) => {
711
744
  response: {
712
745
  status: response.status,
713
746
  ...(response.statusText ? { statusText: response.statusText } : {}),
714
- ...(sanitizeHeaders(response.headers)
715
- ? { headers: sanitizeHeaders(response.headers) }
747
+ ...(sanitizeHeaders(response.headers, redactor)
748
+ ? { headers: sanitizeHeaders(response.headers, redactor) }
716
749
  : {}),
717
- ...(serializeTextBody(response.data, contentType)
718
- ? { body: serializeTextBody(response.data, contentType) }
750
+ ...(serializeTextBody(response.data, contentType, redactor)
751
+ ? { body: serializeTextBody(response.data, contentType, redactor) }
719
752
  : {})
720
753
  }
721
754
  });
@@ -729,6 +762,7 @@ export let recordHttpTraceFromError = (error: AxiosError) => {
729
762
  if (!draft) return;
730
763
  clearTraceDraft(config, draft);
731
764
 
765
+ let redactor = new AuthConfigSecretRedactor(draft.context._getAuthConfigForRedaction());
732
766
  let responseContentType = getContentType(error.response?.headers);
733
767
  draft.context.recordHttpTrace({
734
768
  startedAt: draft.startedAt,
@@ -739,11 +773,11 @@ export let recordHttpTraceFromError = (error: AxiosError) => {
739
773
  response: {
740
774
  status: error.response.status,
741
775
  ...(error.response.statusText ? { statusText: error.response.statusText } : {}),
742
- ...(sanitizeHeaders(error.response.headers)
743
- ? { headers: sanitizeHeaders(error.response.headers) }
776
+ ...(sanitizeHeaders(error.response.headers, redactor)
777
+ ? { headers: sanitizeHeaders(error.response.headers, redactor) }
744
778
  : {}),
745
- ...(serializeTextBody(error.response.data, responseContentType)
746
- ? { body: serializeTextBody(error.response.data, responseContentType) }
779
+ ...(serializeTextBody(error.response.data, responseContentType, redactor)
780
+ ? { body: serializeTextBody(error.response.data, responseContentType, redactor) }
747
781
  : {})
748
782
  }
749
783
  }