mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
e0b861c148
The initial remote-api implementation (3b29f3c) relied on
`https.request({ agent: SocksProxyAgent })` to route token/upload/draft
calls through the SSH tunnel. Bun's `https.request` does not honor
Node's `http.Agent` contract, so the agent was silently bypassed and
requests still originated from the local IP — defeating the entire
IP-allowlist purpose. Two follow-on issues compounded it: tests read
the real `~/.baoyu-skills/.env` because Bun's `os.homedir()` ignores
test-time `process.env.HOME` mutations, and invalid config values were
silently coerced to defaults.
P1 — Bun-portable SOCKS routing:
- Drop `socks-proxy-agent` dependency. Add `socks` direct dep.
- New `wechat-socks-http.ts`: raw TCP via `SocksClient.createConnection`
+ `tls.connect({ socket, servername })` + hand-built HTTP/1.1 (status
line parser, case-insensitive headers, chunked & content-length body
framing). Works identically under Node and Bun because it avoids
`http.Agent` entirely.
- Rewrite `wechat-http.ts` as a fetch-based local client and expose
a `WechatClient = (url, init?) => Promise<WechatHttpResponse>`
functional abstraction.
- `wechat-api.ts`: replace `agent?: http.Agent` with
`client: WechatClient = wechatHttp` on the five HTTP-touching
functions; `withSshTunnel` now yields a `WechatClient`.
- New `wechat-socks-http.test.ts` stands up a real SOCKS5 server
stub + HTTP echo server and asserts `connectionCount === 1`,
proving bytes actually traverse the proxy under both runtimes.
P2 — `HOME` honored under Bun:
- `homeDir()` reads `process.env.HOME` / `USERPROFILE` first, falling
back to `os.homedir()`. `loadWechatExtendConfig` and `loadCredentials`
use it, restoring test isolation.
P3 — Strict config validation:
- Replace lenient `toOptional*` helpers with `parsePort` /
`parsePositiveInt` / `parseStrictHostKeyChecking` that throw with
the key name. `loadWechatExtendConfig` only catches file-read
errors so parse errors surface to the caller. Flip the corresponding
test cases.
Verification:
- `npm test`: 261/261 pass.
- `bun test` in `scripts/`: 39/39 pass.
Co-authored-by: Dame5211 <1079825614@qq.com>
98 lines
2.6 KiB
TypeScript
98 lines
2.6 KiB
TypeScript
export interface WechatHttpInit {
|
|
method?: string;
|
|
headers?: Record<string, string>;
|
|
body?: string | Buffer;
|
|
}
|
|
|
|
export interface WechatHttpResponse {
|
|
status: number;
|
|
statusText: string;
|
|
headers: Record<string, string | string[] | undefined>;
|
|
buffer(): Promise<Buffer>;
|
|
text(): Promise<string>;
|
|
json<T = unknown>(): Promise<T>;
|
|
}
|
|
|
|
export type WechatClient = (
|
|
url: string,
|
|
init?: WechatHttpInit,
|
|
) => Promise<WechatHttpResponse>;
|
|
|
|
export interface MultipartFilePart {
|
|
name: string;
|
|
filename: string;
|
|
contentType: string;
|
|
data: Buffer;
|
|
}
|
|
|
|
export interface MultipartBody {
|
|
contentType: string;
|
|
body: Buffer;
|
|
}
|
|
|
|
export function buildMultipart(parts: MultipartFilePart[]): MultipartBody {
|
|
const boundary = `----WebKitFormBoundary${Date.now().toString(16)}${Math.random().toString(16).slice(2, 10)}`;
|
|
const chunks: Buffer[] = [];
|
|
|
|
for (const part of parts) {
|
|
const header =
|
|
`--${boundary}\r\n` +
|
|
`Content-Disposition: form-data; name="${part.name}"; filename="${part.filename}"\r\n` +
|
|
`Content-Type: ${part.contentType}\r\n\r\n`;
|
|
chunks.push(Buffer.from(header, "utf-8"));
|
|
chunks.push(part.data);
|
|
chunks.push(Buffer.from("\r\n", "utf-8"));
|
|
}
|
|
chunks.push(Buffer.from(`--${boundary}--\r\n`, "utf-8"));
|
|
|
|
return {
|
|
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
body: Buffer.concat(chunks),
|
|
};
|
|
}
|
|
|
|
function headersToRecord(headers: Headers): Record<string, string | string[] | undefined> {
|
|
const out: Record<string, string | string[] | undefined> = {};
|
|
headers.forEach((value, key) => {
|
|
const existing = out[key];
|
|
if (existing === undefined) {
|
|
out[key] = value;
|
|
} else if (Array.isArray(existing)) {
|
|
existing.push(value);
|
|
} else {
|
|
out[key] = [existing, value];
|
|
}
|
|
});
|
|
return out;
|
|
}
|
|
|
|
export const wechatHttp: WechatClient = async (url, init = {}) => {
|
|
const method = init.method ?? (init.body !== undefined ? "POST" : "GET");
|
|
const headers: Record<string, string> = { ...(init.headers ?? {}) };
|
|
|
|
let body: BodyInit | undefined;
|
|
if (init.body !== undefined) {
|
|
body = Buffer.isBuffer(init.body)
|
|
? new Uint8Array(init.body.buffer, init.body.byteOffset, init.body.byteLength)
|
|
: init.body;
|
|
}
|
|
|
|
const res = await fetch(url, { method, headers, body });
|
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
|
|
return {
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
headers: headersToRecord(res.headers),
|
|
async buffer() {
|
|
return buf;
|
|
},
|
|
async text() {
|
|
return buf.toString("utf-8");
|
|
},
|
|
async json<T = unknown>() {
|
|
return JSON.parse(buf.toString("utf-8")) as T;
|
|
},
|
|
};
|
|
};
|