@slates/provider 1.0.0-rc.2 → 1.0.0-rc.20

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 (124) hide show
  1. package/dist/index.cjs +2847 -2
  2. package/dist/index.d.cts +1019 -0
  3. package/dist/index.d.ts +1019 -9
  4. package/dist/index.module.js +2737 -2
  5. package/package.json +9 -6
  6. package/src/action/action.ts +92 -4
  7. package/src/action/attachment.ts +87 -0
  8. package/src/action/builder.test.ts +136 -0
  9. package/src/action/builder.ts +113 -15
  10. package/src/action/directUpload.ts +59 -0
  11. package/src/action/getFileUrlTool.ts +41 -0
  12. package/src/action/index.ts +4 -2
  13. package/src/action/publicTool.ts +36 -0
  14. package/src/action/scopes.test.ts +76 -0
  15. package/src/action/scopes.ts +103 -0
  16. package/src/action/tool.ts +22 -6
  17. package/src/action/trigger.ts +19 -11
  18. package/src/adapter/adapter.ts +38 -0
  19. package/src/adapter/index.test.ts +322 -0
  20. package/src/adapter/index.ts +2 -0
  21. package/src/adapter/spec.ts +154 -0
  22. package/src/auth/auth.ts +3 -3
  23. package/src/auth/index.ts +2 -0
  24. package/src/auth/oauth.test.ts +83 -0
  25. package/src/auth/oauth.ts +150 -0
  26. package/src/auth/redact.test.ts +103 -0
  27. package/src/auth/redact.ts +111 -0
  28. package/src/auth/types.ts +72 -13
  29. package/src/axios/http.test.ts +83 -0
  30. package/src/axios/index.ts +116 -10
  31. package/src/axios/trace.test.ts +377 -0
  32. package/src/axios/trace.ts +803 -0
  33. package/src/config/config.ts +14 -3
  34. package/src/context/context.ts +264 -17
  35. package/src/context/hook.test.ts +46 -0
  36. package/src/context/hook.ts +15 -6
  37. package/src/context/index.ts +1 -0
  38. package/src/context/pQueue.ts +4 -0
  39. package/src/crypto/hmac.test.ts +40 -0
  40. package/src/crypto/hmac.ts +32 -0
  41. package/src/crypto/index.ts +1 -0
  42. package/src/docs.ts +22 -0
  43. package/src/error/api.test.ts +114 -0
  44. package/src/error/api.ts +236 -0
  45. package/src/error/axios.ts +220 -0
  46. package/src/error/base.test.ts +178 -0
  47. package/src/error/base.ts +108 -2
  48. package/src/error/declaration.ts +7 -2
  49. package/src/error/defaults.ts +50 -0
  50. package/src/error/index.ts +4 -0
  51. package/src/error/service.ts +170 -0
  52. package/src/error/types.ts +75 -0
  53. package/src/error/utils.ts +157 -0
  54. package/src/file/index.test.ts +36 -0
  55. package/src/file/index.ts +27 -0
  56. package/src/index.ts +8 -0
  57. package/src/logger/logger.test.ts +57 -0
  58. package/src/logger/logger.ts +72 -11
  59. package/src/module/resolveDefaultExport.test.ts +32 -0
  60. package/src/module/resolveDefaultExport.ts +14 -0
  61. package/src/object/index.test.ts +38 -0
  62. package/src/object/index.ts +12 -0
  63. package/src/specification/slate.ts +40 -4
  64. package/src/specification/specification.ts +10 -6
  65. package/src/webhook/graph.test.ts +37 -0
  66. package/src/webhook/graph.ts +22 -0
  67. package/src/webhook/index.ts +2 -0
  68. package/src/webhook/meta.test.ts +52 -0
  69. package/src/webhook/meta.ts +43 -0
  70. package/dist/action/action.d.ts +0 -98
  71. package/dist/action/action.d.ts.map +0 -1
  72. package/dist/action/builder.d.ts +0 -27
  73. package/dist/action/builder.d.ts.map +0 -1
  74. package/dist/action/index.d.ts +0 -5
  75. package/dist/action/index.d.ts.map +0 -1
  76. package/dist/action/tool.d.ts +0 -14
  77. package/dist/action/tool.d.ts.map +0 -1
  78. package/dist/action/trigger.d.ts +0 -32
  79. package/dist/action/trigger.d.ts.map +0 -1
  80. package/dist/auth/auth.d.ts +0 -30
  81. package/dist/auth/auth.d.ts.map +0 -1
  82. package/dist/auth/index.d.ts +0 -3
  83. package/dist/auth/index.d.ts.map +0 -1
  84. package/dist/auth/types.d.ts +0 -148
  85. package/dist/auth/types.d.ts.map +0 -1
  86. package/dist/axios/index.d.ts +0 -4
  87. package/dist/axios/index.d.ts.map +0 -1
  88. package/dist/config/config.d.ts +0 -26
  89. package/dist/config/config.d.ts.map +0 -1
  90. package/dist/config/index.d.ts +0 -2
  91. package/dist/config/index.d.ts.map +0 -1
  92. package/dist/context/context.d.ts +0 -20
  93. package/dist/context/context.d.ts.map +0 -1
  94. package/dist/context/hook.d.ts +0 -4
  95. package/dist/context/hook.d.ts.map +0 -1
  96. package/dist/context/index.d.ts +0 -2
  97. package/dist/context/index.d.ts.map +0 -1
  98. package/dist/error/base.d.ts +0 -5
  99. package/dist/error/base.d.ts.map +0 -1
  100. package/dist/error/declaration.d.ts +0 -6
  101. package/dist/error/declaration.d.ts.map +0 -1
  102. package/dist/error/index.d.ts +0 -3
  103. package/dist/error/index.d.ts.map +0 -1
  104. package/dist/index.cjs.map +0 -1
  105. package/dist/index.d.ts.map +0 -1
  106. package/dist/index.modern.js +0 -2
  107. package/dist/index.modern.js.map +0 -1
  108. package/dist/index.module.js.map +0 -1
  109. package/dist/index.umd.js +0 -2
  110. package/dist/index.umd.js.map +0 -1
  111. package/dist/logger/index.d.ts +0 -2
  112. package/dist/logger/index.d.ts.map +0 -1
  113. package/dist/logger/logger.d.ts +0 -20
  114. package/dist/logger/logger.d.ts.map +0 -1
  115. package/dist/specification/index.d.ts +0 -3
  116. package/dist/specification/index.d.ts.map +0 -1
  117. package/dist/specification/slate.d.ts +0 -15
  118. package/dist/specification/slate.d.ts.map +0 -1
  119. package/dist/specification/specification.d.ts +0 -30
  120. package/dist/specification/specification.d.ts.map +0 -1
  121. package/dist/specification/zero.d.ts +0 -13
  122. package/dist/specification/zero.d.ts.map +0 -1
  123. package/dist/tokens.d.ts +0 -34
  124. package/dist/tokens.d.ts.map +0 -1
