@slates/provider 1.0.0-rc.28 → 1.0.0-rc.29
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/dist/index.cjs +2321 -1918
- package/dist/index.d.cts +35 -6
- package/dist/index.d.ts +35 -6
- package/dist/index.module.js +2312 -1918
- package/package.json +1 -1
- package/src/auth/redact.test.ts +29 -0
- package/src/auth/redact.ts +44 -21
- package/src/axios/trace.test.ts +305 -1
- package/src/axios/trace.ts +330 -16
- package/src/error/api.test.ts +123 -1
- package/src/error/api.ts +148 -0
- package/src/error/axios.ts +48 -13
- package/src/error/utils.test.ts +654 -0
- package/src/error/utils.ts +268 -13
package/package.json
CHANGED
package/src/auth/redact.test.ts
CHANGED
|
@@ -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
|
|
package/src/auth/redact.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
34
|
+
return this.placeholderFor(stringValue, wholeValue) ?? stringValue;
|
|
25
35
|
});
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
/**
|
|
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:
|
|
44
|
+
embeddable: AuthConfigSecretFilter = ALWAYS,
|
|
45
|
+
wholeValue: AuthConfigSecretFilter = ALWAYS
|
|
32
46
|
): T {
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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.
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
package/src/axios/trace.test.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { AuthConfigSecretRedactor } from '../auth/redact';
|
|
3
|
-
import {
|
|
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
|
+
});
|