@slates/provider 1.0.0-rc.28 → 1.0.0-rc.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/provider",
3
- "version": "1.0.0-rc.28",
3
+ "version": "1.0.0-rc.30",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -4,5 +4,6 @@ export * from './getFileUrlTool';
4
4
  export * from './publicTool';
5
5
  export * from './scopes';
6
6
  export * from './tool';
7
+ export * from './toolCategory';
7
8
  export * from './trigger';
8
9
  export * from './triggerGroup';
@@ -0,0 +1,40 @@
1
+ import type { SlateTool } from './tool';
2
+
3
+ export interface SlateToolCategoryParameters {
4
+ key: string;
5
+ name: string;
6
+ description?: string;
7
+ imageUrl?: string;
8
+ tools: SlateTool<any, any, any, any>[];
9
+ }
10
+
11
+ export class SlateToolCategory {
12
+ private constructor(private readonly _params: SlateToolCategoryParameters) {}
13
+
14
+ static create(params: SlateToolCategoryParameters) {
15
+ return new SlateToolCategory(params);
16
+ }
17
+
18
+ get key() {
19
+ return this._params.key;
20
+ }
21
+
22
+ get name() {
23
+ return this._params.name;
24
+ }
25
+
26
+ get description() {
27
+ return this._params.description;
28
+ }
29
+
30
+ get imageUrl() {
31
+ return this._params.imageUrl;
32
+ }
33
+
34
+ get tools() {
35
+ return this._params.tools;
36
+ }
37
+ }
38
+
39
+ export let toolCategory = (params: SlateToolCategoryParameters) =>
40
+ SlateToolCategory.create(params);
@@ -141,6 +141,35 @@ describe('AuthConfigSecretRedactor.redactEmbedded', () => {
141
141
  });
142
142
  });
143
143
 