@@ -1,28 +1,134 @@
1
- import baseAxios, { CreateAxiosDefaults } from 'axios';
1
+ import type {
2
+ AxiosInstance,
3
+ AxiosResponse,
4
+ CreateAxiosDefaults,
5
+ InternalAxiosRequestConfig
6
+ } from 'axios';
7
+ import baseAxios, { getAdapter, isAxiosError } from 'axios';
2
8
  import { getCurrentContext } from '../context/hook';
9
+ import type { SlateAxiosErrorOptions } from '../error';
10
+ import { SlateError } from '../error';
11
+ import {
12
+ attachHttpTraceDraft,
13
+ recordHttpTraceFromError,
14
+ recordHttpTraceFromResponse
15
+ } from './trace';
3
16
 
4
- export let createAxios = (config?: CreateAxiosDefaults) => {
5
- let instance = baseAxios.create({
6
- ...config,
7
- headers: {
8
- ...config?.headers
9
- }
10
- });
17
+ export interface SlateAxiosDefaults extends CreateAxiosDefaults {
18
+ errorMapping?: SlateAxiosErrorOptions;
19
+ }
11
20
 
21
+ export type SlateAxiosErrorAdapter = (error: unknown, operation: string) => unknown;
22
+
23
+ export interface SlateAuthenticatedAxiosDefaults extends SlateAxiosDefaults {
24
+ authHeader?: {
25
+ name?: string;
26
+ value: string;
27
+ };
28
+ contentType?: string | false;
29
+ errorAdapter?: (error: unknown) => unknown;
30
+ }
31
+
32
+ let applySlateInterceptors = (
33
+ instance: AxiosInstance,
34
+ errorMapping?: SlateAxiosErrorOptions
35
+ ) => {
12
36
  instance.interceptors.request.use(
13
37
  request => {
14
38
  // Has to be called in the context of an action execution
15
39
  let ctx = getCurrentContext();
16
40
  let spec = ctx.specification;
41
+ let providerToken = spec.key.replace(/[^A-Za-z0-9!#$%&'*+\-.^_`|~]/g, '-');
42
+
43
+ request.headers.set('User-Agent', `slates.dev/1.0.0 ${providerToken || 'unknown'}`);
44
+ request.headers.set('X-Slates-Provider', spec.key);
45
+
46
+ let tracedRequest = attachHttpTraceDraft(request, ctx) as InternalAxiosRequestConfig & {
47
+ __slatesTraceAdapterWrapped?: boolean;
48
+ };
17
49
 
18
- request.headers.set('User-Agent', `slates.dev@1.0.0/${spec.key}`);
50
+ if (!tracedRequest.__slatesTraceAdapterWrapped) {
51
+ let adapter = getAdapter(tracedRequest.adapter ?? baseAxios.defaults.adapter);
52
+ tracedRequest.adapter = async config => {
53
+ try {
54
+ let response = await adapter(config);
55
+ return recordHttpTraceFromResponse(response);
56
+ } catch (error) {
57
+ if (isAxiosError(error)) {
58
+ recordHttpTraceFromError(error);
59
+ }
60
+ throw error;
61
+ }
62
+ };
63
+ tracedRequest.__slatesTraceAdapterWrapped = true;
64
+ }
19
65
 
20
- return request;
66
+ return tracedRequest;
21
67
  },
22
68
  error => Promise.reject(error)
23
69
  );
24
70
 
71
+ instance.interceptors.response.use(
72
+ response => response,
73
+ error => Promise.reject(SlateError.fromAxios(error, errorMapping))
74
+ );
75
+ };
76
+
77
+ applySlateInterceptors(baseAxios);
78
+
79
+ export let createAxios = (config?: SlateAxiosDefaults) => {
80
+ let { errorMapping, ...axiosConfig } = config ?? {};
81
+ let instance = baseAxios.create({
82
+ ...axiosConfig,
83
+ headers: {
84
+ ...axiosConfig.headers
85
+ }
86
+ });
87
+
88
+ applySlateInterceptors(instance, errorMapping);
89
+ return instance;
90
+ };
91
+
92
+ export let createAuthenticatedAxios = (config: SlateAuthenticatedAxiosDefaults) => {
93
+ let { authHeader, contentType = 'application/json', errorAdapter, ...axiosConfig } = config;
94
+ let instance = createAxios({
95
+ ...axiosConfig,
96
+ headers: {
97
+ ...(contentType === false ? {} : { 'Content-Type': contentType }),
98
+ ...(authHeader ? { [authHeader.name ?? 'Authorization']: authHeader.value } : {}),
99
+ ...axiosConfig.headers
100
+ }
101
+ });
102
+
103
+ if (errorAdapter) {
104
+ instance.interceptors.response.use(
105
+ response => response,
106
+ error => Promise.reject(errorAdapter(error))
107
+ );
108
+ }
109
+
25
110
  return instance;
26
111
  };
27
112
 
113
+ export let requestAxios = async <T = unknown>(
114
+ operation: string,
115
+ request: () => Promise<AxiosResponse<T>>,
116
+ errorAdapter: SlateAxiosErrorAdapter
117
+ ) => {
118
+ try {
119
+ return await request();
120
+ } catch (error) {
121
+ throw errorAdapter(error, operation);
122
+ }
123
+ };
124
+
125
+ export let requestAxiosData = async <T = unknown>(
126
+ operation: string,
127
+ request: () => Promise<AxiosResponse<T>>,
128
+ errorAdapter: SlateAxiosErrorAdapter
129
+ ) => {
130
+ let response = await requestAxios(operation, request, errorAdapter);
131
+ return response.data as T;
132
+ };
133
+
28
134
  export let axios = createAxios();
@@ -0,0 +1,377 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __traceInternals } from './trace';
3
+
4
+ let {
5
+ sanitizeFreeText,
6
+ sanitizeUrl,
7
+ sanitizeHeaders,
8
+ isSafeHeaderName,
9
+ isSecretKeyName,
10
+ isUrlFormSecretKeyName,
11
+ redactStructuredValue
12
+ } = __traceInternals;
13
+
14
+ describe('isSecretKeyName (structured keys)', () => {
15
+ let secretLike = [
16
+ 'authorization',
17
+ 'Authorization',
18
+ 'proxy-authorization',
19
+ 'x-api-key',
20
+ 'X-Api-Key',
21
+ 'apikey',
22
+ 'api_key',
23
+ 'api.key',
24
+ 'access_token',
25
+ 'refresh-token',
26
+ 'id_token',
27
+ 'client_secret',
28
+ 'client-secret',
29
+ 'consumer_secret',
30
+ 'private_key',
31
+ 'privateKey',
32
+ 'webhook-signing-secret',
33
+ 'auth_code',
34
+ 'authorization_code',
35
+ 'code_verifier',
36
+ 'code_challenge',
37
+ 'password',
38
+ 'passwd',
39
+ 'pwd',
40
+ 'passphrase',
41
+ 'pin',
42
+ 'passcode',
43
+ 'secret',
44
+ 'credentials',
45
+ 'creds',
46
+ 'x-auth',
47
+ 'x-csrf-token',
48
+ 'x-xsrf-token',
49
+ 'jwt',
50
+ 'session-id',
51
+ 'sid',
52
+ 'cookie',
53
+ 'set-cookie',
54
+ 'x-hub-signature',
55
+ 'signature',
56
+ 'x-sig',
57
+ 'hmac',
58
+ 'salt',
59
+ 'nonce',
60
+ 'otp',
61
+ 'totp',
62
+ 'mfa',
63
+ '2fa',
64
+ 'verification-code',
65
+ 'sms_code',
66
+ 'mnemonic',
67
+ 'recovery-key',
68
+ 'card_number',
69
+ 'cvv',
70
+ 'cvc',
71
+ 'ssn',
72
+ 'orgname_access_token',
73
+ 'my.private.key'
74
+ ];
75
+
76
+ let nonSecretLike = [
77
+ 'accept',
78
+ 'content-type',
79
+ 'user-agent',
80
+ 'etag',
81
+ 'location',
82
+ 'retry-after',
83
+ 'x-request-id',
84
+ 'configure',
85
+ 'config',
86
+ 'design',
87
+ 'signal',
88
+ 'assignment',
89
+ 'condescend',
90
+ 'code',
91
+ 'status-code',
92
+ 'error-code',
93
+ 'http-code',
94
+ 'status',
95
+ 'encode',
96
+ 'decoder',
97
+ 'object-key',
98
+ 'partition-key',
99
+ 'sort-key',
100
+ 'first-name',
101
+ 'last-name',
102
+ 'email-address',
103
+ 'phone',
104
+ 'timestamp',
105
+ 'created-at',
106
+ 'token_type',
107
+ 'token-type',
108
+ 'grant_type',
109
+ 'grant-type',
110
+ 'auth_type',
111
+ 'auth_method',
112
+ 'signature_method',
113
+ 'signature_version',
114
+ 'key_id',
115
+ 'kid',
116
+ 'key_name',
117
+ 'password_strength'
118
+ ];
119
+
120
+ for (let key of secretLike) {
121
+ it(`treats "${key}" as secret`, () => {
122
+ expect(isSecretKeyName(key)).toBe(true);
123
+ });
124
+ }
125
+
126
+ for (let key of nonSecretLike) {
127
+ it(`leaves "${key}" alone`, () => {
128
+ expect(isSecretKeyName(key)).toBe(false);
129
+ });
130
+ }
131
+ });
132
+
133
+ describe('isUrlFormSecretKeyName (URL/form keys)', () => {
134
+ it('flags OAuth auth code key', () => {
135
+ expect(isUrlFormSecretKeyName('code')).toBe(true);
136
+ });
137
+
138
+ it('does not flag grant_type or token_type', () => {
139
+ expect(isUrlFormSecretKeyName('grant_type')).toBe(false);
140
+ expect(isUrlFormSecretKeyName('token_type')).toBe(false);
141
+ });
142
+
143
+ it('does not flag common non-secret keys', () => {
144
+ expect(isUrlFormSecretKeyName('page')).toBe(false);
145
+ expect(isUrlFormSecretKeyName('limit')).toBe(false);
146
+ expect(isUrlFormSecretKeyName('q')).toBe(false);
147
+ });
148
+ });
149
+
150
+ describe('sanitizeFreeText', () => {
151
+ it('redacts Bearer tokens', () => {
152
+ expect(sanitizeFreeText('Authorization: Bearer abc123XYZ==')).toBe(
153
+ 'Authorization: Bearer [redacted]'
154
+ );
155
+ });
156
+
157
+ it('redacts Basic/Digest/Negotiate schemes', () => {
158
+ expect(sanitizeFreeText('Basic dXNlcjpwdw==')).toBe('Basic [redacted]');
159
+ expect(sanitizeFreeText('Digest abcdef123456')).toBe('Digest [redacted]');
160
+ expect(sanitizeFreeText('Negotiate YIIFigYGKwYB')).toBe('Negotiate [redacted]');
161
+ });
162
+
163
+ it('redacts JWT tokens by value', () => {
164
+ let jwt =
165
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
166
+ expect(sanitizeFreeText(`token=${jwt}`)).not.toContain(jwt);
167
+ expect(sanitizeFreeText(`user said: ${jwt} ok`)).toContain('[redacted]');
168
+ expect(sanitizeFreeText(`user said: ${jwt} ok`)).not.toContain(jwt);
169
+ });
170
+
171
+ it('redacts AWS access key IDs', () => {
172
+ let key = 'AKIAIOSFODNN7EXAMPLE';
173
+ expect(sanitizeFreeText(`AWS key ${key} embedded`)).not.toContain(key);
174
+ });
175
+
176
+ it('redacts GitHub tokens', () => {
177
+ let token = `ghp_${'a'.repeat(40)}`;
178
+ expect(sanitizeFreeText(token)).toBe('[redacted]');
179
+ });
180
+
181
+ it('redacts Slack tokens', () => {
182
+ let token = 'xoxb-1234567890-abcdefghij';
183
+ expect(sanitizeFreeText(`token=${token}`)).not.toContain(token);
184
+ });
185
+
186
+ it('redacts Stripe secret keys', () => {
187
+ let key = `sk_live_${'a'.repeat(24)}`;
188
+ expect(sanitizeFreeText(`{"key":"${key}"}`)).not.toContain(key);
189
+ });
190
+
191
+ it('redacts OpenAI API keys', () => {
192
+ let key = `sk-proj-${'a'.repeat(40)}`;
193
+ expect(sanitizeFreeText(key)).toBe('[redacted]');
194
+ });
195
+
196
+ it('redacts Anthropic API keys', () => {
197
+ let key = `sk-ant-${'a'.repeat(40)}`;
198
+ expect(sanitizeFreeText(key)).toBe('[redacted]');
199
+ });
200
+
201
+ it('redacts Google API keys', () => {
202
+ let key = `AIza${'a'.repeat(35)}`;
203
+ expect(sanitizeFreeText(key)).toBe('[redacted]');
204
+ });
205
+
206
+ it('redacts PEM private key blocks', () => {
207
+ let pem =
208
+ '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----';
209
+ let input = `key: ${pem}`;
210
+ let redacted = sanitizeFreeText(input);
211
+ expect(redacted).not.toContain('MIIEpAIBAAK');
212
+ expect(redacted).toContain('[redacted]');
213
+ });
214
+
215
+ it('redacts form-encoded OAuth auth code', () => {
216
+ expect(
217
+ sanitizeFreeText('grant_type=authorization_code&code=abc123&client_secret=xyz')
218
+ ).toBe('grant_type=authorization_code&code=[redacted]&client_secret=[redacted]');
219
+ });
220
+
221
+ it('redacts form-encoded client_secret without touching grant_type', () => {
222
+ expect(sanitizeFreeText('grant_type=password&password=hunter2')).toBe(
223
+ 'grant_type=password&password=[redacted]'
224
+ );
225
+ });
226
+
227
+ it('redacts JSON api_key / secret / token values in stringified JSON', () => {
228
+ let input = JSON.stringify({
229
+ api_key: 'abc',
230
+ access_token: 'tkn',
231
+ refresh_token: 'ref',
232
+ signature: 'sig',
233
+ nested: 'ok'
234
+ });
235
+ let out = sanitizeFreeText(input);
236
+ expect(out).toContain('"api_key":"[redacted]"');
237
+ expect(out).toContain('"access_token":"[redacted]"');
238
+ expect(out).toContain('"refresh_token":"[redacted]"');
239
+ expect(out).toContain('"signature":"[redacted]"');
240
+ expect(out).toContain('"nested":"ok"');
241
+ });
242
+
243
+ it('does not redact innocuous key=value pairs', () => {
244
+ expect(sanitizeFreeText('page=2&limit=10')).toBe('page=2&limit=10');
245
+ });
246
+ });
247
+
248
+ describe('sanitizeUrl', () => {
249
+ it('redacts common secret query params', () => {
250
+ expect(
251
+ sanitizeUrl('https://example.com/cb?api_key=abc&client_secret=def&visible=yes')
252
+ ).toBe('https://example.com/cb?api_key=[redacted]&client_secret=[redacted]&visible=yes');
253
+ });
254
+
255
+ it('redacts OAuth auth code in query', () => {
256
+ expect(sanitizeUrl('https://example.com/cb?code=abc&state=xyz')).toBe(
257
+ 'https://example.com/cb?code=[redacted]&state=xyz'
258
+ );
259
+ });
260
+
261
+ it('strips basic auth userinfo', () => {
262
+ expect(sanitizeUrl('https://user:pass@example.com/api?foo=bar')).toBe(
263
+ 'https://example.com/api?foo=bar'
264
+ );
265
+ });
266
+
267
+ it('redacts secrets embedded in URL fragment (OAuth implicit flow)', () => {
268
+ let input = 'https://example.com/cb#access_token=xyz&token_type=bearer&expires_in=3600';
269
+ let out = sanitizeUrl(input);
270
+ expect(out).toContain('access_token=[redacted]');
271
+ expect(out).toContain('token_type=bearer');
272
+ expect(out).toContain('expires_in=3600');
273
+ });
274
+
275
+ it('leaves innocuous URLs unchanged', () => {
276
+ expect(sanitizeUrl('https://example.com/api?page=2&limit=10')).toBe(
277
+ 'https://example.com/api?page=2&limit=10'
278
+ );
279
+ });
280
+ });
281
+
282
+ describe('sanitizeHeaders', () => {
283
+ it('drops headers with secret-like names', () => {
284
+ let sanitized = sanitizeHeaders({
285
+ authorization: 'Bearer xyz',
286
+ 'x-api-key': 'key',
287
+ 'x-auth-token': 'tkn',
288
+ 'x-csrf-token': 'csrf',
289
+ cookie: 'session=abc',
290
+ 'set-cookie': 'session=abc',
291
+ 'x-hub-signature': 'sig',
292
+ 'x-auth-code': 'code'
293
+ });
294
+ expect(sanitized).toBeUndefined();
295
+ });
296
+
297
+ it('keeps safe headers and redacts tokens in their values', () => {
298
+ let sanitized = sanitizeHeaders({
299
+ 'content-type': 'application/json',
300
+ accept: 'application/json',
301
+ 'user-agent': 'test-agent',
302
+ 'x-request-id': 'req-123'
303
+ });
304
+ expect(sanitized).toEqual({
305
+ 'content-type': 'application/json',
306
+ accept: 'application/json',
307
+ 'user-agent': 'test-agent',
308
+ 'x-request-id': 'req-123'
309
+ });
310
+ });
311
+
312
+ it('redacts JWTs/secret tokens that leak into safe header values', () => {
313
+ let jwt =
314
+ 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2huIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
315
+ let sanitized = sanitizeHeaders({
316
+ 'user-agent': `my-app/${jwt}`
317
+ });
318
+ expect(sanitized?.['user-agent']).not.toContain(jwt);
319
+ expect(sanitized?.['user-agent']).toContain('[redacted]');
320
+ });
321
+ });
322
+
323
+ describe('isSafeHeaderName', () => {
324
+ it('rejects common compound secret header names', () => {
325
+ expect(isSafeHeaderName('x-amz-security-token')).toBe(false);
326
+ expect(isSafeHeaderName('x-org-api-key')).toBe(false);
327
+ expect(isSafeHeaderName('authorization')).toBe(false);
328
+ expect(isSafeHeaderName('x-auth-code')).toBe(false);
329
+ expect(isSafeHeaderName('x-webhook-secret')).toBe(false);
330
+ });
331
+
332
+ it('does not falsely reject look-alike names', () => {
333
+ expect(isSafeHeaderName('x-correlation-id')).toBe(true);
334
+ expect(isSafeHeaderName('content-type')).toBe(true);
335
+ });
336
+
337
+ it('allows ratelimit-* families', () => {
338
+ expect(isSafeHeaderName('ratelimit-remaining')).toBe(true);
339
+ expect(isSafeHeaderName('x-ratelimit-reset')).toBe(true);
340
+ });
341
+ });
342
+
343
+ describe('redactStructuredValue', () => {
344
+ it('replaces secret-named fields with [redacted]', () => {
345
+ let out = redactStructuredValue({
346
+ api_key: 'abc',
347
+ accessToken: 'xyz',
348
+ nested: { client_secret: 'nested-secret', other: 'visible' },
349
+ list: ['a', 'b']
350
+ });
351
+
352
+ expect(out).toEqual({
353
+ api_key: '[redacted]',
354
+ accessToken: '[redacted]',
355
+ nested: { client_secret: '[redacted]', other: 'visible' },
356
+ list: ['a', 'b']
357
+ });
358
+ });
359
+
360
+ it('scrubs JWT values inside unrelated fields', () => {
361
+ let jwt =
362
+ 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2huIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
363
+ let out = redactStructuredValue({ message: `user: ${jwt}` }) as { message: string };
364
+ expect(out.message).not.toContain(jwt);
365
+ expect(out.message).toContain('[redacted]');
366
+ });
367
+
368
+ it('does not redact HTTP status code integers', () => {
369
+ let out = redactStructuredValue({ error: 'not_found', code: 404 });
370
+ expect(out).toEqual({ error: 'not_found', code: 404 });
371
+ });
372
+
373
+ it('preserves arrays and numerics', () => {
374
+ let out = redactStructuredValue([1, 'two', { three: 3 }]);
375
+ expect(out).toEqual([1, 'two', { three: 3 }]);
376
+ });
377
+ });