voice-transcription/multipart.ts
Rama main · réplica de lectura del repositorio original.
export interface MultipartField {
name: string;
value: string;
}
export interface MultipartFile {
name: string;
filename: string;
contentType: string;
data: Uint8Array;
}
// Assemble a multipart/form-data body as raw bytes so a binary attachment survives intact (a string body
// would be UTF-8 re-encoded and corrupt non-UTF-8 bytes). Pure — no ctx.
// NOTE: intentionally duplicated per plugin (plugins ship as self-contained zips) — keep all copies in
// sync; scripts/shared-copies.test.mjs fails the build when they drift.
export function buildMultipartBody(boundary: string, fields: MultipartField[], files: MultipartFile[]): Buffer {
const parts: Buffer[] = [];
for (const f of fields) {
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${f.name}"\r\n\r\n${f.value}\r\n`));
}
for (const file of files) {
// filename + contentType come from attacker-controlled WhatsApp media metadata. Strip CR/LF (and a `"`
// from the filename) so they can't break out of the part headers and inject extra multipart parts.
const filename = file.filename.replace(/[\r\n"]/g, '');
const contentType = file.contentType.replace(/[\r\n]/g, '');
parts.push(
Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="${file.name}"; filename="${filename}"\r\n` +
`Content-Type: ${contentType}\r\n\r\n`,
),
);
parts.push(Buffer.from(file.data));
parts.push(Buffer.from('\r\n'));
}
parts.push(Buffer.from(`--${boundary}--\r\n`));
return Buffer.concat(parts);
}