Skip to content

Commit 46b63e4

Browse files
committed
feat(respect-core): don't do arrayBufferToBase64
1 parent 5e64262 commit 46b63e4

File tree

12 files changed

+282
-52
lines changed

12 files changed

+282
-52
lines changed

packages/cli/src/commands/respect/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { resolveMtlsCertificates } from './mtls/resolve-mtls-certificates.js';
1111
import { withMtlsClientIfNeeded } from './mtls/create-mtls-client.js';
1212
import { withHar } from './har-logs/index.js';
1313
import { createHarLog } from './har-logs/har-logs.js';
14+
import { jsonStringifyWithArrayBuffer } from '../../utils/json-stringify-with-array-buffer.js';
1415

1516
export type RespectArgv = {
1617
files: string[];
@@ -129,7 +130,7 @@ export async function handleRespect({
129130
totalTime: performance.now() - startedAt,
130131
} as JsonLogs;
131132

132-
writeFileSync(argv['json-output'], JSON.stringify(jsonOutputData, null, 2), 'utf-8');
133+
writeFileSync(argv['json-output'], jsonStringifyWithArrayBuffer(jsonOutputData, 2), 'utf-8');
133134

134135
logger.output(blue(logger.indent(`JSON logs saved in ${green(argv['json-output'])}`, 2)));
135136
logger.printNewLine();
@@ -140,7 +141,7 @@ export async function handleRespect({
140141
// TODO: implement multiple run files HAR output
141142
for (const result of runAllFilesResult) {
142143
const parsedHarLogs = maskSecrets(harLogs, result.ctx.secretFields || new Set());
143-
writeFileSync(argv['har-output'], JSON.stringify(parsedHarLogs, null, 2), 'utf-8');
144+
writeFileSync(argv['har-output'], jsonStringifyWithArrayBuffer(parsedHarLogs, 2), 'utf-8');
144145
logger.output(blue(`Har logs saved in ${green(argv['har-output'])}`));
145146
logger.printNewLine();
146147
logger.printNewLine();
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { jsonStringifyWithArrayBuffer } from '../json-stringify-with-array-buffer.js';
2+
3+
describe('jsonStringifyWithArrayBuffer', () => {
4+
it('should serialize ArrayBuffer to base64', () => {
5+
const arrayBuffer = new ArrayBuffer(4);
6+
const uint8Array = new Uint8Array(arrayBuffer);
7+
uint8Array.set([1, 2, 3, 4]);
8+
9+
const obj = { binaryData: arrayBuffer };
10+
const result = jsonStringifyWithArrayBuffer(obj);
11+
const parsed = JSON.parse(result);
12+
13+
expect(parsed.binaryData).toEqual({
14+
__type: 'ArrayBuffer',
15+
data: 'AQIDBA==', // base64 of [1, 2, 3, 4]
16+
byteLength: 4,
17+
});
18+
});
19+
20+
it('should serialize File objects', () => {
21+
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
22+
23+
const obj = { fileData: file };
24+
const result = jsonStringifyWithArrayBuffer(obj);
25+
const parsed = JSON.parse(result);
26+
27+
expect(parsed.fileData).toEqual({
28+
__type: 'File',
29+
name: 'test.txt',
30+
size: 12,
31+
type: 'text/plain',
32+
lastModified: expect.any(Number),
33+
});
34+
});
35+
36+
it('should handle mixed objects with ArrayBuffer and File', () => {
37+
const arrayBuffer = new ArrayBuffer(2);
38+
const uint8Array = new Uint8Array(arrayBuffer);
39+
uint8Array.set([10, 20]);
40+
41+
const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });
42+
43+
const obj = {
44+
binary: arrayBuffer,
45+
file: file,
46+
normal: 'string value',
47+
number: 42,
48+
};
49+
50+
const result = jsonStringifyWithArrayBuffer(obj);
51+
const parsed = JSON.parse(result);
52+
53+
expect(parsed.binary).toEqual({
54+
__type: 'ArrayBuffer',
55+
data: 'ChQ=', // base64 of [10, 20]
56+
byteLength: 2,
57+
});
58+
59+
expect(parsed.file).toEqual({
60+
__type: 'File',
61+
name: 'hello.txt',
62+
size: 5,
63+
type: 'text/plain',
64+
lastModified: expect.any(Number),
65+
});
66+
67+
expect(parsed.normal).toBe('string value');
68+
expect(parsed.number).toBe(42);
69+
});
70+
71+
it('should handle nested objects', () => {
72+
const arrayBuffer = new ArrayBuffer(1);
73+
const uint8Array = new Uint8Array(arrayBuffer);
74+
uint8Array.set([100]);
75+
76+
const file = new File(['nested'], 'nested.txt', { type: 'text/plain' });
77+
78+
const obj = {
79+
level1: {
80+
level2: {
81+
binary: arrayBuffer,
82+
file: file,
83+
},
84+
},
85+
};
86+
87+
const result = jsonStringifyWithArrayBuffer(obj);
88+
const parsed = JSON.parse(result);
89+
90+
expect(parsed.level1.level2.binary).toEqual({
91+
__type: 'ArrayBuffer',
92+
data: 'ZA==', // base64 of [100]
93+
byteLength: 1,
94+
});
95+
96+
expect(parsed.level1.level2.file).toEqual({
97+
__type: 'File',
98+
name: 'nested.txt',
99+
size: 6,
100+
type: 'text/plain',
101+
lastModified: expect.any(Number),
102+
});
103+
});
104+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export function jsonStringifyWithArrayBuffer(obj: any, space?: string | number): string {
2+
return JSON.stringify(
3+
obj,
4+
(key, value) => {
5+
if (value instanceof ArrayBuffer) {
6+
// Convert ArrayBuffer to base64 string for JSON serialization
7+
const uint8Array = new Uint8Array(value);
8+
const base64 = Buffer.from(uint8Array).toString('base64');
9+
return {
10+
__type: 'ArrayBuffer',
11+
data: base64,
12+
byteLength: value.byteLength,
13+
};
14+
}
15+
16+
if (value instanceof File) {
17+
// Convert File to a serializable object - avoid accessing properties to prevent errors
18+
return {
19+
__type: 'File',
20+
name: value.name || '[File Object]',
21+
size: value.size || 0,
22+
type: value.type || '',
23+
lastModified: value.lastModified || 0,
24+
};
25+
}
26+
27+
return value;
28+
},
29+
space
30+
);
31+
}

packages/respect-core/src/modules/__tests__/logger-output/mask-secrets.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,33 @@ describe('maskSecrets', () => {
9595
const result = maskSecrets('Bearer token123456', new Set(['token123456']));
9696
expect(result).toEqual('Bearer ********');
9797
});
98+
99+
it('should preserve ArrayBuffer objects without breaking them', () => {
100+
const originalArrayBuffer = new ArrayBuffer(8);
101+
const originalData = new Uint8Array(originalArrayBuffer);
102+
originalData.set([1, 2, 3, 4, 5, 6, 7, 8]); // Set some test data
103+
104+
const objWithArrayBuffer = {
105+
binaryData: originalArrayBuffer,
106+
token: 'secret123',
107+
nested: {
108+
anotherBinary: originalArrayBuffer,
109+
password: 'password456',
110+
},
111+
};
112+
113+
const result = maskSecrets(objWithArrayBuffer, new Set(['secret123', 'password456']));
114+
115+
// Verify ArrayBuffer is preserved
116+
expect(result.binaryData).toBe(originalArrayBuffer);
117+
expect(result.nested.anotherBinary).toBe(originalArrayBuffer);
118+
119+
// Verify the data inside ArrayBuffer is intact
120+
const resultData = new Uint8Array(result.binaryData);
121+
expect(resultData).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]));
122+
123+
// Verify secrets are masked
124+
expect(result.token).toBe('********');
125+
expect(result.nested.password).toBe('********');
126+
});
98127
});