144
+ describe('AuthConfigSecretRedactor filters', () => {
145
+ let authConfig = { token: 'secret-token', accountId: '123456', username: 'secret-token' };
146
+ let onlyToken = (_secret: string, path: string) => path === 'token';
147
+
148
+ it('limits whole-value replacement to fields that pass the filter', () => {
149
+ let redactor = new AuthConfigSecretRedactor(authConfig);
150
+
151
+ expect(redactor.redact(['123456', 'secret-token'], onlyToken)).toEqual([
152
+ '123456',
153
+ '$$MT$secret$authConfig$token'
154
+ ]);
155
+ expect(
156
+ redactor.redactEmbedded(['123456', 'secret-token'], () => false, onlyToken)
157
+ ).toEqual(['123456', '$$MT$secret$authConfig$token']);
158
+ expect(redactor.redactEmbedded('id 123456 / secret-token', onlyToken, onlyToken)).toBe(
159
+ 'id 123456 / $$MT$secret$authConfig$token$$'
160
+ );
161
+ });
162
+
163
+ it('keeps replacing every field, last path first, without a filter', () => {
164
+ let redactor = new AuthConfigSecretRedactor(authConfig);
165
+
166
+ expect(redactor.redact(['123456', 'secret-token'])).toEqual([
167
+ '$$MT$secret$authConfig$accountId',
168
+ '$$MT$secret$authConfig$username'
169
+ ]);
170
+ });
171
+ });
172
+
144
173
  describe('redactUrlAttachmentSecrets', () => {
145
174
  let authConfig = { token: '123SECRET', nested: { token2: 'also_secret' } };
146
175
 
@@ -7,38 +7,52 @@ import type { SlateAttachment } from '../action/attachment';
7
7
  */
8
8
  export let AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX = '$$MT$secret$authConfig$';
9
9
 
10
+ /** Decides whether an auth-config value (with its dotted path) may be replaced. */
11
+ export type AuthConfigSecretFilter = (secret: string, path: string) => boolean;
12
+
13
+ let ALWAYS: AuthConfigSecretFilter = () => true;
14
+
10
15
  /**
11
16
  * Maps auth-config string leaves to `$$MT$secret$authConfig$<path>` placeholders.
12
17
  * `redact` preserves whole-value substitution for HTTP traces; `redactEmbedded` also
13
18
  * replaces embedded values in attachment credentials, terminating those placeholders with `$$`.
19
+ * Both accept a `wholeValue` filter so diagnostics (traces, error text) can skip
20
+ * non-secret fields such as account IDs, emails, or subdomains; attachment callers keep
21
+ * the default, because the hub resolves every placeholder back to its value.
14
22
  */
15
23
  export class AuthConfigSecretRedactor {
16
- private readonly secretToPlaceholder = new Map<string, string>();
24
+ // A value shared by several fields keeps every path, so a filter that rejects one
25
+ // field (e.g. `username`) can still match another (e.g. `token`) with the same value.
26
+ private readonly secretToPaths = new Map<string, string[]>();
17
27
 
18
28
  public constructor(authConfig: Record<string, unknown> | undefined) {
19
29
  if (authConfig) this.buildSecretMap(authConfig);
20
30
  }
21
31
 
22
- public redact<T>(value: T): T {
32
+ public redact<T>(value: T, wholeValue: AuthConfigSecretFilter = ALWAYS): T {
23
33
  return this.transform(value, stringValue => {
24
- return this.secretToPlaceholder.get(stringValue) ?? stringValue;
34
+ return this.placeholderFor(stringValue, wholeValue) ?? stringValue;
25
35
  });
26
36
  }
27
37
 
28
- /** `embeddable` limits which secrets are also replaced inside longer strings. */
38
+ /**
39
+ * `embeddable` limits which secrets are also replaced inside longer strings;
40
+ * `wholeValue` limits which secrets replace a string that equals them exactly.
41
+ */
29
42
  public redactEmbedded<T>(
30
43
  value: T,
31
- embeddable: (secret: string, path: string) => boolean = () => true
44
+ embeddable: AuthConfigSecretFilter = ALWAYS,
45
+ wholeValue: AuthConfigSecretFilter = ALWAYS
32
46
  ): T {
33
- let secrets = [...this.secretToPlaceholder.entries()]
34
- .filter(([secret, placeholder]) =>
35
- embeddable(secret, placeholder.slice(AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX.length))
36
- )
37
- .map(([secret]) => secret);
38
- if (secrets.length === 0) return this.redact(value);
47
+ let embeddedPlaceholders = new Map<string, string>();
48
+ for (let secret of this.secretToPaths.keys()) {
49
+ let placeholder = this.placeholderFor(secret, embeddable);
50
+ if (placeholder) embeddedPlaceholders.set(secret, placeholder);
51
+ }
52
+ if (embeddedPlaceholders.size === 0) return this.redact(value, wholeValue);
39
53
 
40
54
  let pattern = new RegExp(
41
- secrets
55
+ [...embeddedPlaceholders.keys()]
42
56
  .sort((a, b) => b.length - a.length)
43
57
  // Escape regex metacharacters so values match literally; $& inserts the matched character.
44
58
  .map(secret => secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
@@ -47,25 +61,34 @@ export class AuthConfigSecretRedactor {
47
61
  );
48
62
 
49
63
  return this.transform(value, stringValue => {
50
- let exactPlaceholder = this.secretToPlaceholder.get(stringValue);
64
+ let exactPlaceholder = this.placeholderFor(stringValue, wholeValue);
51
65
  if (exactPlaceholder) return exactPlaceholder;
52
66
 
53
- return stringValue.replace(
54
- pattern,
55
- secret => `${this.secretToPlaceholder.get(secret)}$$`
56
- );
67
+ return stringValue.replace(pattern, secret => `${embeddedPlaceholders.get(secret)}$$`);
57
68
  });
58
69
  }
59
70
 
71
+ // The last registered path that passes the filter wins, matching the historical
72
+ // last-write-wins behavior when no filter is given.
73
+ private placeholderFor(value: string, filter: AuthConfigSecretFilter) {
74
+ let paths = this.secretToPaths.get(value);
75
+ if (!paths) return undefined;
76
+ for (let index = paths.length - 1; index >= 0; index--) {
77
+ let path = paths[index];
78
+ if (path !== undefined && filter(value, path))
79
+ return `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path}`;
80
+ }
81
+ return undefined;
82
+ }
83
+
60
84
  private buildSecretMap(value: unknown, path: string[] = []): void {
61
85
  if (typeof value === 'string') {
62
86
  // Never register "" -- it would replace every empty header/query value with a placeholder.
63
87
  if (value.length === 0) return;
64
88
 
65
- this.secretToPlaceholder.set(
66
- value,
67
- `${AUTH_CONFIG_SECRET_PLACEHOLDER_PREFIX}${path.join('.')}`
68
- );
89
+ let paths = this.secretToPaths.get(value);
90
+ if (paths) paths.push(path.join('.'));
91
+ else this.secretToPaths.set(value, [path.join('.')]);
69
92
  return;
70
93
  }
71
94
 
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { AuthConfigSecretRedactor } from '../auth/redact';
3
- import { __traceInternals } from './trace';
3
+ import {
4
+ __traceInternals,
5
+ redactAuthConfigSecretsInText,
6
+ sanitizeSecretValuesInText
7
+ } from './trace';
4
8
 
5
9
  let {
6
10
  sanitizeFreeText,
@@ -8,6 +12,8 @@ let {
8
12
  sanitizeHeaders,
9
13
  isSafeHeaderName,
10
14
  isSecretKeyName,
15
+ isSecretFieldName,
16
+ isAuthConfigSecretValue,
11
17
  isUrlFormSecretKeyName,
12
18
  redactStructuredValue
13
19
  } = __traceInternals;
@@ -246,6 +252,53 @@ describe('sanitizeFreeText', () => {
246
252
  });
247
253
  });
248
254
 
255
+ describe('sanitizeSecretValuesInText', () => {
256
+ it('redacts credential header values regardless of length', () => {
257
+ expect(sanitizeSecretValuesInText('Authorization: Bearer abc')).toBe(
258
+ 'Authorization: Bearer [redacted]'
259
+ );
260
+ expect(sanitizeSecretValuesInText('proxy-authorization=Basic xyz')).toBe(
261
+ 'proxy-authorization=Basic [redacted]'
262
+ );
263
+ expect(sanitizeSecretValuesInText('sent X-API-Key: k1 to server')).toBe(
264
+ 'sent X-API-Key: [redacted] to server'
265
+ );
266
+ expect(sanitizeSecretValuesInText('apiKey=short&page=2')).toBe('apiKey=[redacted]&page=2');
267
+ expect(sanitizeSecretValuesInText('Cookie: sid=1; theme=dark next')).toBe(
268
+ 'Cookie: [redacted] next'
269
+ );
270
+ expect(sanitizeSecretValuesInText('set-cookie: "sess=abc"')).toBe(
271
+ 'set-cookie: "[redacted]"'
272
+ );
273
+ });
274
+
275
+ it('redacts 8+ character scheme credentials but keeps prose', () => {
276
+ expect(sanitizeSecretValuesInText('got Bearer abc12345 back')).toBe(
277
+ 'got Bearer [redacted] back'
278
+ );
279
+ expect(sanitizeSecretValuesInText('Basic dXNlcjpwYXNz')).toBe('Basic [redacted]');
280
+ expect(sanitizeSecretValuesInText('token a1b2c3d4e5')).toBe('token [redacted]');
281
+
282
+ for (let prose of [
283
+ 'Invalid token provided',
284
+ 'token expired',
285
+ 'Basic authentication failed',
286
+ 'Bearer Authentication required',
287
+ 'Token verification failed',
288
+ 'Request failed with error code=404',
289
+ 'Invalid token code=404'
290
+ ]) {
291
+ expect(sanitizeSecretValuesInText(prose)).toBe(prose);
292
+ }
293
+ });
294
+
295
+ it('redacts single-quoted secret pairs', () => {
296
+ expect(
297
+ sanitizeSecretValuesInText("{'access_token': 'abc', 'token_type': 'bearer', 'ok': 'x'}")
298
+ ).toBe("{'access_token': '[redacted]', 'token_type': 'bearer', 'ok': 'x'}");
299
+ });
300
+ });
301
+
249
302
  describe('sanitizeUrl', () => {
250
303
  it('redacts common secret query params', () => {
251
304
  expect(
@@ -396,3 +449,254 @@ describe('redactStructuredValue', () => {
396
449
  expect(out).toEqual([1, 'two', { three: 3 }]);
397
450
  });
398
451
  });
452
+
453
+ describe('isSecretFieldName (split-camelCase classifier)', () => {
454
+ it('matches raw and split camelCase secret keys', () => {
455
+ for (let key of [
456
+ 'apiSecret',
457
+ 'api_secret',
458
+ 'accessKey',
459
+ 'access_key',
460
+ 'botToken',
461
+ 'bot_token',
462
+ 'githubToken',
463
+ 'github_token',
464
+ 'clientSecret',
465
+ 'sessionToken',
466
+ 'hapikey',
467
+ 'JSESSIONID',
468
+ 'PHPSESSID',
469
+ 'userSessionId',
470
+ 'secret_id'
471
+ ]) {
472
+ expect(isSecretFieldName(key), key).toBe(true);
473
+ }
474
+ });
475
+
476
+ it('skips descriptors, cursors, and ordinary ids', () => {
477
+ for (let key of [
478
+ 'tokenType',
479
+ 'tokenCount',
480
+ 'tokenExpiresAt',
481
+ 'pageToken',
482
+ 'accountId',
483
+ 'apiKeyId',
484
+ 'email',
485
+ 'subdomain'
486
+ ]) {
487
+ expect(isSecretFieldName(key), key).toBe(false);
488
+ }
489
+ });
490
+ });
491
+
492
+ describe('isAuthConfigSecretValue (auth-config paths)', () => {
493
+ it('accepts secret-named and credential-role fields', () => {
494
+ expect(isAuthConfigSecretValue('x', 'token')).toBe(true);
495
+ expect(isAuthConfigSecretValue('x', 'oauth.accessToken')).toBe(true);
496
+ expect(isAuthConfigSecretValue('x', 'apiKeys.0')).toBe(true);
497
+ expect(isAuthConfigSecretValue('x', 'subscriptionKey')).toBe(true);
498
+ expect(isAuthConfigSecretValue('x', 'writeKey')).toBe(true);
499
+ expect(isAuthConfigSecretValue('x', 'provisioningHash')).toBe(true);
500
+ expect(isAuthConfigSecretValue('Basic dXNlcjpw', 'authorizationHeader')).toBe(true);
501
+ });
502
+
503
+ it('rejects non-secret fields', () => {
504
+ for (let path of [
505
+ 'accountId',
506
+ 'email',
507
+ 'tokenType',
508
+ 'subdomain',
509
+ 'scope',
510
+ 'expiresAt',
511
+ 'clientId',
512
+ 'region',
513
+ 'oauth.tenantId'
514
+ ]) {
515
+ expect(isAuthConfigSecretValue('x', path), path).toBe(false);
516
+ }
517
+ expect(isAuthConfigSecretValue('authorization', 'tokenHeader')).toBe(false);
518
+ expect(isAuthConfigSecretValue('project-access-token', 'tokenHeader')).toBe(false);
519
+ });
520
+ });
521
+
522
+ describe('auth-config literal redaction in traces', () => {
523
+ let redactor = new AuthConfigSecretRedactor({
524
+ token: 'tg-secret-123456',
525
+ pin: '1234',
526
+ accountId: '123456',
527
+ email: 'me@acme.com',
528
+ tokenType: 'Bearer',
529
+ subdomain: 'acme',
530
+ scope: 'read',
531
+ expiresAt: '2026-10-13T12:00:00.000Z'
532
+ });
533
+
534
+ it('replaces only secret-named whole values in structured bodies', () => {
535
+ expect(
536
+ redactStructuredValue(
537
+ {
538
+ id: '123456',
539
+ email: 'me@acme.com',
540
+ type: 'Bearer',
541
+ subdomain: 'acme',
542
+ scope: 'read',
543
+ expiresAt: '2026-10-13T12:00:00.000Z',
544
+ echoed: 'tg-secret-123456',
545
+ short: '1234'
546
+ },
547
+ 0,
548
+ redactor
549
+ )
550
+ ).toEqual({
551
+ id: '123456',
552
+ email: 'me@acme.com',
553
+ type: 'Bearer',
554
+ subdomain: 'acme',
555
+ scope: 'read',
556
+ expiresAt: '2026-10-13T12:00:00.000Z',
557
+ echoed: '$$MT$secret$authConfig$token',
558
+ short: '$$MT$secret$authConfig$pin'
559
+ });
560
+ expect(sanitizeFreeText('acme', redactor)).toBe('acme');
561
+ });
562
+
563
+ it('keeps non-secret query and fragment values but replaces secret ones', () => {
564
+ expect(
565
+ sanitizeUrl(
566
+ 'https://acme.example.com/v1?account_id=123456&scope=read&echo=tg-secret-123456#sub=acme',
567
+ redactor
568
+ )
569
+ ).toBe(
570
+ 'https://acme.example.com/v1?account_id=123456&scope=read&echo=%24%24MT%24secret%24authConfig%24token#sub=acme'
571
+ );
572
+ });
573
+
574
+ it('replaces camelCase secret auth fields embedded in text', () => {
575
+ let camel = new AuthConfigSecretRedactor({
576
+ apiSecret: 'api-secret-value',
577
+ accessKey: 'access-key-value',
578
+ botToken: '123:bot-token-value',
579
+ accountName: 'acme-corporation'
580
+ });
581
+ expect(
582
+ redactAuthConfigSecretsInText(
583
+ 'api-secret-value access-key-value /bot123:bot-token-value/ acme-corporation',
584
+ camel
585
+ )
586
+ ).toBe(
587
+ '$$MT$secret$authConfig$apiSecret$$ $$MT$secret$authConfig$accessKey$$ ' +
588
+ '/bot$$MT$secret$authConfig$botToken$$/ acme-corporation'
589
+ );
590
+ });
591
+ });
592
+
593
+ describe('sanitizeSecretValuesInText header schemes', () => {
594
+ it('redacts credentials after any authorization scheme', () => {
595
+ let cases: [string, string][] = [
596
+ ['Authorization: OAuth 00D5g000004XyZ!AQ4AQ', 'Authorization: OAuth [redacted]'],
597
+ ['Authorization: Bot MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ', 'Authorization: Bot [redacted]'],
598
+ ['Authorization: SSWS 00QCjAl4MlV-WPXM', 'Authorization: SSWS [redacted]'],
599
+ [
600
+ 'Authorization: Zoho-oauthtoken 1000.abc.123 rejected',
601
+ 'Authorization: Zoho-oauthtoken [redacted] rejected'
602
+ ],
603
+ [
604
+ 'authorization: Klaviyo-API-Key pk_abc123',
605
+ 'authorization: Klaviyo-API-Key [redacted]'
606
+ ],
607
+ [
608
+ 'Authorization: Digest username="Mufasa", realm="x", nonce="dcd98b", response="6629fae49393a05397450978507c4ef1"',
609
+ 'Authorization: Digest [redacted]'
610
+ ],
611
+ [
612
+ 'Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=host, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7',
613
+ 'Authorization: AWS4-HMAC-SHA256 [redacted]'
614
+ ],
615
+ ['Proxy-Authorization: "Custom abc123def"', 'Proxy-Authorization: "Custom [redacted]"'],
616
+ ['Authorization: abc123def please retry', 'Authorization: [redacted] please retry']
617
+ ];
618
+
619
+ for (let [input, expected] of cases) {
620
+ expect(sanitizeSecretValuesInText(input), input).toBe(expected);
621
+ }
622
+ });
623
+
624
+ it('keeps header-name prose that holds no credential', () => {
625
+ for (let prose of [
626
+ 'Authorization: missing or invalid',
627
+ 'Authorization: Basic',
628
+ 'Missing cookie: please log in',
629
+ 'X-API-Key: invalid',
630
+ 'api_key: required'
631
+ ]) {
632
+ expect(sanitizeSecretValuesInText(prose), prose).toBe(prose);
633
+ }
634
+ });
635
+ });
636
+
637
+ describe('sanitizeSecretValuesInText prose key: value pairs', () => {
638
+ it('redacts secret-named pairs with credential values', () => {
639
+ let cases: [string, string][] = [
640
+ ['password: Hunter2', 'password: [redacted]'],
641
+ ['access_token: abc', 'access_token: [redacted]'],
642
+ ['refresh_token: 1//0gabc', 'refresh_token: [redacted]'],
643
+ ['sessionToken: FwoGZXIvYXdzEJr', 'sessionToken: [redacted]'],
644
+ ['secret: s3cr3tvalue', 'secret: [redacted]'],
645
+ ['Invalid token: ya29.a0AfH6SMBx.', 'Invalid token: [redacted].'],
646
+ [
647
+ "{ access_token: 'abc', token_type: 'bearer' }",
648
+ "{ access_token: '[redacted]', token_type: 'bearer' }"
649
+ ],
650
+ ['client_secret:"xyz"', 'client_secret:"[redacted]"']
651
+ ];
652
+
653
+ for (let [input, expected] of cases) {
654
+ expect(sanitizeSecretValuesInText(input), input).toBe(expected);
655
+ }
656
+ });
657
+
658
+ it('keeps state words, non-secret keys, and URLs', () => {
659
+ for (let prose of [
660
+ 'token: expired',
661
+ 'token: expired.',
662
+ 'Error fetching access token: invalid_grant',
663
+ 'refresh_token: invalid_grant',
664
+ 'password: required',
665
+ 'access_token: missing',
666
+ 'refresh_token: revoked',
667
+ 'Error: Request failed',
668
+ 'see https://api.example.com:443/v1',
669
+ 'token_type: bearer',
670
+ 'tokenCount: 12'
671
+ ]) {
672
+ expect(sanitizeSecretValuesInText(prose), prose).toBe(prose);
673
+ }
674
+ });
675
+ });
676
+
677
+ describe('sanitizeUrl credential query values', () => {
678
+ it('redacts key and hapikey params and known credential formats', () => {
679
+ let googleKey = `AIza${'b'.repeat(35)}`;
680
+ expect(sanitizeUrl(`https://maps.googleapis.com/api?key=${googleKey}&q=x`)).toBe(
681
+ 'https://maps.googleapis.com/api?key=[redacted]&q=x'
682
+ );
683
+ expect(sanitizeUrl('https://api.hubapi.com/contacts?hapikey=abc-123')).toBe(
684
+ 'https://api.hubapi.com/contacts?hapikey=[redacted]'
685
+ );
686
+ expect(sanitizeUrl(`https://example.com/x?q=sk_live_${'a'.repeat(24)}&page=2`)).toBe(
687
+ 'https://example.com/x?q=[redacted]&page=2'
688
+ );
689
+ expect(sanitizeUrl(`https://example.com/x#q=sk_live_${'a'.repeat(24)}`)).toBe(
690
+ 'https://example.com/x#q=[redacted]'
691
+ );
692
+ expect(sanitizeUrl(`not a url?q=sk_live_${'a'.repeat(24)}`)).toBe(
693
+ 'not a url?q=[redacted]'
694
+ );
695
+ });
696
+
697
+ it('keeps compound key params', () => {
698
+ expect(sanitizeUrl('https://example.com/x?sort_key=name&partition_key=p1')).toBe(
699
+ 'https://example.com/x?sort_key=name&partition_key=p1'
700
+ );
701
+ });
702
+ });