import assert from 'node:assert/strict'; import { createCipheriv, randomBytes } from 'node:crypto'; import test from 'node:test'; import { createWeixinApi, decryptWeixinImage, extractWeixinFiles, extractWeixinText, normalizeWeixinApiBaseUrl, parseWeixinImageAesKey, splitWeixinText, weixinImageDownloadUrl, WeixinApiError, } from '../../../src/channels/weixin/weixin-api.mjs'; function jsonResponse(value, init) { return new Response(JSON.stringify(value), { status: 200, headers: { 'content-type': 'application/json' }, ...init, }); } function encryptImage(plaintext, key) { const cipher = createCipheriv('aes-128-ecb', key, null); return Buffer.concat([cipher.update(plaintext), cipher.final()]); } test('iLink image references download lazily from the canonical CDN and decrypt AES-128-ECB', async () => { const key = randomBytes(16); const plaintext = Buffer.from([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x01, 0x02, 0x03, ]); const ciphertext = encryptImage(plaintext, key); const calls = []; const api = createWeixinApi({ fetchImpl: async (url, init) => { calls.push({ url: url.toString(), init }); return new Response(ciphertext, { headers: { 'content-length': String(ciphertext.length) }, }); }, }); const images = api.inboundImages({ item_list: [{ type: 2, image_item: { aeskey: key.toString('hex'), media: { encrypt_query_param: 'one=two&three=four' }, }, }], }); assert.equal(images.length, 1); assert.equal(calls.length, 0); const loaded = await images[0].load({ maxBytes: 1_024 }); assert.equal(loaded.equals(plaintext), true); assert.equal( calls[0].url, 'https://novac2c.cdn.weixin.qq.com/c2c/download?encrypted_query_param=one%3Dtwo%26three%3Dfour', ); assert.equal(calls[0].init.method, 'GET'); assert.equal(calls[0].init.redirect, 'manual'); }); test('iLink native file items download lazily and decrypt without image size or type rules', async () => { const key = randomBytes(16); const plaintext = Buffer.from('weixin-native-file'); const ciphertext = encryptImage(plaintext, key); const calls = []; const files = extractWeixinFiles({ item_list: [{ type: 4, file_item: { media: { encrypt_query_param: 'native-file-ticket', aes_key: Buffer.from(key.toString('hex')).toString('base64'), encrypt_type: 1, }, file_name: '微信报告.zip', len: String(plaintext.length), }, }], }, { fetchImpl: async (url, init) => { calls.push({ url: url.toString(), init }); return new Response(ciphertext); }, }); assert.equal(files.length, 1); assert.equal(files[0].name, '微信报告.zip'); assert.equal(files[0].size, plaintext.length); assert.equal(calls.length, 0, 'file download stays lazy'); assert.deepEqual(await files[0].load({}), plaintext); assert.deepEqual(calls, [{ url: 'https://novac2c.cdn.weixin.qq.com/c2c/download?encrypted_query_param=native-file-ticket', init: { method: 'GET', signal: undefined, redirect: 'manual' }, }]); }); test('Weixin image keys support both documented CDN encodings and reject unsafe URLs', () => { const key = randomBytes(16); assert.equal(parseWeixinImageAesKey({ aeskey: key.toString('hex') }).equals(key), true); assert.equal(parseWeixinImageAesKey({ media: { aes_key: key.toString('base64') }, }).equals(key), true); assert.equal(parseWeixinImageAesKey({ media: { aes_key: Buffer.from(key.toString('hex'), 'ascii').toString('base64') }, }).equals(key), true); assert.throws( () => parseWeixinImageAesKey({ aeskey: 'not-a-key' }), (error) => error instanceof WeixinApiError && error.code === 'invalid-image-key', ); assert.equal( weixinImageDownloadUrl({ full_url: 'https://novac2c.cdn.weixin.qq.com/c2c/download?id=one#fragment' }), 'https://novac2c.cdn.weixin.qq.com/c2c/download?id=one', ); assert.throws( () => weixinImageDownloadUrl({ full_url: 'https://attacker.example/c2c/download?id=one' }), (error) => error instanceof WeixinApiError && error.code === 'untrusted-image-url', ); const encrypted = encryptImage(Buffer.from('image payload'), key); assert.equal(decryptWeixinImage(encrypted, key).toString(), 'image payload'); }); test('QR login uses the Tencent iLink headers and keeps local tokens out of the URL', async () => { const calls = []; const api = createWeixinApi({ fetchImpl: async (url, init) => { calls.push({ url: url.toString(), init }); return jsonResponse({ qrcode: 'private-qr-token', qrcode_img_content: 'https://liteapp.weixin.qq.com/q/example', }); }, }); const login = await api.beginLogin({ localTokens: [' token-a ', 'token-a', 'token-b'] }); assert.deepEqual(login, { qrcode: 'private-qr-token', qrcodeUrl: 'https://liteapp.weixin.qq.com/q/example', }); assert.match(calls[0].url, /ilink\/bot\/get_bot_qrcode\?bot_type=3$/); assert.doesNotMatch(calls[0].url, /token-a|token-b/); assert.equal(calls[0].init.headers['iLink-App-Id'], 'bot'); assert.equal(calls[0].init.headers['iLink-App-ClientVersion'], String((2 << 16) | (4 << 8) | 6)); assert.equal(calls[0].init.headers.Authorization, undefined); assert.deepEqual(JSON.parse(calls[0].init.body), { local_token_list: ['token-a', 'token-b'] }); }); test('login polling submits a verification code only to an approved Weixin host', async () => { const calls = []; const api = createWeixinApi({ fetchImpl: async (url, init) => { calls.push({ url: url.toString(), init }); return jsonResponse({ status: 'scaned' }); }, }); const status = await api.pollLogin({ qrcode: 'secret-qr', baseUrl: 'https://shard.ilinkai.weixin.qq.com', verifyCode: '123456', }); assert.equal(status.status, 'scaned'); assert.match(calls[0].url, /qrcode=secret-qr&verify_code=123456$/); assert.equal(calls[0].init.method, 'GET'); assert.equal(calls[0].init.headers.AuthorizationType, undefined); await assert.rejects( api.pollLogin({ qrcode: 'secret', baseUrl: 'https://attacker.test' }), (error) => error instanceof WeixinApiError && error.code === 'untrusted-base-url', ); assert.equal(calls.length, 1); }); test('sendText emits the iLink message envelope without reflecting the token in its body', async () => { const calls = []; const api = createWeixinApi({ fetchImpl: async (url, init) => { calls.push({ url: url.toString(), init }); return jsonResponse({ ret: 0 }); }, }); await api.sendText({ baseUrl: 'https://ilinkai.weixin.qq.com', token: 'host-only-token', toUserId: 'wx-user', text: 'Harness reply', contextToken: 'message-context', runId: 'run-1', }); assert.equal(calls[0].init.headers.Authorization, 'Bearer host-only-token'); const body = JSON.parse(calls[0].init.body); assert.equal(body.msg.to_user_id, 'wx-user'); assert.equal(body.msg.context_token, 'message-context'); assert.equal(body.msg.item_list[0].text_item.text, 'Harness reply'); assert.equal(body.base_info.channel_version, '2.4.6'); assert.equal(body.base_info.bot_agent, 'DeepSeekHarness/1.1.0'); assert.doesNotMatch(calls[0].init.body, /host-only-token/); }); test('sendFile uses the iLink 2.4.6 encrypted CDN flow and sends a native file item', async () => { const calls = []; const api = createWeixinApi({ fetchImpl: async (url, init) => { calls.push({ url: url.toString(), init }); if (url.pathname.endsWith('/getuploadurl')) { return jsonResponse({ ret: 0, upload_full_url: 'https://novac2c.cdn.weixin.qq.com/c2c/upload?ticket=one', }); } if (url.pathname === '/c2c/upload') { return new Response(null, { status: 200, headers: { 'x-encrypted-param': 'download-ticket' }, }); } return jsonResponse({ ret: 0 }); }, }); const plaintext = Buffer.from('weixin-native-file'); const result = await api.sendFile({ baseUrl: 'https://ilinkai.weixin.qq.com', token: 'host-only-token', toUserId: 'wx-user', contextToken: 'message-context', runId: 'run-1', file: { artifactId: 'artifact-one', deliveryKey: 'session:turn:artifact-one', fileName: 'result.txt', mediaType: 'text/plain', bytes: plaintext, }, }); assert.equal(calls.length, 3); const ticket = JSON.parse(calls[0].init.body); assert.equal(ticket.media_type, 3); assert.equal(ticket.to_user_id, 'wx-user'); assert.equal(ticket.rawsize, plaintext.length); assert.equal(ticket.rawfilemd5, 'ae8e1f4207c0468828419884f7329cb8'); assert.equal(ticket.filesize, 32); assert.equal(ticket.no_need_thumb, true); assert.match(ticket.aeskey, /^[0-9a-f]{32}$/); assert.equal(calls[1].init.headers.Authorization, undefined); assert.equal(calls[1].init.headers['content-type'], 'application/octet-stream'); assert.equal(calls[1].init.body.byteLength, 32); assert.equal( decryptWeixinImage(calls[1].init.body, Buffer.from(ticket.aeskey, 'hex')).equals(plaintext), true, ); const sent = JSON.parse(calls[2].init.body).msg; assert.equal(sent.context_token, 'message-context'); assert.equal(sent.run_id, 'run-1'); assert.match(sent.client_id, /^dsh-weixin-[0-9a-f]{32}$/); assert.equal(result.messageId, sent.client_id); assert.deepEqual(sent.item_list, [{ type: 4, file_item: { media: { encrypt_query_param: 'download-ticket', aes_key: Buffer.from(ticket.aeskey).toString('base64'), encrypt_type: 1, }, file_name: 'result.txt', len: String(plaintext.length), }, }]); }); test('sendImage uses the encrypted CDN flow and sends a native image item', async () => { const calls = []; const api = createWeixinApi({ fetchImpl: async (url, init) => { calls.push({ url: url.toString(), init }); if (url.pathname.endsWith('/getuploadurl')) { return jsonResponse({ ret: 0, upload_full_url: 'https://novac2c.cdn.weixin.qq.com/c2c/upload?ticket=image-one', }); } if (url.pathname === '/c2c/upload') { return new Response(null, { status: 200, headers: { 'x-encrypted-param': 'download-image-ticket' }, }); } return jsonResponse({ ret: 0 }); }, }); const plaintext = Buffer.from([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x01, 0x02, 0x03, ]); const result = await api.sendImage({ baseUrl: 'https://ilinkai.weixin.qq.com', token: 'host-only-token', toUserId: 'wx-user', contextToken: 'message-context', runId: 'run-image', file: { artifactId: 'artifact-image', deliveryKey: 'session:turn:artifact-image', fileName: 'result.png', mediaType: 'image/png', bytes: plaintext, }, }); assert.equal(calls.length, 3); const ticket = JSON.parse(calls[0].init.body); assert.equal(ticket.media_type, 1); assert.equal(ticket.to_user_id, 'wx-user'); assert.equal(ticket.rawsize, plaintext.length); assert.equal(ticket.filesize, 16); assert.equal(ticket.no_need_thumb, true); assert.match(ticket.aeskey, /^[0-9a-f]{32}$/); assert.equal( decryptWeixinImage(calls[1].init.body, Buffer.from(ticket.aeskey, 'hex')).equals(plaintext), true, ); const sent = JSON.parse(calls[2].init.body).msg; assert.equal(sent.context_token, 'message-context'); assert.equal(sent.run_id, 'run-image'); assert.match(sent.client_id, /^dsh-weixin-[0-9a-f]{32}$/); assert.equal(result.messageId, sent.client_id); assert.deepEqual(sent.item_list, [{ type: 2, image_item: { media: { encrypt_query_param: 'download-image-ticket', aes_key: Buffer.from(ticket.aeskey).toString('base64'), encrypt_type: 1, }, mid_size: 16, }, }]); }); function weixinFileRequest(overrides = {}) { return { baseUrl: 'https://ilinkai.weixin.qq.com', token: 'host-only-token', toUserId: 'wx-user', file: { artifactId: 'artifact-error-case', deliveryKey: 'session:turn:artifact-error-case', fileName: 'result.txt', mediaType: 'text/plain', bytes: Buffer.from('weixin-error-case'), }, ...overrides, }; } function weixinFileFetch(finalResponse) { return async (url, init) => { if (url.pathname.endsWith('/getuploadurl')) { return jsonResponse({ ret: 0, upload_full_url: 'https://novac2c.cdn.weixin.qq.com/c2c/upload?ticket=error-case', }); } if (url.pathname === '/c2c/upload') { return new Response(null, { status: 200, headers: { 'x-encrypted-param': 'download-error-case' }, }); } return finalResponse(url, init); }; } test('sendFile marks every ambiguous sendmessage result as uncertain', async (t) => { const cases = [ { name: 'network failure', finalResponse: async () => { throw new TypeError('private socket detail'); }, }, { name: 'timeout', finalResponse: async () => { throw new WeixinApiError('timeout', 'private timeout detail'); }, }, { name: 'invalid JSON', finalResponse: async () => new Response('{not-json', { status: 200 }), }, { name: 'HTTP 5xx', finalResponse: async () => jsonResponse({ ret: -1 }, { status: 503 }), }, ]; for (const scenario of cases) { await t.test(scenario.name, async () => { const api = createWeixinApi({ fetchImpl: weixinFileFetch(scenario.finalResponse), }); await assert.rejects( api.sendFile(weixinFileRequest()), (error) => error.code === 'artifact-delivery-uncertain' && !error.message.includes('private'), ); }); } }); test('sendImage preserves the uncertain final-delivery boundary', async () => { const api = createWeixinApi({ fetchImpl: weixinFileFetch(async () => { throw new TypeError('private socket detail'); }), }); await assert.rejects( api.sendImage(weixinFileRequest({ file: { artifactId: 'artifact-image-error', deliveryKey: 'session:turn:artifact-image-error', fileName: 'result.png', mediaType: 'image/png', bytes: Buffer.from('image-error-case'), }, })), (error) => error.code === 'artifact-delivery-uncertain' && !error.message.includes('private'), ); }); test('sendFile maps definitive sendmessage rejection statuses without treating them as uncertain', async (t) => { const cases = [ { name: 'permission', status: 403, code: 'artifact-permission-required' }, { name: 'too large', status: 413, code: 'artifact-too-large' }, { name: 'rate limited', status: 429, code: 'artifact-rate-limited' }, { name: 'provider rejected', status: 400, code: 'artifact-provider-rejected' }, ]; for (const scenario of cases) { await t.test(scenario.name, async () => { const api = createWeixinApi({ fetchImpl: weixinFileFetch(async () => jsonResponse( { ret: scenario.status }, { status: scenario.status }, )), }); await assert.rejects( api.sendFile(weixinFileRequest()), (error) => error.code === scenario.code, ); }); } const permissionApi = createWeixinApi({ fetchImpl: weixinFileFetch(async () => jsonResponse({ ret: 403 })), }); await assert.rejects( permissionApi.sendFile(weixinFileRequest()), (error) => error.code === 'artifact-permission-required' && error.providerCode === '403', ); const api = createWeixinApi({ fetchImpl: weixinFileFetch(async () => jsonResponse({ ret: -2001 })), }); await assert.rejects( api.sendFile(weixinFileRequest()), (error) => error.code === 'artifact-provider-rejected' && error.providerCode === '-2001', ); }); test('sendFile preserves caller abort and never marks a pre-send preparation failure uncertain', async () => { let preparationCalls = 0; const preparationApi = createWeixinApi({ fetchImpl: async () => { preparationCalls += 1; throw new TypeError('private upload preparation failure'); }, }); await assert.rejects( preparationApi.sendFile(weixinFileRequest()), (error) => error.code === 'artifact-provider-failed' && error.code !== 'artifact-delivery-uncertain', ); assert.equal(preparationCalls, 1); const controller = new AbortController(); const reason = new Error('caller stopped the turn'); const abortApi = createWeixinApi({ fetchImpl: weixinFileFetch(async () => { controller.abort(reason); throw new DOMException('Aborted', 'AbortError'); }), }); await assert.rejects( abortApi.sendFile(weixinFileRequest({ signal: controller.signal })), (error) => error === reason && error.code !== 'artifact-delivery-uncertain', ); }); test('sendFile rejects a provider upload URL outside the canonical Weixin CDN', async () => { let calls = 0; const api = createWeixinApi({ fetchImpl: async () => { calls += 1; return jsonResponse({ ret: 0, upload_full_url: 'https://attacker.example/c2c/upload?ticket=one', }); }, }); await assert.rejects( api.sendFile({ baseUrl: 'https://ilinkai.weixin.qq.com', token: 'host-only-token', toUserId: 'wx-user', file: { fileName: 'result.txt', bytes: Buffer.from('safe') }, }), (error) => error instanceof WeixinApiError && error.code === 'untrusted-upload-url', ); assert.equal(calls, 1); }); test('getUpdates converts its own long-poll timeout into an empty successful poll', async () => { const api = createWeixinApi({ fetchImpl: async (_url, init) => new Promise((_resolve, reject) => { init.signal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError'))); }), }); const result = await api.getUpdates({ baseUrl: 'https://ilinkai.weixin.qq.com', token: 'token', getUpdatesBuf: 'cursor', timeoutMs: 2, }); assert.deepEqual(result, { ret: 0, msgs: [], get_updates_buf: 'cursor' }); }); test('Weixin URL, inbound text, and reply chunk helpers enforce their narrow formats', () => { assert.equal( normalizeWeixinApiBaseUrl('https://ilinkai.weixin.qq.com/path'), 'https://ilinkai.weixin.qq.com/path/', ); assert.throws(() => normalizeWeixinApiBaseUrl('https://ilinkai.weixin.qq.com:444/')); assert.equal(extractWeixinText({ item_list: [{ type: 1, text_item: { text: ' 你好 ' } }] }), '你好'); assert.equal(extractWeixinText({ item_list: [{ type: 3, voice_item: { text: '语音转写' } }] }), '语音转写'); assert.deepEqual(splitWeixinText('abcdefgh', 5), ['abcde', 'fgh']); });