packages/respect-core/src/modules/logger-output/display-checks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ function displayVerboseLogs({
9898
formattedBody = JSON.stringify(JSON.parse(body), null, 2);
9999
} else if (body instanceof File) {
100100
formattedBody = `[File: ${body.name}]`;
101+
} else if (body instanceof ArrayBuffer) {
102+
formattedBody = `[Binary: ${body.byteLength} bytes]`;
101103
} else {
102104
formattedBody = body;
103105
}

packages/respect-core/src/modules/logger-output/mask-secrets.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { deepCopy } from '../../utils/deepCopy.js';
2+
13
export const POTENTIALLY_SECRET_FIELDS = [
24
'token',
35
'access_token',
@@ -22,7 +24,7 @@ export function maskSecrets<T extends { [x: string]: any } | string>(
2224
return maskedString as T;
2325
}
2426

25-
const masked = JSON.parse(JSON.stringify(target));
27+
const masked = deepCopy(target);
2628
const maskIfContainsSecret = (value: string): string => {
2729
let maskedValue = value;
2830

@@ -40,7 +42,21 @@ export function maskSecrets<T extends { [x: string]: any } | string>(
4042
if (typeof current[key] === 'string') {
4143
current[key] = maskIfContainsSecret(current[key]);
4244
} else if (typeof current[key] === 'object' && current[key] !== null) {
43-
maskRecursive(current[key]);
45+
// Skip special objects that should not be modified
46+
if (
47+
!(current[key] instanceof File) &&
48+
!(current[key] instanceof ArrayBuffer) &&
49+
!(current[key] instanceof Blob) &&
50+
!(current[key] instanceof FormData) &&
51+
!(current[key] instanceof Date) &&
52+
!(current[key] instanceof RegExp) &&
53+
!(current[key] instanceof Map) &&
54+
!(current[key] instanceof Set) &&
55+
!(current[key] instanceof URL) &&
56+
!(current[key] instanceof Error)
57+
) {
58+
maskRecursive(current[key]);
59+
}
4460
}
4561
}
4662
};

packages/respect-core/src/modules/logger-output/verbose-logs.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ export function getVerboseLogs({
1818
if (headerParams && Object.keys(headerParams).length > 0) {
1919
verboseLogs.headerParams = headerParams;
2020
}
21-
if (body && (body instanceof FormData || body instanceof File || Object.keys(body).length > 0)) {
21+
if (
22+
body &&
23+
(body instanceof FormData ||
24+
body instanceof File ||
25+
Object.keys(body).length > 0 ||
26+
body instanceof ArrayBuffer)
27+
) {
2228
verboseLogs.body = body;
2329
}
2430

packages/respect-core/src/utils/__tests__/base64.test.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { deepCopy } from '../deepCopy.js';
2+
3+
describe('deepCopy', () => {
4+
it('should deep copy an object', () => {
5+
const obj = { a: 1, b: { c: 2 } };
6+
const copy = deepCopy(obj);
7+
expect(copy).toEqual(obj);
8+
});
9+
10+
it('should deep copy an object with circular references', () => {
11+
const obj = { a: 1 };
12+
// @ts-ignore
13+
obj.b = obj;
14+
const copy = deepCopy(obj);
15+
expect(copy).toEqual(obj);
16+
});
17+
18+
it('should deep copy an object with File and ArrayBuffer', () => {
19+
const file = new File(['test'], 'test.txt', { type: 'text/plain' });
20+
const arrayBuffer = new ArrayBuffer(8);
21+
const copy = deepCopy({ file, arrayBuffer });
22+
expect(copy).toEqual({ file, arrayBuffer });
23+
});
24+
25+
it('should deep copy an object with nested objects', () => {
26+
const obj = { a: 1, b: { c: 2 } };
27+
const copy = deepCopy(obj);
28+
expect(copy).toEqual(obj);
29+
});
30+
31+
it('should deep copy an object with nested objects and circular references', () => {
32+
const obj = { a: 1, b: { c: 2 } };
33+
// @ts-ignore
34+
obj.b.d = obj;
35+
const copy = deepCopy(obj);
36+
expect(copy).toEqual(obj);
37+
});
38+
});

packages/respect-core/src/utils/api-fetcher.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { collectSecretFields } from '../modules/flow-runner/index.js';
1818
import { parseWwwAuthenticateHeader } from './digest-auth/parse-www-authenticate-header.js';
1919
import { generateDigestAuthHeader } from './digest-auth/generate-digest-auth-header.js';
2020
import { isBinaryContentType } from './isBinaryContentType.js';
21-
import { arrayBufferToBase64 } from './base64.js';
2221

2322
import type { RequestData } from '../modules/flow-runner/index.js';
2423

@@ -362,12 +361,14 @@ export class ApiFetcher implements IFetcher {
362361
// Handle response body based on content type
363362
if (isBinaryContentType(responseContentType)) {
364363
const arrayBuffer = await fetchResult.arrayBuffer();
365-
responseBody = arrayBufferToBase64(arrayBuffer);
364+
responseBody = arrayBuffer;
366365
} else {
367366
responseBody = await fetchResult.text();
368367
}
369368
const transformedBody = responseBody
370-
? isJsonContentType(responseContentType)
369+
? responseBody instanceof ArrayBuffer
370+
? responseBody
371+
: isJsonContentType(responseContentType)
371372
? JSON.parse(responseBody)
372373
: responseBody
373374
: {};

0 commit comments

Comments
 (0)